From 8682a75b0a02dae1ad5ee64ab8204bf2d91c71af Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 16 Feb 2024 11:57:54 +0100 Subject: [PATCH 01/32] version++ --- matrix-sdk-android/build.gradle | 2 +- vector-app/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index f46fe81432f..d2d3dd2d29b 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -62,7 +62,7 @@ android { // that the app's state is completely cleared between tests. testInstrumentationRunnerArguments clearPackageData: 'true' - buildConfigField "String", "SDK_VERSION", "\"1.6.12\"" + buildConfigField "String", "SDK_VERSION", "\"1.6.14\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" diff --git a/vector-app/build.gradle b/vector-app/build.gradle index a5df2ae1d53..eed61f97b71 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -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' From 1dd73ad31e46b9417b60863ebf9a1aed8136e641 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 20 Feb 2024 12:22:59 +0100 Subject: [PATCH 02/32] Update release note with CVE and GHSA identifiers --- CHANGES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index df477df4d4b..74421a21d8b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,8 +5,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 🐛 ---------- From bc5c31d8f78d60ed10f6790a507900f7323d4f12 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 29 Feb 2024 13:58:44 +0100 Subject: [PATCH 03/32] Increase decryption failure grace period --- .../vector/app/features/analytics/DecryptionFailureTracker.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt index d596741d532..988e49023af 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt @@ -40,8 +40,8 @@ private data class DecryptionFailure( ) private typealias DetailedErrorName = Pair -private const val GRACE_PERIOD_MILLIS = 4_000 -private const val CHECK_INTERVAL = 2_000L +private const val GRACE_PERIOD_MILLIS = 30_000 +private const val CHECK_INTERVAL = 40_000L /** * Tracks decryption errors that are visible to the user. From 79462bcb90078431ce9ebeea6381639c0f462831 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 29 Feb 2024 16:25:50 +0100 Subject: [PATCH 04/32] revert previous commit --- .../vector/app/features/analytics/DecryptionFailureTracker.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt index 988e49023af..d596741d532 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt @@ -40,8 +40,8 @@ private data class DecryptionFailure( ) private typealias DetailedErrorName = Pair -private const val GRACE_PERIOD_MILLIS = 30_000 -private const val CHECK_INTERVAL = 40_000L +private const val GRACE_PERIOD_MILLIS = 4_000 +private const val CHECK_INTERVAL = 2_000L /** * Tracks decryption errors that are visible to the user. From 21d685f981a869106e032014fe22826876acb2eb Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 4 Mar 2024 10:22:35 +0100 Subject: [PATCH 05/32] Fix send button blinking with RTE --- changelog.d/send_button_blinking.bugfix | 1 + .../detail/composer/RichTextComposerLayout.kt | 27 +++++++++++++------ 2 files changed, 20 insertions(+), 8 deletions(-) create mode 100644 changelog.d/send_button_blinking.bugfix diff --git a/changelog.d/send_button_blinking.bugfix b/changelog.d/send_button_blinking.bugfix new file mode 100644 index 00000000000..d6359a659f5 --- /dev/null +++ b/changelog.d/send_button_blinking.bugfix @@ -0,0 +1 @@ +Fix send button blinking once for each character you are typing in RTE. diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt index b0923885e8d..a0d28be3652 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt @@ -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 } } } From 7e6c40b0758dc27c0c7e990295c69c5392fcdda3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Mar 2024 23:27:46 +0000 Subject: [PATCH 06/32] Bump io.element.android:wysiwyg from 2.29.0 to 2.30.0 Bumps [io.element.android:wysiwyg](https://github.com/matrix-org/matrix-wysiwyg) from 2.29.0 to 2.30.0. - [Changelog](https://github.com/matrix-org/matrix-rich-text-editor/blob/main/CHANGELOG.md) - [Commits](https://github.com/matrix-org/matrix-wysiwyg/compare/2.29.0...2.30.0) --- updated-dependencies: - dependency-name: io.element.android:wysiwyg dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 8b9125d9e8a..1cbb5c54fe1 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -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.30.0" ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$moshi", From 0df8932dac4210edd5868435bb88450c9b459b2e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Mar 2024 23:07:14 +0000 Subject: [PATCH 07/32] Bump io.element.android:wysiwyg from 2.30.0 to 2.31.0 Bumps [io.element.android:wysiwyg](https://github.com/matrix-org/matrix-wysiwyg) from 2.30.0 to 2.31.0. - [Changelog](https://github.com/matrix-org/matrix-rich-text-editor/blob/main/CHANGELOG.md) - [Commits](https://github.com/matrix-org/matrix-wysiwyg/compare/2.30.0...2.31.0) --- updated-dependencies: - dependency-name: io.element.android:wysiwyg dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 1cbb5c54fe1..e32d00b0424 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -101,7 +101,7 @@ ext.libs = [ ], element : [ 'opusencoder' : "io.element.android:opusencoder:1.1.0", - 'wysiwyg' : "io.element.android:wysiwyg:2.30.0" + 'wysiwyg' : "io.element.android:wysiwyg:2.31.0" ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$moshi", From ee591714910f38915efb0d9a941b5e0b626c21b0 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 12 Mar 2024 09:00:50 +0100 Subject: [PATCH 08/32] Fix StreamEventsManager not signaling event decryptions --- .../android/sdk/internal/crypto/DecryptRoomEventUseCase.kt | 6 ++++-- .../matrix/android/sdk/internal/crypto/RustCryptoService.kt | 5 ++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DecryptRoomEventUseCase.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DecryptRoomEventUseCase.kt index 12255d07830..25a1ffc0053 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DecryptRoomEventUseCase.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DecryptRoomEventUseCase.kt @@ -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) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt index c0407ca4e8d..5ba74f705b2 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt @@ -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)) { From c0da558c962257dc0c3e03e4239c11c19d5f553b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 Mar 2024 16:10:47 +0100 Subject: [PATCH 09/32] Ignore files created by copilot --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ab162f0755a..4752469dc1c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ /local.properties # idea files: exclude everything except dictionnaries .idea/caches +.idea/copilot .idea/libraries .idea/inspectionProfiles .idea/sonarlint From 5cd78c02aacb697f2cfe19fead1b29a4e22d9f95 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 Mar 2024 18:23:50 +0100 Subject: [PATCH 10/32] Ensure the keys are updated as soon as possible. Else it seems that we had to wait for the next sync response. --- .../crosssigning/CrossSigningSettingsViewModel.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt index 51c7928e1f3..3ea66b5dfe1 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt @@ -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(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) { From 1155c43fe07c10e46e3e187b40b028bff30e04aa Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 Mar 2024 17:47:52 +0100 Subject: [PATCH 11/32] BootstrapReAuthFragment: fix infinite loading wheel by submitting at start up. --- .../crypto/recover/BootstrapReAuthFragment.kt | 7 ++++++ .../recover/BootstrapReAuthViewModel.kt | 23 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapReAuthViewModel.kt diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapReAuthFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapReAuthFragment.kt index f32ba735a18..d259f1f7381 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapReAuthFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapReAuthFragment.kt @@ -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 -> diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapReAuthViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapReAuthViewModel.kt new file mode 100644 index 00000000000..8dedaaa15a6 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapReAuthViewModel.kt @@ -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 +} From 24c7131ab2cabda2bd092ff4f86a85938e27049c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 Mar 2024 10:56:35 +0100 Subject: [PATCH 12/32] towncrier --- changelog.d/8786.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/8786.bugfix diff --git a/changelog.d/8786.bugfix b/changelog.d/8786.bugfix new file mode 100644 index 00000000000..5b295dcf54e --- /dev/null +++ b/changelog.d/8786.bugfix @@ -0,0 +1 @@ + Fix infinite loading on secure backup setup ("Re-Authentication needed" bottom sheet). From d299ebc4f34fff9cc7ac871f680b3acbcfd1a2a7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Mar 2024 23:52:32 +0000 Subject: [PATCH 13/32] Bump io.element.android:wysiwyg from 2.31.0 to 2.34.0 Bumps [io.element.android:wysiwyg](https://github.com/matrix-org/matrix-wysiwyg) from 2.31.0 to 2.34.0. - [Changelog](https://github.com/matrix-org/matrix-rich-text-editor/blob/main/CHANGELOG.md) - [Commits](https://github.com/matrix-org/matrix-wysiwyg/compare/2.31.0...2.34.0) --- updated-dependencies: - dependency-name: io.element.android:wysiwyg dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index e32d00b0424..f2de0931b9f 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -101,7 +101,7 @@ ext.libs = [ ], element : [ 'opusencoder' : "io.element.android:opusencoder:1.1.0", - 'wysiwyg' : "io.element.android:wysiwyg:2.31.0" + 'wysiwyg' : "io.element.android:wysiwyg:2.34.0" ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$moshi", From fda38e90e56aeea81fef8f5fcb7cb30a2f218851 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 26 Mar 2024 18:28:05 +0100 Subject: [PATCH 14/32] Update analytic events --- .../app/features/analytics/plan/Error.kt | 103 ++++++++++++- .../features/analytics/plan/Interaction.kt | 41 ++++++ .../features/analytics/plan/MobileScreen.kt | 6 + .../plan/NotificationTroubleshoot.kt | 41 ++++++ .../features/analytics/plan/PollCreation.kt | 4 +- .../app/features/analytics/plan/PollEnd.kt | 2 +- .../app/features/analytics/plan/PollVote.kt | 2 +- .../features/analytics/plan/RoomModeration.kt | 137 ++++++++++++++++++ .../analytics/plan/SuperProperties.kt | 61 ++++++++ .../app/features/analytics/plan/ViewRoom.kt | 6 + 10 files changed, 398 insertions(+), 5 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/analytics/plan/NotificationTroubleshoot.kt create mode 100644 vector/src/main/java/im/vector/app/features/analytics/plan/RoomModeration.kt create mode 100644 vector/src/main/java/im/vector/app/features/analytics/plan/SuperProperties.kt diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/Error.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/Error.kt index 386c0908488..fbc598c8ebf 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/Error.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/Error.kt @@ -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().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() } } } diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/Interaction.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/Interaction.kt index 1df1b35439b..4aa84353e58 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/Interaction.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/Interaction.kt @@ -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. */ diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/MobileScreen.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/MobileScreen.kt index 59b53aaa89d..d08b0d1921a 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/MobileScreen.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/MobileScreen.kt @@ -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. */ diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/NotificationTroubleshoot.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/NotificationTroubleshoot.kt new file mode 100644 index 00000000000..9a4e6bd84b0 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/NotificationTroubleshoot.kt @@ -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? { + return mutableMapOf().apply { + put("hasError", hasError) + }.takeIf { it.isNotEmpty() } + } +} diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/PollCreation.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/PollCreation.kt index c9ee1afd47a..ebb7a3efd04 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/PollCreation.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/PollCreation.kt @@ -41,12 +41,12 @@ data class PollCreation( enum class Action { /** - * Newly created poll. + * Newly created poll */ Create, /** - * Edit of an existing poll. + * Edit of an existing poll */ Edit, } diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/PollEnd.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/PollEnd.kt index f55e39522d3..8750d70a5f9 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/PollEnd.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/PollEnd.kt @@ -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, diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/PollVote.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/PollVote.kt index 722f52bdec8..9918063ef9b 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/PollVote.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/PollVote.kt @@ -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, diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/RoomModeration.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/RoomModeration.kt new file mode 100644 index 00000000000..7dd03caa766 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/RoomModeration.kt @@ -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? { + return mutableMapOf().apply { + put("action", action.name) + role?.let { put("role", it.name) } + }.takeIf { it.isNotEmpty() } + } +} diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/SuperProperties.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/SuperProperties.kt new file mode 100644 index 00000000000..cd5d9dfdc7f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/SuperProperties.kt @@ -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? { + return mutableMapOf().apply { + appPlatform?.let { put("appPlatform", it) } + cryptoSDK?.let { put("cryptoSDK", it.name) } + cryptoSDKVersion?.let { put("cryptoSDKVersion", it) } + }.takeIf { it.isNotEmpty() } + } +} diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/ViewRoom.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/ViewRoom.kt index 366979025a5..93c166dc88b 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/ViewRoom.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/ViewRoom.kt @@ -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. */ From f559dcdd85882d03b859955dc4017db2c222fd63 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 27 Mar 2024 08:08:58 +0100 Subject: [PATCH 15/32] Add missing periods. --- .../java/im/vector/app/features/analytics/plan/Error.kt | 6 +++--- .../im/vector/app/features/analytics/plan/PollCreation.kt | 4 ++-- .../vector/app/features/analytics/plan/SuperProperties.kt | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/Error.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/Error.kt index fbc598c8ebf..98553b92586 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/Error.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/Error.kt @@ -49,14 +49,14 @@ data class Error( */ val isFederated: Boolean? = null, /** - * true if the current user is using matrix.org + * 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 + * permanent should be -1. */ val timeToDecryptMillis: Int? = null, /** @@ -65,7 +65,7 @@ data class Error( */ val userTrustsOwnIdentity: Boolean? = null, /** - * true if that unable to decrypt error was visible to the user + * true if that unable to decrypt error was visible to the user. */ val wasVisibleToUser: Boolean? = null, ) : VectorAnalyticsEvent { diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/PollCreation.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/PollCreation.kt index ebb7a3efd04..c9ee1afd47a 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/PollCreation.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/PollCreation.kt @@ -41,12 +41,12 @@ data class PollCreation( enum class Action { /** - * Newly created poll + * Newly created poll. */ Create, /** - * Edit of an existing poll + * Edit of an existing poll. */ Edit, } diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/SuperProperties.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/SuperProperties.kt index cd5d9dfdc7f..b62ae85a41a 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/SuperProperties.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/SuperProperties.kt @@ -26,7 +26,7 @@ package im.vector.app.features.analytics.plan */ data class SuperProperties( /** - * Used by web to identify the platform (Web Platform/Electron Platform) + * Used by web to identify the platform (Web Platform/Electron Platform). */ val appPlatform: String? = null, /** From 3fa3eb11298543006d8e9de983dfa670e5215b48 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 14 Mar 2024 10:54:31 +0100 Subject: [PATCH 16/32] fix rust device to CryptoDeviceInfo mapping --- .../matrix/android/sdk/internal/crypto/Device.kt | 2 -- .../android/sdk/internal/crypto/OlmMachine.kt | 14 +++++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/Device.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/Device.kt index 0bd6ed06d1c..d2865f0f65f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/Device.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/Device.kt @@ -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, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt index f90ae4a3452..4fe59fb1dde 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt @@ -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) { From 72d2199f1ae226e3503629b0172833181796a267 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 14 Mar 2024 11:04:35 +0100 Subject: [PATCH 17/32] refactor: StreamEventsManager report MXCryptoError instead of throwable --- .../org/matrix/android/sdk/api/session/LiveEventListener.kt | 3 ++- .../matrix/android/sdk/internal/session/StreamEventsManager.kt | 3 ++- vector/src/main/java/im/vector/app/UISIDetector.kt | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/LiveEventListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/LiveEventListener.kt index b4b283c86a2..ce0a01a4918 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/LiveEventListener.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/LiveEventListener.kt @@ -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) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt index ce34b0430ec..3e7beed0472 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt @@ -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 { diff --git a/vector/src/main/java/im/vector/app/UISIDetector.kt b/vector/src/main/java/im/vector/app/UISIDetector.kt index 4a9d8ae266c..7188fb0dc93 100644 --- a/vector/src/main/java/im/vector/app/UISIDetector.kt +++ b/vector/src/main/java/im/vector/app/UISIDetector.kt @@ -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 From 1f430a40159718859a5791922fb8728270faddcd Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 14 Mar 2024 11:25:44 +0100 Subject: [PATCH 18/32] Analytics tracker, support report custom properties not yet in schame --- .../im/vector/app/features/analytics/AnalyticsTracker.kt | 4 +++- .../app/features/analytics/impl/DefaultVectorAnalytics.kt | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/analytics/AnalyticsTracker.kt b/vector/src/main/java/im/vector/app/features/analytics/AnalyticsTracker.kt index 871782e473c..669202dcbb4 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/AnalyticsTracker.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/AnalyticsTracker.kt @@ -23,8 +23,10 @@ import im.vector.app.features.analytics.plan.UserProperties interface AnalyticsTracker { /** * Capture an Event. + * + * @param customProperties Some custom properties to attach to the event. */ - fun capture(event: VectorAnalyticsEvent) + fun capture(event: VectorAnalyticsEvent, customProperties: Map? = null) /** * Track a displayed screen. diff --git a/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt index 2a7d0ac975f..ff80c81ec73 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt @@ -171,11 +171,15 @@ class DefaultVectorAnalytics @Inject constructor( } } - override fun capture(event: VectorAnalyticsEvent) { + override fun capture(event: VectorAnalyticsEvent, customProperties: Map?) { Timber.tag(analyticsTag.value).d("capture($event)") posthog ?.takeIf { userConsent == true } - ?.capture(event.getName(), event.getProperties()?.toPostHogProperties()) + ?.capture( + event.getName(), + (customProperties.orEmpty() + + event.getProperties().orEmpty()).toPostHogProperties() + ) } override fun screen(screen: VectorAnalyticsScreen) { From fcc5181a2862a5dfff1c5f74731bad9d5ee2bd4f Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 14 Mar 2024 11:26:36 +0100 Subject: [PATCH 19/32] Refactor Decryption Failure Tracker and report new properties --- .../java/im/vector/app/VectorApplication.kt | 3 + .../vector/app/core/di/ActiveSessionHolder.kt | 6 - .../features/analytics/DecryptionFailure.kt | 78 +++ .../analytics/DecryptionFailureTracker.kt | 309 ++++++--- .../home/room/detail/TimelineViewModel.kt | 1 - .../timeline/factory/TimelineItemFactory.kt | 4 +- .../analytics/DecryptionFailureTrackerTest.kt | 617 ++++++++++++++++++ .../im/vector/app/test/fakes/FakeSession.kt | 5 +- .../java/im/vector/app/test/fakes/FakeUri.kt | 1 + 9 files changed, 925 insertions(+), 99 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/analytics/DecryptionFailure.kt create mode 100644 vector/src/test/java/im/vector/app/features/analytics/DecryptionFailureTrackerTest.kt diff --git a/vector-app/src/main/java/im/vector/app/VectorApplication.kt b/vector-app/src/main/java/im/vector/app/VectorApplication.kt index 7b41c127736..fe4cc3311b0 100644 --- a/vector-app/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector-app/src/main/java/im/vector/app/VectorApplication.kt @@ -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 diff --git a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt index 472d0896f9a..5523c849944 100644 --- a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt +++ b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt @@ -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() { diff --git a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailure.kt b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailure.kt new file mode 100644 index 00000000000..034b4cbcc30 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailure.kt @@ -0,0 +1,78 @@ +/* + * 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 + 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.error.toAnalyticsErrorName(), + // this is deprecated keep for backward compatibility + cryptoModule = Error.CryptoModule.Rust + ) +} + +fun DecryptionFailure.toCustomProperties(): Map { + val properties = mutableMapOf() + if (timeToDecryptMillis != null) { + properties["timeToDecryptMillis"] = timeToDecryptMillis + } else { + properties["timeToDecryptMillis"] = -1 + } + isFederated?.let { + properties["isFederated"] = it + } + properties["isMatrixDotOrg"] = isMatrixDotOrg + properties["wasVisibleToUser"] = wasVisibleOnScreen + properties["userTrustsOwnIdentity"] = ownIdentityTrustedAtTimeOfDecryptionFailure + eventLocalAgeAtDecryptionFailure?.let { + properties["eventLocalAgeAtDecryptionFailure"] = it + } + return properties +} + +private fun MXCryptoError.toAnalyticsErrorName(): Error.Name { + return if (this is MXCryptoError.Base) { + when (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 + } +} diff --git a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt index d596741d532..53c5d87770a 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt @@ -16,149 +16,280 @@ 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 - +// 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 { + + // 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() - private val scope: CoroutineScope = CoroutineScope(SupervisorJob()) - private val failures = mutableListOf() + // List of eventId that have been reported, to avoid double reporting private val alreadyReported = mutableListOf() - 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) + } + } - private fun addDecryptionFailure(failure: DecryptionFailure) { - // de duplicate - synchronized(failures) { - if (failures.none { it.failedEventId == failure.failedEventId }) { - failures.add(failure) + // LiveEventListener callbacks + + override fun onEventDecrypted(event: Event, clearEvent: JsonDict) { + Timber.v("Event decrypted ${event.eventId}") + event.eventId?.let { + post { + handleEventDecrypted(it) } } } - private fun removeFailureForEventId(eventId: String) { - synchronized(failures) { - failures.removeIfCompat { it.failedEventId == eventId } + 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 checkFailures() { - val now = clock.epochMillis() - val aggregatedErrors: Map> - synchronized(failures) { - val toReport = mutableListOf() - failures.removeAll { failure -> - (now - failure.timeStamp > GRACE_PERIOD_MILLIS).also { - if (it) { - toReport.add(failure) - } - } + 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 + } + 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 - aggregatedErrors = toReport - .groupBy { it.error.toAnalyticsErrorName() } - .mapValues { - it.value.map { it.failedEventId } - } + if (currentTicker == null) { + rescheduleTicker() } + } - 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 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) } } - 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 + 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() + val properties = decryptionFailure.toCustomProperties() + + Timber.v("capture error $error with properties $properties") + analyticsTracker.capture(error, properties) + + // now remove from tracked + trackedEventsMap.remove(decryptionFailure.failedEventId) + // mark as already reported + alreadyReported.add(decryptionFailure.failedEventId) + } + + private fun checkFailures() { + val now = clock.epochMillis() + Timber.v("Check failures now $now") + // report the definitely failed + val toReport = trackedEventsMap.values.filter { + now - it.timeStamp > MAX_WAIT_MILLIS + } + toReport.forEach { + reportFailure( + it.copy(timeToDecryptMillis = -1) + ) } - return DetailedErrorName(detailed, errorName) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 3793ed18d2e..9e3802101eb 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 84b71ceedfc..3482eaf4ad2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -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) } } } diff --git a/vector/src/test/java/im/vector/app/features/analytics/DecryptionFailureTrackerTest.kt b/vector/src/test/java/im/vector/app/features/analytics/DecryptionFailureTrackerTest.kt new file mode 100644 index 00000000000..7f2f6e4c0b1 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/analytics/DecryptionFailureTrackerTest.kt @@ -0,0 +1,617 @@ +/* + * 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 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.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(), 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(), any()) } + + verify { + fakeAnalyticsTracker.capture( + Error( + "mxc_crypto_error_type|", + Error.CryptoModule.Rust, + Error.Domain.E2EE, + Error.Name.OlmKeysNotSentError + ), + any() + ) + } + + // 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(), any()) } + + decryptionFailureTracker.stop() + } + + @Test + fun `should report time to decrypt for late decryption`() = runTest { + val fakeSession = fakeMxOrgTestSession + + val propertiesSlot = slot>() + + every { + fakeAnalyticsTracker.capture(any(), customProperties = capture(propertiesSlot)) + } 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(), any()) } + + val properties = propertiesSlot.captured + properties["timeToDecryptMillis"] shouldBeEqualTo 7000L + + decryptionFailureTracker.stop() + } + + @Test + fun `should report isMatrixDotOrg`() = runTest { + val fakeSession = fakeMxOrgTestSession + + val propertiesSlot = slot>() + + every { + fakeAnalyticsTracker.capture(any(), customProperties = capture(propertiesSlot)) + } 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() + + propertiesSlot.captured["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() + + propertiesSlot.captured["isMatrixDotOrg"] shouldBeEqualTo false + + decryptionFailureTracker.stop() + } + + @Test + fun `should report if user trusted it's identity at time of decryption`() = runTest { + val fakeSession = fakeMxOrgTestSession + + val propertiesSlot = slot>() + + every { + fakeAnalyticsTracker.capture(any(), customProperties = capture(propertiesSlot)) + } 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() + + propertiesSlot.captured["userTrustsOwnIdentity"] shouldBeEqualTo false + + decryptionFailureTracker.onEventDecrypted(event2, emptyMap()) + runCurrent() + + propertiesSlot.captured["userTrustsOwnIdentity"] shouldBeEqualTo true + + verify(exactly = 2) { fakeAnalyticsTracker.capture(any(), any()) } + + decryptionFailureTracker.stop() + } + + @Test + fun `should not report same event twice`() = runTest { + val fakeSession = fakeMxOrgTestSession + + every { + fakeAnalyticsTracker.capture(any(), 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(), any()) } + + decryptionFailureTracker.onEventDecryptionError(event, aUISIError) + + runCurrent() + + decryptionFailureTracker.onEventDecrypted(event, emptyMap()) + runCurrent() + + verify(exactly = 1) { fakeAnalyticsTracker.capture(any(), any()) } + + decryptionFailureTracker.stop() + } + + @Test + fun `should report if isFedrated`() = runTest { + val fakeSession = fakeMxOrgTestSession + + val propertiesSlot = slot>() + + every { + fakeAnalyticsTracker.capture(any(), customProperties = capture(propertiesSlot)) + } 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() + + propertiesSlot.captured["isFederated"] shouldBeEqualTo false + + decryptionFailureTracker.onEventDecrypted(event2, emptyMap()) + runCurrent() + + propertiesSlot.captured["isFederated"] shouldBeEqualTo true + + verify(exactly = 2) { fakeAnalyticsTracker.capture(any(), any()) } + + decryptionFailureTracker.stop() + } + + @Test + fun `should report if wasVisibleToUser`() = runTest { + val fakeSession = fakeMxOrgTestSession + + val propertiesSlot = slot>() + + every { + fakeAnalyticsTracker.capture(any(), customProperties = capture(propertiesSlot)) + } 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(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() + + propertiesSlot.captured["wasVisibleToUser"] shouldBeEqualTo false + + decryptionFailureTracker.onEventDecrypted(event2, emptyMap()) + runCurrent() + + propertiesSlot.captured["wasVisibleToUser"] shouldBeEqualTo true + + verify(exactly = 2) { fakeAnalyticsTracker.capture(any(), any()) } + + decryptionFailureTracker.stop() + } + + @Test + fun `should report if event relative age to session`() = runTest { + val fakeSession = fakeMxOrgTestSession + + val propertiesSlot = slot>() + + 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 + + every { + fakeAnalyticsTracker.capture(any(), customProperties = capture(propertiesSlot)) + } 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() + + propertiesSlot.captured["eventLocalAgeAtDecryptionFailure"] shouldBeEqualTo (historicalEventTimestamp - sessionCreationTime) + + decryptionFailureTracker.onEventDecrypted(liveEvent, emptyMap()) + runCurrent() + + propertiesSlot.captured["eventLocalAgeAtDecryptionFailure"] shouldBeEqualTo (liveEventTimestamp - sessionCreationTime) + propertiesSlot.captured["eventLocalAgeAtDecryptionFailure"] shouldBeEqualTo 60 * 1000L + + verify(exactly = 2) { fakeAnalyticsTracker.capture(any(), any()) } + + decryptionFailureTracker.stop() + } + + @Test + fun `should report if permanent UTD`() = runTest { + val fakeSession = fakeMxOrgTestSession + + val propertiesSlot = slot>() + + every { + fakeAnalyticsTracker.capture(any(), customProperties = capture(propertiesSlot)) + } 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(), any()) } + + propertiesSlot.captured["timeToDecryptMillis"] shouldBeEqualTo -1L + decryptionFailureTracker.stop() + } + + @Test + fun `with multiple UTD`() = runTest { + val fakeSession = fakeMxOrgTestSession + + val propertiesSlot = slot>() + + every { + fakeAnalyticsTracker.capture(any(), customProperties = capture(propertiesSlot)) + } 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(), any()) } + + decryptionFailureTracker.stop() + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt index 12da88d286c..9c791305b6a 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt @@ -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 diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeUri.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeUri.kt index 08bfac8db15..e24f14294ec 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeUri.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeUri.kt @@ -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(':')) } } From 28eead74cb72c30f7fa53e00862c06d1cd12e71e Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 14 Mar 2024 11:35:34 +0100 Subject: [PATCH 20/32] doc update --- .../java/im/vector/app/features/analytics/DecryptionFailure.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailure.kt b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailure.kt index 034b4cbcc30..f6134e291ba 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailure.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailure.kt @@ -26,7 +26,8 @@ data class DecryptionFailure( val error: MXCryptoError, val wasVisibleOnScreen: Boolean, val ownIdentityTrustedAtTimeOfDecryptionFailure: Boolean, - // If this is set, it means that the event was decrypted but late + // 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, From 4d04b276ffa7431a95844eea0af6eb11bd6b69bd Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 27 Mar 2024 10:49:08 +0100 Subject: [PATCH 21/32] use the new updated analytics events --- .../features/analytics/AnalyticsTracker.kt | 4 +- .../features/analytics/DecryptionFailure.kt | 28 ++--- .../analytics/DecryptionFailureTracker.kt | 4 +- .../analytics/impl/DefaultVectorAnalytics.kt | 5 +- .../analytics/DecryptionFailureTrackerTest.kt | 107 +++++++++--------- 5 files changed, 67 insertions(+), 81 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/analytics/AnalyticsTracker.kt b/vector/src/main/java/im/vector/app/features/analytics/AnalyticsTracker.kt index 669202dcbb4..871782e473c 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/AnalyticsTracker.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/AnalyticsTracker.kt @@ -23,10 +23,8 @@ import im.vector.app.features.analytics.plan.UserProperties interface AnalyticsTracker { /** * Capture an Event. - * - * @param customProperties Some custom properties to attach to the event. */ - fun capture(event: VectorAnalyticsEvent, customProperties: Map? = null) + fun capture(event: VectorAnalyticsEvent) /** * Track a displayed screen. diff --git a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailure.kt b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailure.kt index f6134e291ba..6dd45c570e7 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailure.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailure.kt @@ -41,29 +41,17 @@ fun DecryptionFailure.toAnalyticsEvent(): Error { domain = Error.Domain.E2EE, name = this.error.toAnalyticsErrorName(), // this is deprecated keep for backward compatibility - cryptoModule = Error.CryptoModule.Rust + cryptoModule = Error.CryptoModule.Rust, + cryptoSDK = Error.CryptoSDK.Rust, + eventLocalAgeMillis = eventLocalAgeAtDecryptionFailure?.toInt(), + isFederated = isFederated, + isMatrixDotOrg = isMatrixDotOrg, + timeToDecryptMillis = timeToDecryptMillis?.toInt() ?: -1, + wasVisibleToUser = wasVisibleOnScreen, + userTrustsOwnIdentity = ownIdentityTrustedAtTimeOfDecryptionFailure, ) } -fun DecryptionFailure.toCustomProperties(): Map { - val properties = mutableMapOf() - if (timeToDecryptMillis != null) { - properties["timeToDecryptMillis"] = timeToDecryptMillis - } else { - properties["timeToDecryptMillis"] = -1 - } - isFederated?.let { - properties["isFederated"] = it - } - properties["isMatrixDotOrg"] = isMatrixDotOrg - properties["wasVisibleToUser"] = wasVisibleOnScreen - properties["userTrustsOwnIdentity"] = ownIdentityTrustedAtTimeOfDecryptionFailure - eventLocalAgeAtDecryptionFailure?.let { - properties["eventLocalAgeAtDecryptionFailure"] = it - } - return properties -} - private fun MXCryptoError.toAnalyticsErrorName(): Error.Name { return if (this is MXCryptoError.Base) { when (errorType) { diff --git a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt index 53c5d87770a..f96d8a82626 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt @@ -268,10 +268,8 @@ class DecryptionFailureTracker @Inject constructor( private fun reportFailure(decryptionFailure: DecryptionFailure) { Timber.v("Report failure for event ${decryptionFailure.failedEventId}") val error = decryptionFailure.toAnalyticsEvent() - val properties = decryptionFailure.toCustomProperties() - Timber.v("capture error $error with properties $properties") - analyticsTracker.capture(error, properties) + analyticsTracker.capture(error) // now remove from tracked trackedEventsMap.remove(decryptionFailure.failedEventId) diff --git a/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt index ff80c81ec73..d4f34ffaf29 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt @@ -171,14 +171,13 @@ class DefaultVectorAnalytics @Inject constructor( } } - override fun capture(event: VectorAnalyticsEvent, customProperties: Map?) { + override fun capture(event: VectorAnalyticsEvent) { Timber.tag(analyticsTag.value).d("capture($event)") posthog ?.takeIf { userConsent == true } ?.capture( event.getName(), - (customProperties.orEmpty() + - event.getProperties().orEmpty()).toPostHogProperties() + event.getProperties().orEmpty().toPostHogProperties() ) } diff --git a/vector/src/test/java/im/vector/app/features/analytics/DecryptionFailureTrackerTest.kt b/vector/src/test/java/im/vector/app/features/analytics/DecryptionFailureTrackerTest.kt index 7f2f6e4c0b1..6088d2c465c 100644 --- a/vector/src/test/java/im/vector/app/features/analytics/DecryptionFailureTrackerTest.kt +++ b/vector/src/test/java/im/vector/app/features/analytics/DecryptionFailureTrackerTest.kt @@ -16,6 +16,7 @@ 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 @@ -107,7 +108,7 @@ class DecryptionFailureTrackerTest { val fakeSession = fakeMxOrgTestSession every { - fakeAnalyticsTracker.capture(any(), any()) + fakeAnalyticsTracker.capture(any()) } just runs fakeClock.givenEpoch(100_000) @@ -129,17 +130,22 @@ class DecryptionFailureTrackerTest { runCurrent() // it should report - verify(exactly = 1) { fakeAnalyticsTracker.capture(any(), any()) } + verify(exactly = 1) { fakeAnalyticsTracker.capture(any()) } verify { fakeAnalyticsTracker.capture( - Error( + im.vector.app.features.analytics.plan.Error( "mxc_crypto_error_type|", - Error.CryptoModule.Rust, - Error.Domain.E2EE, - Error.Name.OlmKeysNotSentError + 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 ), - any() ) } @@ -177,7 +183,7 @@ class DecryptionFailureTrackerTest { runCurrent() // it should not have reported it - verify(exactly = 0) { fakeAnalyticsTracker.capture(any(), any()) } + verify(exactly = 0) { fakeAnalyticsTracker.capture(any()) } decryptionFailureTracker.stop() } @@ -186,10 +192,10 @@ class DecryptionFailureTrackerTest { fun `should report time to decrypt for late decryption`() = runTest { val fakeSession = fakeMxOrgTestSession - val propertiesSlot = slot>() + val eventSlot = slot() every { - fakeAnalyticsTracker.capture(any(), customProperties = capture(propertiesSlot)) + fakeAnalyticsTracker.capture(event = capture(eventSlot)) } just runs var currentFakeTime = 100_000L @@ -217,10 +223,10 @@ class DecryptionFailureTrackerTest { runCurrent() // it should report - verify(exactly = 1) { fakeAnalyticsTracker.capture(any(), any()) } + verify(exactly = 1) { fakeAnalyticsTracker.capture(any()) } - val properties = propertiesSlot.captured - properties["timeToDecryptMillis"] shouldBeEqualTo 7000L + val error = eventSlot.captured as Error + error.timeToDecryptMillis shouldBeEqualTo 7000 decryptionFailureTracker.stop() } @@ -229,10 +235,10 @@ class DecryptionFailureTrackerTest { fun `should report isMatrixDotOrg`() = runTest { val fakeSession = fakeMxOrgTestSession - val propertiesSlot = slot>() + val eventSlot = slot() every { - fakeAnalyticsTracker.capture(any(), customProperties = capture(propertiesSlot)) + fakeAnalyticsTracker.capture(event = capture(eventSlot)) } just runs var currentFakeTime = 100_000L @@ -254,7 +260,8 @@ class DecryptionFailureTrackerTest { decryptionFailureTracker.onEventDecrypted(event, emptyMap()) runCurrent() - propertiesSlot.captured["isMatrixDotOrg"] shouldBeEqualTo true + val error = eventSlot.captured as Error + error.isMatrixDotOrg shouldBeEqualTo true val otherSession = FakeSession().apply { givenSessionParams( @@ -285,7 +292,7 @@ class DecryptionFailureTrackerTest { decryptionFailureTracker.onEventDecrypted(event2, emptyMap()) runCurrent() - propertiesSlot.captured["isMatrixDotOrg"] shouldBeEqualTo false + (eventSlot.captured as Error).isMatrixDotOrg shouldBeEqualTo false decryptionFailureTracker.stop() } @@ -294,10 +301,10 @@ class DecryptionFailureTrackerTest { fun `should report if user trusted it's identity at time of decryption`() = runTest { val fakeSession = fakeMxOrgTestSession - val propertiesSlot = slot>() + val eventSlot = slot() every { - fakeAnalyticsTracker.capture(any(), customProperties = capture(propertiesSlot)) + fakeAnalyticsTracker.capture(event = capture(eventSlot)) } just runs var currentFakeTime = 100_000L @@ -325,14 +332,14 @@ class DecryptionFailureTrackerTest { decryptionFailureTracker.onEventDecrypted(event, emptyMap()) runCurrent() - propertiesSlot.captured["userTrustsOwnIdentity"] shouldBeEqualTo false + (eventSlot.captured as Error).userTrustsOwnIdentity shouldBeEqualTo false decryptionFailureTracker.onEventDecrypted(event2, emptyMap()) runCurrent() - propertiesSlot.captured["userTrustsOwnIdentity"] shouldBeEqualTo true + (eventSlot.captured as Error).userTrustsOwnIdentity shouldBeEqualTo true - verify(exactly = 2) { fakeAnalyticsTracker.capture(any(), any()) } + verify(exactly = 2) { fakeAnalyticsTracker.capture(any()) } decryptionFailureTracker.stop() } @@ -342,7 +349,7 @@ class DecryptionFailureTrackerTest { val fakeSession = fakeMxOrgTestSession every { - fakeAnalyticsTracker.capture(any(), any()) + fakeAnalyticsTracker.capture(any()) } just runs var currentFakeTime = 100_000L @@ -365,7 +372,7 @@ class DecryptionFailureTrackerTest { decryptionFailureTracker.onEventDecrypted(event, emptyMap()) runCurrent() - verify(exactly = 1) { fakeAnalyticsTracker.capture(any(), any()) } + verify(exactly = 1) { fakeAnalyticsTracker.capture(any()) } decryptionFailureTracker.onEventDecryptionError(event, aUISIError) @@ -374,7 +381,7 @@ class DecryptionFailureTrackerTest { decryptionFailureTracker.onEventDecrypted(event, emptyMap()) runCurrent() - verify(exactly = 1) { fakeAnalyticsTracker.capture(any(), any()) } + verify(exactly = 1) { fakeAnalyticsTracker.capture(any()) } decryptionFailureTracker.stop() } @@ -383,10 +390,10 @@ class DecryptionFailureTrackerTest { fun `should report if isFedrated`() = runTest { val fakeSession = fakeMxOrgTestSession - val propertiesSlot = slot>() + val eventSlot = slot() every { - fakeAnalyticsTracker.capture(any(), customProperties = capture(propertiesSlot)) + fakeAnalyticsTracker.capture(event = capture(eventSlot)) } just runs var currentFakeTime = 100_000L @@ -415,14 +422,14 @@ class DecryptionFailureTrackerTest { decryptionFailureTracker.onEventDecrypted(event, emptyMap()) runCurrent() - propertiesSlot.captured["isFederated"] shouldBeEqualTo false + (eventSlot.captured as Error).isFederated shouldBeEqualTo false decryptionFailureTracker.onEventDecrypted(event2, emptyMap()) runCurrent() - propertiesSlot.captured["isFederated"] shouldBeEqualTo true + (eventSlot.captured as Error).isFederated shouldBeEqualTo true - verify(exactly = 2) { fakeAnalyticsTracker.capture(any(), any()) } + verify(exactly = 2) { fakeAnalyticsTracker.capture(any()) } decryptionFailureTracker.stop() } @@ -430,13 +437,11 @@ class DecryptionFailureTrackerTest { @Test fun `should report if wasVisibleToUser`() = runTest { val fakeSession = fakeMxOrgTestSession - - val propertiesSlot = slot>() + val eventSlot = slot() every { - fakeAnalyticsTracker.capture(any(), customProperties = capture(propertiesSlot)) + fakeAnalyticsTracker.capture(event = capture(eventSlot)) } just runs - var currentFakeTime = 100_000L fakeClock.givenEpoch(currentFakeTime) fakeActiveSessionDataSource.setActiveSession(fakeSession) @@ -470,14 +475,14 @@ class DecryptionFailureTrackerTest { decryptionFailureTracker.onEventDecrypted(event, emptyMap()) runCurrent() - propertiesSlot.captured["wasVisibleToUser"] shouldBeEqualTo false + (eventSlot.captured as Error).wasVisibleToUser shouldBeEqualTo false decryptionFailureTracker.onEventDecrypted(event2, emptyMap()) runCurrent() - propertiesSlot.captured["wasVisibleToUser"] shouldBeEqualTo true + (eventSlot.captured as Error).wasVisibleToUser shouldBeEqualTo true - verify(exactly = 2) { fakeAnalyticsTracker.capture(any(), any()) } + verify(exactly = 2) { fakeAnalyticsTracker.capture(any()) } decryptionFailureTracker.stop() } @@ -486,16 +491,16 @@ class DecryptionFailureTrackerTest { fun `should report if event relative age to session`() = runTest { val fakeSession = fakeMxOrgTestSession - val propertiesSlot = slot>() - 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() + every { - fakeAnalyticsTracker.capture(any(), customProperties = capture(propertiesSlot)) + fakeAnalyticsTracker.capture(event = capture(eventSlot)) } just runs fakeSession.fakeCryptoService.cryptoDeviceInfo = CryptoDeviceInfo( @@ -532,15 +537,15 @@ class DecryptionFailureTrackerTest { decryptionFailureTracker.onEventDecrypted(event, emptyMap()) runCurrent() - propertiesSlot.captured["eventLocalAgeAtDecryptionFailure"] shouldBeEqualTo (historicalEventTimestamp - sessionCreationTime) + (eventSlot.captured as Error).eventLocalAgeMillis shouldBeEqualTo (historicalEventTimestamp - sessionCreationTime).toInt() decryptionFailureTracker.onEventDecrypted(liveEvent, emptyMap()) runCurrent() - propertiesSlot.captured["eventLocalAgeAtDecryptionFailure"] shouldBeEqualTo (liveEventTimestamp - sessionCreationTime) - propertiesSlot.captured["eventLocalAgeAtDecryptionFailure"] shouldBeEqualTo 60 * 1000L + (eventSlot.captured as Error).eventLocalAgeMillis shouldBeEqualTo (liveEventTimestamp - sessionCreationTime).toInt() + (eventSlot.captured as Error).eventLocalAgeMillis shouldBeEqualTo 60 * 1000 - verify(exactly = 2) { fakeAnalyticsTracker.capture(any(), any()) } + verify(exactly = 2) { fakeAnalyticsTracker.capture(any()) } decryptionFailureTracker.stop() } @@ -549,10 +554,10 @@ class DecryptionFailureTrackerTest { fun `should report if permanent UTD`() = runTest { val fakeSession = fakeMxOrgTestSession - val propertiesSlot = slot>() + val eventSlot = slot() every { - fakeAnalyticsTracker.capture(any(), customProperties = capture(propertiesSlot)) + fakeAnalyticsTracker.capture(event = capture(eventSlot)) } just runs var currentFakeTime = 100_000L @@ -571,9 +576,9 @@ class DecryptionFailureTrackerTest { advanceTimeBy(70_000) runCurrent() - verify(exactly = 1) { fakeAnalyticsTracker.capture(any(), any()) } + verify(exactly = 1) { fakeAnalyticsTracker.capture(any()) } - propertiesSlot.captured["timeToDecryptMillis"] shouldBeEqualTo -1L + (eventSlot.captured as Error).timeToDecryptMillis shouldBeEqualTo -1 decryptionFailureTracker.stop() } @@ -581,10 +586,8 @@ class DecryptionFailureTrackerTest { fun `with multiple UTD`() = runTest { val fakeSession = fakeMxOrgTestSession - val propertiesSlot = slot>() - every { - fakeAnalyticsTracker.capture(any(), customProperties = capture(propertiesSlot)) + fakeAnalyticsTracker.capture(any()) } just runs var currentFakeTime = 100_000L @@ -610,7 +613,7 @@ class DecryptionFailureTrackerTest { advanceTimeBy(70_000) runCurrent() - verify(exactly = 11) { fakeAnalyticsTracker.capture(any(), any()) } + verify(exactly = 11) { fakeAnalyticsTracker.capture(any()) } decryptionFailureTracker.stop() } From 9ebc10f4c2b39a441690d215aec83510110da04a Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 27 Mar 2024 11:31:12 +0100 Subject: [PATCH 22/32] KDoc punctuation --- .../vector/app/features/analytics/DecryptionFailureTracker.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt index f96d8a82626..613daf3ded1 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt @@ -89,7 +89,7 @@ class DecryptionFailureTracker @Inject constructor( private var currentTicker: Job? = null /** - * Start the tracker + * Start the tracker. * * @param scope The coroutine scope to use, exposed for tests. If null, it will use the default one */ From 393f3f30309aebf7395f0e6eeb93cd743d15a800 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 27 Mar 2024 11:59:21 +0100 Subject: [PATCH 23/32] Use new error name for expected UTD --- .../features/analytics/DecryptionFailure.kt | 17 ++- .../analytics/DecryptionFailureTrackerTest.kt | 137 ++++++++++++++++++ 2 files changed, 150 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailure.kt b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailure.kt index 6dd45c570e7..f7fb177e12c 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailure.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailure.kt @@ -39,7 +39,7 @@ fun DecryptionFailure.toAnalyticsEvent(): Error { return Error( context = "mxc_crypto_error_type|${errorMsg}", domain = Error.Domain.E2EE, - name = this.error.toAnalyticsErrorName(), + name = this.toAnalyticsErrorName(), // this is deprecated keep for backward compatibility cryptoModule = Error.CryptoModule.Rust, cryptoSDK = Error.CryptoSDK.Rust, @@ -52,9 +52,10 @@ fun DecryptionFailure.toAnalyticsEvent(): Error { ) } -private fun MXCryptoError.toAnalyticsErrorName(): Error.Name { - return if (this is MXCryptoError.Base) { - when (errorType) { +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 @@ -64,4 +65,12 @@ private fun MXCryptoError.toAnalyticsErrorName(): Error.Name { } 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 } diff --git a/vector/src/test/java/im/vector/app/features/analytics/DecryptionFailureTrackerTest.kt b/vector/src/test/java/im/vector/app/features/analytics/DecryptionFailureTrackerTest.kt index 6088d2c465c..3be9a6dd182 100644 --- a/vector/src/test/java/im/vector/app/features/analytics/DecryptionFailureTrackerTest.kt +++ b/vector/src/test/java/im/vector/app/features/analytics/DecryptionFailureTrackerTest.kt @@ -35,6 +35,7 @@ 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 @@ -550,6 +551,142 @@ class DecryptionFailureTrackerTest { 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() + + 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() + + 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() + + 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 From 0f3f2b164eb035353b1c72ec51136bc3414ba320 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 27 Mar 2024 12:00:53 +0100 Subject: [PATCH 24/32] add changelog --- changelog.d/8780.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/8780.misc diff --git a/changelog.d/8780.misc b/changelog.d/8780.misc new file mode 100644 index 00000000000..03253757559 --- /dev/null +++ b/changelog.d/8780.misc @@ -0,0 +1 @@ +Improve UTD reporting by adding additional fields to the report. From 699ccf1d16be91d5d34b433a849c3ebd5687e828 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Mar 2024 23:44:13 +0000 Subject: [PATCH 25/32] Bump io.element.android:wysiwyg from 2.34.0 to 2.35.0 Bumps [io.element.android:wysiwyg](https://github.com/matrix-org/matrix-wysiwyg) from 2.34.0 to 2.35.0. - [Changelog](https://github.com/matrix-org/matrix-rich-text-editor/blob/main/CHANGELOG.md) - [Commits](https://github.com/matrix-org/matrix-wysiwyg/compare/2.34.0...2.35.0) --- updated-dependencies: - dependency-name: io.element.android:wysiwyg dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index f2de0931b9f..7c9ca635365 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -101,7 +101,7 @@ ext.libs = [ ], element : [ 'opusencoder' : "io.element.android:opusencoder:1.1.0", - 'wysiwyg' : "io.element.android:wysiwyg:2.34.0" + 'wysiwyg' : "io.element.android:wysiwyg:2.35.0" ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$moshi", From 2bbb49bdd3e85f10844844309ad1bf333a226e18 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 2 Apr 2024 09:33:10 +0200 Subject: [PATCH 26/32] Fix: should remember already reported events --- .../app/features/analytics/DecryptionFailureTracker.kt | 4 ++++ .../app/features/analytics/DecryptionFailureTrackerTest.kt | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt index 613daf3ded1..fcbc67169eb 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt @@ -201,6 +201,10 @@ class DecryptionFailureTracker @Inject constructor( // 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) } diff --git a/vector/src/test/java/im/vector/app/features/analytics/DecryptionFailureTrackerTest.kt b/vector/src/test/java/im/vector/app/features/analytics/DecryptionFailureTrackerTest.kt index 3be9a6dd182..2f11d4c2eb1 100644 --- a/vector/src/test/java/im/vector/app/features/analytics/DecryptionFailureTrackerTest.kt +++ b/vector/src/test/java/im/vector/app/features/analytics/DecryptionFailureTrackerTest.kt @@ -376,9 +376,12 @@ class DecryptionFailureTrackerTest { 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() From 752c884eaaf2c4a3aa4efff7fc2f62819a28fa15 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 2 Apr 2024 11:44:05 +0200 Subject: [PATCH 27/32] Analytics | if no property use null instead of empty map --- .../app/features/analytics/impl/DefaultVectorAnalytics.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt index d4f34ffaf29..acc6ebf51e8 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt @@ -177,7 +177,7 @@ class DefaultVectorAnalytics @Inject constructor( ?.takeIf { userConsent == true } ?.capture( event.getName(), - event.getProperties().orEmpty().toPostHogProperties() + event.getProperties()?.toPostHogProperties() ) } From 99ec61e120bec0d3fefc5155a95a720325aaddcd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 2 Apr 2024 15:11:27 +0200 Subject: [PATCH 28/32] Add action to report a user form the message detail bottom sheet. #8796 --- .../ui-strings/src/main/res/values/strings.xml | 3 +++ .../home/room/detail/RoomDetailAction.kt | 3 ++- .../home/room/detail/TimelineFragment.kt | 17 +++++++++++++++++ .../detail/timeline/action/EventSharedAction.kt | 3 +++ .../timeline/action/MessageActionsViewModel.kt | 6 ++++++ 5 files changed, 31 insertions(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 577101b0d30..2a98069c2e3 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -1953,8 +1953,11 @@ "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." "Reported as inappropriate" "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." + "Reported user" + "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." Ignore user + Report user "All messages (noisy)" "All messages" diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index 478ed4a58d8..30bcf7f8eb9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -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() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index feaad386cb5..f80855663fd 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -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) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt index 7bf9f536f20..18ff638390b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt @@ -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) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 62aed5c3c61..8809c4f0bf1 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -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, + ) + ) } } From b14cb81ece226161d12790b95a61ff68373a3234 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 2 Apr 2024 15:44:25 +0200 Subject: [PATCH 29/32] Add action to report a user form the user profile view. EventId is not relevant, but requested by the API. --- .../RoomMemberProfileAction.kt | 1 + .../RoomMemberProfileController.kt | 17 ++++++++++++---- .../RoomMemberProfileFragment.kt | 13 ++++++++++++ .../RoomMemberProfileViewEvents.kt | 1 + .../RoomMemberProfileViewModel.kt | 20 +++++++++++++++++++ 5 files changed, 48 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileAction.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileAction.kt index e2298d9b53d..874f3c73b8a 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileAction.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileAction.kt @@ -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() diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt index 9585e6aaa15..e74bad1acb1 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt @@ -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) } } } diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt index 020512af360..7ac5bfea0cc 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt @@ -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) } diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewEvents.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewEvents.kt index d04de8b9369..0bf8ef1b6e4 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewEvents.kt @@ -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() diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt index d38b2a0a692..f688793f4b4 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt @@ -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()) From 5ce080100671941bd0422b878918fb2986f30aad Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 2 Apr 2024 15:56:28 +0200 Subject: [PATCH 30/32] towncrier. --- changelog.d/8796.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/8796.misc diff --git a/changelog.d/8796.misc b/changelog.d/8796.misc new file mode 100644 index 00000000000..ea630f0aa3e --- /dev/null +++ b/changelog.d/8796.misc @@ -0,0 +1 @@ + Add a report user action in the message bottom sheet and on the user profile page. From ea9751ea8fa0e7f7b29ab2b673caa6b95739cca7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 2 Apr 2024 18:15:02 +0200 Subject: [PATCH 31/32] Changelog for version 1.6.14 --- CHANGES.md | 14 ++++++++++++++ changelog.d/8780.misc | 1 - changelog.d/8786.bugfix | 1 - changelog.d/8796.misc | 1 - changelog.d/send_button_blinking.bugfix | 1 - 5 files changed, 14 insertions(+), 4 deletions(-) delete mode 100644 changelog.d/8780.misc delete mode 100644 changelog.d/8786.bugfix delete mode 100644 changelog.d/8796.misc delete mode 100644 changelog.d/send_button_blinking.bugfix diff --git a/CHANGES.md b/CHANGES.md index 74421a21d8b..c4cd36e50fb 100644 --- a/CHANGES.md +++ b/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) ======================================= diff --git a/changelog.d/8780.misc b/changelog.d/8780.misc deleted file mode 100644 index 03253757559..00000000000 --- a/changelog.d/8780.misc +++ /dev/null @@ -1 +0,0 @@ -Improve UTD reporting by adding additional fields to the report. diff --git a/changelog.d/8786.bugfix b/changelog.d/8786.bugfix deleted file mode 100644 index 5b295dcf54e..00000000000 --- a/changelog.d/8786.bugfix +++ /dev/null @@ -1 +0,0 @@ - Fix infinite loading on secure backup setup ("Re-Authentication needed" bottom sheet). diff --git a/changelog.d/8796.misc b/changelog.d/8796.misc deleted file mode 100644 index ea630f0aa3e..00000000000 --- a/changelog.d/8796.misc +++ /dev/null @@ -1 +0,0 @@ - Add a report user action in the message bottom sheet and on the user profile page. diff --git a/changelog.d/send_button_blinking.bugfix b/changelog.d/send_button_blinking.bugfix deleted file mode 100644 index d6359a659f5..00000000000 --- a/changelog.d/send_button_blinking.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix send button blinking once for each character you are typing in RTE. From f7095d8f6b1d8012f0912eeafe205ff58ee4eb5a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 2 Apr 2024 18:15:27 +0200 Subject: [PATCH 32/32] Adding fastlane file for version 1.6.14 --- fastlane/metadata/android/en-US/changelogs/40106140.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/en-US/changelogs/40106140.txt diff --git a/fastlane/metadata/android/en-US/changelogs/40106140.txt b/fastlane/metadata/android/en-US/changelogs/40106140.txt new file mode 100644 index 00000000000..ec926749603 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40106140.txt @@ -0,0 +1,2 @@ +Main changes in this version: Bugfixes and improvements. +Full changelog: https://github.com/element-hq/element-android/releases