Skip to content

Commit

Permalink
Merge pull request #2754 from element-hq/feature/valere/expected_utd_…
Browse files Browse the repository at this point in the history
…integration

Expected UTDs due to membership support
  • Loading branch information
bmarty authored Apr 30, 2024
2 parents ae8ee87 + cf9f2c0 commit 60c8d6e
Show file tree
Hide file tree
Showing 15 changed files with 147 additions and 13 deletions.
1 change: 1 addition & 0 deletions changelog.d/2754.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for expected decryption errors due to membership (UX and analytics).
Original file line number Diff line number Diff line change
Expand Up @@ -19,36 +19,45 @@ package io.element.android.features.messages.impl.timeline.components.event
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContentProvider
import io.element.android.libraries.designsystem.icons.CompoundDrawables
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
import io.element.android.libraries.matrix.api.timeline.item.event.UtdCause
import io.element.android.libraries.ui.strings.CommonStrings

@Composable
fun TimelineItemEncryptedView(
@Suppress("UNUSED_PARAMETER") content: TimelineItemEncryptedContent,
content: TimelineItemEncryptedContent,
onContentLayoutChanged: (ContentAvoidingLayoutData) -> Unit,
modifier: Modifier = Modifier
) {
val isMembershipUtd = (content.data as? UnableToDecryptContent.Data.MegolmV1AesSha2)?.utdCause == UtdCause.Membership
val (textId, iconId) = if (isMembershipUtd) {
CommonStrings.common_unable_to_decrypt_no_access to CompoundDrawables.ic_compound_block
} else {
CommonStrings.common_waiting_for_decryption_key to CompoundDrawables.ic_compound_time
}
TimelineItemInformativeView(
text = stringResource(id = CommonStrings.common_waiting_for_decryption_key),
text = stringResource(id = textId),
iconDescription = stringResource(id = CommonStrings.dialog_title_warning),
iconResourceId = CompoundDrawables.ic_compound_time,
iconResourceId = iconId,
onContentLayoutChanged = onContentLayoutChanged,
modifier = modifier
)
}

@PreviewsDayNight
@Composable
internal fun TimelineItemEncryptedViewPreview() = ElementPreview {
internal fun TimelineItemEncryptedViewPreview(
@PreviewParameter(TimelineItemEncryptedContentProvider::class) content: TimelineItemEncryptedContent
) = ElementPreview {
TimelineItemEncryptedView(
content = TimelineItemEncryptedContent(
data = UnableToDecryptContent.Data.Unknown
),
content = content,
onContentLayoutChanged = {},
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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 io.element.android.features.messages.impl.timeline.model.event

import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
import io.element.android.libraries.matrix.api.timeline.item.event.UtdCause

open class TimelineItemEncryptedContentProvider : PreviewParameterProvider<TimelineItemEncryptedContent> {
override val values: Sequence<TimelineItemEncryptedContent>
get() = sequenceOf(
aTimelineItemEncryptedContent(),
aTimelineItemEncryptedContent(
data = UnableToDecryptContent.Data.MegolmV1AesSha2(
sessionId = "sessionId",
utdCause = UtdCause.Membership,
)
),
aTimelineItemEncryptedContent(
data = UnableToDecryptContent.Data.MegolmV1AesSha2(
sessionId = "sessionId",
utdCause = UtdCause.Unknown,
)
),
)
}

private fun aTimelineItemEncryptedContent(
data: UnableToDecryptContent.Data = UnableToDecryptContent.Data.Unknown
) = TimelineItemEncryptedContent(
data = data
)
3 changes: 1 addition & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,8 @@ kotlinpoet = "com.squareup:kotlinpoet:1.16.0"
# Analytics
posthog = "com.posthog:posthog-android:3.1.18"
sentry = "io.sentry:sentry-android:7.8.0"
# Note: only 0.19.0 will compile properly
# main branch can be tested replacing the version with main-SNAPSHOT
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.15.0"
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.20.0"

# Emojibase
matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.1.3"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ data class UnableToDecryptContent(
) : Data

data class MegolmV1AesSha2(
val sessionId: String
val sessionId: String,
val utdCause: UtdCause
) : Data

data object Unknown : Data
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* 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 io.element.android.libraries.matrix.api.timeline.item.event

enum class UtdCause {
Unknown,
Membership,
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,26 @@ import io.element.android.services.analytics.api.AnalyticsService
import org.matrix.rustcomponents.sdk.UnableToDecryptDelegate
import org.matrix.rustcomponents.sdk.UnableToDecryptInfo
import timber.log.Timber
import uniffi.matrix_sdk_crypto.UtdCause
import javax.inject.Inject

class UtdTracker @Inject constructor(
private val analyticsService: AnalyticsService,
) : UnableToDecryptDelegate {
override fun onUtd(info: UnableToDecryptInfo) {
Timber.d("onUtd for event ${info.eventId}, timeToDecryptMs: ${info.timeToDecryptMs}")
val name = when (info.cause) {
UtdCause.UNKNOWN -> Error.Name.OlmKeysNotSentError
UtdCause.MEMBERSHIP -> Error.Name.ExpectedDueToMembership
}
val event = Error(
context = null,
// Keep cryptoModule for compatibility.
cryptoModule = Error.CryptoModule.Rust,
cryptoSDK = Error.CryptoSDK.Rust,
timeToDecryptMillis = info.timeToDecryptMs?.toInt() ?: -1,
domain = Error.Domain.E2EE,
// TODO get a more specific error name from `info`
name = Error.Name.OlmKeysNotSentError,
name = name,
)
analyticsService.capture(event)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
import io.element.android.libraries.matrix.api.timeline.item.event.UtdCause
import io.element.android.libraries.matrix.impl.media.map
import io.element.android.libraries.matrix.impl.poll.map
import kotlinx.collections.immutable.toImmutableList
Expand All @@ -41,6 +42,7 @@ import org.matrix.rustcomponents.sdk.use
import org.matrix.rustcomponents.sdk.EncryptedMessage as RustEncryptedMessage
import org.matrix.rustcomponents.sdk.MembershipChange as RustMembershipChange
import org.matrix.rustcomponents.sdk.OtherState as RustOtherState
import uniffi.matrix_sdk_crypto.UtdCause as RustUtdCause

class TimelineEventContentMapper(private val eventMessageMapper: EventMessageMapper = EventMessageMapper()) {
fun map(content: TimelineItemContent): EventContent {
Expand Down Expand Up @@ -148,6 +150,13 @@ private fun RustMembershipChange.map(): MembershipChange {
}
}

private fun RustUtdCause.map(): UtdCause {
return when (this) {
RustUtdCause.MEMBERSHIP -> UtdCause.Membership
RustUtdCause.UNKNOWN -> UtdCause.Unknown
}
}

// TODO extract state events?
private fun RustOtherState.map(): OtherState {
return when (this) {
Expand Down Expand Up @@ -177,7 +186,7 @@ private fun RustOtherState.map(): OtherState {

private fun RustEncryptedMessage.map(): UnableToDecryptContent.Data {
return when (this) {
is RustEncryptedMessage.MegolmV1AesSha2 -> UnableToDecryptContent.Data.MegolmV1AesSha2(sessionId)
is RustEncryptedMessage.MegolmV1AesSha2 -> UnableToDecryptContent.Data.MegolmV1AesSha2(sessionId, cause.map())
is RustEncryptedMessage.OlmV1Curve25519AesSha2 -> UnableToDecryptContent.Data.OlmV1Curve25519AesSha2(senderKey)
RustEncryptedMessage.Unknown -> UnableToDecryptContent.Data.Unknown
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,29 @@ class UtdTrackerTest {
assertThat(fakeAnalyticsService.screenEvents).isEmpty()
assertThat(fakeAnalyticsService.trackedErrors).isEmpty()
}

@Test
fun `when onUtd is called with membership cause, the expected analytics Event is sent`() {
val fakeAnalyticsService = FakeAnalyticsService()
val sut = UtdTracker(fakeAnalyticsService)
sut.onUtd(
UnableToDecryptInfo(
eventId = AN_EVENT_ID.value,
timeToDecryptMs = 123.toULong(),
cause = UtdCause.MEMBERSHIP,
)
)
assertThat(fakeAnalyticsService.capturedEvents).containsExactly(
Error(
context = null,
cryptoModule = Error.CryptoModule.Rust,
cryptoSDK = Error.CryptoSDK.Rust,
timeToDecryptMillis = 123,
domain = Error.Domain.E2EE,
name = Error.Name.ExpectedDueToMembership
)
)
assertThat(fakeAnalyticsService.screenEvents).isEmpty()
assertThat(fakeAnalyticsService.trackedErrors).isEmpty()
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 60c8d6e

Please sign in to comment.