Merge remote-tracking branch 'origin/develop' into dependency-cleanup

This commit is contained in:
Olivér Falvai 2022-05-26 14:04:39 +02:00
commit 9ce9ad6d3a
221 changed files with 3490 additions and 918 deletions

View File

@ -36,7 +36,7 @@ Other changes
- Reformatted project code ([#5953](https://github.com/vector-im/element-android/issues/5953)) - Reformatted project code ([#5953](https://github.com/vector-im/element-android/issues/5953))
- Update check for server-side threads support to match spec. ([#5997](https://github.com/vector-im/element-android/issues/5997)) - Update check for server-side threads support to match spec. ([#5997](https://github.com/vector-im/element-android/issues/5997))
- Setup detekt ([#6038](https://github.com/vector-im/element-android/issues/6038)) - Setup detekt ([#6038](https://github.com/vector-im/element-android/issues/6038))
- Notify the user for each new message ([#46312](https://github.com/vector-im/element-android/issues/46312)) - Notify the user for each new message ([#4632](https://github.com/vector-im/element-android/issues/4632))
Changes in Element v1.4.14 (2022-05-05) Changes in Element v1.4.14 (2022-05-05)

View File

@ -27,7 +27,7 @@ buildscript {
classpath 'com.google.gms:google-services:4.3.10' classpath 'com.google.gms:google-services:4.3.10'
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.3' classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.3'
classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5' classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5'
classpath "com.likethesalad.android:stem-plugin:2.0.0" classpath "com.likethesalad.android:stem-plugin:2.1.1"
classpath 'org.owasp:dependency-check-gradle:7.1.0.1' classpath 'org.owasp:dependency-check-gradle:7.1.0.1'
classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.6.21" classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.6.21"
classpath "org.jetbrains.kotlinx:kotlinx-knit:0.4.0" classpath "org.jetbrains.kotlinx:kotlinx-knit:0.4.0"

1
changelog.d/5278.wip Normal file
View File

@ -0,0 +1 @@
Adds email input and verification screens to the new FTUE onboarding flow

1
changelog.d/5283.wip Normal file
View File

@ -0,0 +1 @@
FTUE - Adds the redesigned Sign In screen

1
changelog.d/5783.wip Normal file
View File

@ -0,0 +1 @@
FTUE - Overrides sign up flow ordering for matrix.org only

1
changelog.d/5856.bugfix Normal file
View File

@ -0,0 +1 @@
Use fixed text size in read receipt counter

1
changelog.d/6012.wip Normal file
View File

@ -0,0 +1 @@
Live location sharing: navigation from timeline to map screen

1
changelog.d/6032.bugfix Normal file
View File

@ -0,0 +1 @@
Revert: Use member name instead of room name in DM creation item

1
changelog.d/6074.bugfix Normal file
View File

@ -0,0 +1 @@
Poll refactoring with unit tests

1
changelog.d/6077.sdk Normal file
View File

@ -0,0 +1 @@
Improve replay attacks and reduce duplicate message index errors

1
changelog.d/6089.misc Normal file
View File

@ -0,0 +1 @@
Test: Ensure calling 'fail()' is not caught by the catch block

1
changelog.d/6098.feature Normal file
View File

@ -0,0 +1 @@
Labs flag for enabling live location sharing

1
changelog.d/6100.misc Normal file
View File

@ -0,0 +1 @@
Excludes transitive optional non FOSS google location dependency from fdroid builds

1
changelog.d/6103.bugfix Normal file
View File

@ -0,0 +1 @@
Glide - Use current drawable while loading new static map image

1
changelog.d/6109.bugfix Normal file
View File

@ -0,0 +1 @@
Fix sending multiple invites to a room reaching only one or two people

1
changelog.d/6123.wip Normal file
View File

@ -0,0 +1 @@
[Live location sharing] Update entity in DB when a live is timed out

1
changelog.d/6140.bugfix Normal file
View File

@ -0,0 +1 @@
Prevent widget web view from reloading on screen / orientation change

1
changelog.d/6141.misc Normal file
View File

@ -0,0 +1 @@
Downgrade gradle from 7.2.0 to 7.1.3

1
changelog.d/6148.bugfix Normal file
View File

@ -0,0 +1 @@
Fix decrypting redacted event from sending errors

View File

@ -7,7 +7,10 @@ ext.versions = [
'targetCompat' : JavaVersion.VERSION_11, 'targetCompat' : JavaVersion.VERSION_11,
] ]
def gradle = "7.2.0"
// Pinned to 7.1.3 because of https://github.com/vector-im/element-android/issues/6142
// Please test carefully before upgrading again.
def gradle = "7.1.3"
// Ref: https://kotlinlang.org/releases.html // Ref: https://kotlinlang.org/releases.html
def kotlin = "1.6.21" def kotlin = "1.6.21"
def kotlinCoroutines = "1.6.1" def kotlinCoroutines = "1.6.1"
@ -23,7 +26,7 @@ def mavericks = "2.6.1"
def glide = "4.13.2" def glide = "4.13.2"
def bigImageViewer = "1.8.1" def bigImageViewer = "1.8.1"
def jjwt = "0.11.5" def jjwt = "0.11.5"
def vanniktechEmoji = "0.9.0" def vanniktechEmoji = "0.13.0"
// Testing // Testing
def mockk = "1.12.4" def mockk = "1.12.4"
@ -51,7 +54,7 @@ ext.libs = [
'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1", 'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1",
'exifinterface' : "androidx.exifinterface:exifinterface:1.3.3", 'exifinterface' : "androidx.exifinterface:exifinterface:1.3.3",
'fragmentKtx' : "androidx.fragment:fragment-ktx:1.4.1", 'fragmentKtx' : "androidx.fragment:fragment-ktx:1.4.1",
'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.3", 'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.4",
'work' : "androidx.work:work-runtime-ktx:2.7.1", 'work' : "androidx.work:work-runtime-ktx:2.7.1",
'autoFill' : "androidx.autofill:autofill:1.1.0", 'autoFill' : "androidx.autofill:autofill:1.1.0",
'preferenceKtx' : "androidx.preference:preference-ktx:1.2.0", 'preferenceKtx' : "androidx.preference:preference-ktx:1.2.0",
@ -110,6 +113,10 @@ ext.libs = [
'mavericks' : "com.airbnb.android:mavericks:$mavericks", 'mavericks' : "com.airbnb.android:mavericks:$mavericks",
'mavericksTesting' : "com.airbnb.android:mavericks-testing:$mavericks" 'mavericksTesting' : "com.airbnb.android:mavericks-testing:$mavericks"
], ],
maplibre : [
'androidSdk' : "org.maplibre.gl:android-sdk:9.5.2",
'pluginAnnotation' : "org.maplibre.gl:android-plugin-annotation-v9:1.0.0"
],
mockk : [ mockk : [
'mockk' : "io.mockk:mockk:$mockk", 'mockk' : "io.mockk:mockk:$mockk",
'mockkAndroid' : "io.mockk:mockk-android:$mockk" 'mockkAndroid' : "io.mockk:mockk-android:$mockk"

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<gradient
android:angle="@integer/rtl_mirror_flip"
android:endColor="#55DFD1FF"
android:startColor="#55A5F2E0" />
</shape>
</item>
<item>
<shape>
<gradient
android:angle="90"
android:endColor="@android:color/transparent"
android:startColor="?android:colorBackground" />
</shape>
</item>
</layer-list>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources xmlns:tools="http://schemas.android.com/tools">
<style name="TimelineContentStubBaseParams"> <style name="TimelineContentStubBaseParams">
<item name="android:layout_width">match_parent</item> <item name="android:layout_width">match_parent</item>
@ -33,5 +33,8 @@
<item name="android:gravity">center</item> <item name="android:gravity">center</item>
</style> </style>
<style name="TimelineFixedSizeCaptionStyle" parent="@style/Widget.Vector.TextView.Caption">
<item name="android:textSize" tools:ignore="SpUsage">12dp</item>
</style>
</resources> </resources>

View File

@ -0,0 +1,44 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk
import junit.framework.TestCase.fail
/**
* Will fail the test if invoking [block] is not throwing a Throwable.
*
* @param message the failure message, if the block does not throw any Throwable
* @param failureBlock a Lambda to be able to do extra check on the thrown Throwable
* @param block the block to test
*/
internal suspend fun mustFail(
message: String = "must fail",
failureBlock: ((Throwable) -> Unit)? = null,
block: suspend () -> Unit,
) {
val isSuccess = try {
block.invoke()
true
} catch (throwable: Throwable) {
failureBlock?.invoke(throwable)
false
}
if (isSuccess) {
fail(message)
}
}

View File

@ -40,6 +40,9 @@ class RetryTestRule(val retryCount: Int = 3) : TestRule {
for (i in 0 until retryCount) { for (i in 0 until retryCount) {
try { try {
base.evaluate() base.evaluate()
if (i > 0) {
println("Retried test $i times")
}
return return
} catch (t: Throwable) { } catch (t: Throwable) {
caughtThrowable = t caughtThrowable = t

View File

@ -0,0 +1,79 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.getTimelineEvent
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestHelper
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
class DecryptRedactedEventTest : InstrumentedTest {
@Test
fun doNotFailToDecryptRedactedEvent() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val e2eRoomID = testData.roomId
val aliceSession = testData.firstSession
val bobSession = testData.secondSession!!
val roomALicePOV = aliceSession.getRoom(e2eRoomID)!!
val timelineEvent = testHelper.sendTextMessage(roomALicePOV, "Hello", 1).first()
val redactionReason = "Wrong Room"
roomALicePOV.sendService().redactEvent(timelineEvent.root, redactionReason)
// get the event from bob
testHelper.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) {
bobSession.getRoom(e2eRoomID)?.getTimelineEvent(timelineEvent.eventId)?.root?.isRedacted() == true
}
}
val eventBobPov = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(timelineEvent.eventId)!!
testHelper.runBlockingTest {
try {
val result = bobSession.cryptoService().decryptEvent(eventBobPov.root, "")
Assert.assertEquals(
"Unexpected redacted reason",
redactionReason,
result.clearEvent.toModel<Event>()?.unsignedData?.redactedEvent?.content?.get("reason")
)
Assert.assertEquals(
"Unexpected Redacted event id",
timelineEvent.eventId,
result.clearEvent.toModel<Event>()?.unsignedData?.redactedEvent?.redacts
)
} catch (failure: Throwable) {
Assert.fail("Should not throw when decrypting a redacted event")
}
}
}
}

View File

@ -23,6 +23,7 @@ import org.amshove.kluent.fail
import org.amshove.kluent.internal.assertEquals import org.amshove.kluent.internal.assertEquals
import org.junit.Assert import org.junit.Assert
import org.junit.FixMethodOrder import org.junit.FixMethodOrder
import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -62,11 +63,13 @@ import org.matrix.android.sdk.common.RetryTestRule
import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.common.TestMatrixCallback import org.matrix.android.sdk.common.TestMatrixCallback
import org.matrix.android.sdk.mustFail
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
@RunWith(JUnit4::class) @RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM) @FixMethodOrder(MethodSorters.JVM)
@LargeTest @LargeTest
@Ignore("This test fails with an unhandled exception thrown from a coroutine which terminates the entire test run.")
class E2eeSanityTests : InstrumentedTest { class E2eeSanityTests : InstrumentedTest {
@get:Rule val rule = RetryTestRule(3) @get:Rule val rule = RetryTestRule(3)
@ -525,10 +528,8 @@ class E2eeSanityTests : InstrumentedTest {
// Confirm we can decrypt one but not the other // Confirm we can decrypt one but not the other
testHelper.runBlockingTest { testHelper.runBlockingTest {
try { mustFail(message = "Should not be able to decrypt event") {
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "") newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
fail("Should not be able to decrypt event")
} catch (_: MXCryptoError) {
} }
} }

View File

@ -21,7 +21,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest import androidx.test.filters.LargeTest
import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.assertTrue import junit.framework.TestCase.assertTrue
import junit.framework.TestCase.fail
import org.amshove.kluent.internal.assertEquals import org.amshove.kluent.internal.assertEquals
import org.junit.Assert import org.junit.Assert
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
@ -47,6 +46,7 @@ import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.RetryTestRule import org.matrix.android.sdk.common.RetryTestRule
import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.mustFail
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM) @FixMethodOrder(MethodSorters.JVM)
@ -95,12 +95,10 @@ class KeyShareTests : InstrumentedTest {
assertNotNull(receivedEvent) assertNotNull(receivedEvent)
assert(receivedEvent!!.isEncrypted()) assert(receivedEvent!!.isEncrypted())
try {
commonTestHelper.runBlockingTest { commonTestHelper.runBlockingTest {
mustFail {
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
} }
fail("should fail")
} catch (failure: Throwable) {
} }
val outgoingRequestsBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequests() val outgoingRequestsBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
@ -168,12 +166,10 @@ class KeyShareTests : InstrumentedTest {
} }
} }
try {
commonTestHelper.runBlockingTest { commonTestHelper.runBlockingTest {
mustFail {
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
} }
fail("should fail")
} catch (failure: Throwable) {
} }
// Mark the device as trusted // Mark the device as trusted

View File

@ -42,6 +42,7 @@ import org.matrix.android.sdk.common.MockOkHttpInterceptor
import org.matrix.android.sdk.common.RetryTestRule import org.matrix.android.sdk.common.RetryTestRule
import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.mustFail
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM) @FixMethodOrder(MethodSorters.JVM)
@ -96,18 +97,20 @@ class WithHeldTests : InstrumentedTest {
// ============================= // =============================
// Bob should not be able to decrypt because the keys is withheld // Bob should not be able to decrypt because the keys is withheld
try {
// .. might need to wait a bit for stability? // .. might need to wait a bit for stability?
testHelper.runBlockingTest { testHelper.runBlockingTest {
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") mustFail(
} message = "This session should not be able to decrypt",
Assert.fail("This session should not be able to decrypt") failureBlock = { failure ->
} catch (failure: Throwable) {
val type = (failure as MXCryptoError.Base).errorType val type = (failure as MXCryptoError.Base).errorType
val technicalMessage = failure.technicalMessage val technicalMessage = failure.technicalMessage
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage) Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage)
} }
) {
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
}
}
// Let's see if the reply we got from bob first session is unverified // Let's see if the reply we got from bob first session is unverified
testHelper.waitWithLatch { latch -> testHelper.waitWithLatch { latch ->
@ -137,17 +140,18 @@ class WithHeldTests : InstrumentedTest {
} }
// Previous message should still be undecryptable (partially withheld session) // Previous message should still be undecryptable (partially withheld session)
try {
// .. might need to wait a bit for stability? // .. might need to wait a bit for stability?
testHelper.runBlockingTest { testHelper.runBlockingTest {
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") mustFail(
} message = "This session should not be able to decrypt",
Assert.fail("This session should not be able to decrypt") failureBlock = { failure ->
} catch (failure: Throwable) {
val type = (failure as MXCryptoError.Base).errorType val type = (failure as MXCryptoError.Base).errorType
val technicalMessage = failure.technicalMessage val technicalMessage = failure.technicalMessage
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage) Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage)
}) {
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
}
} }
testHelper.signOutAndClose(aliceSession) testHelper.signOutAndClose(aliceSession)
@ -190,17 +194,18 @@ class WithHeldTests : InstrumentedTest {
// Previous message should still be undecryptable (partially withheld session) // Previous message should still be undecryptable (partially withheld session)
val eventBobPOV = bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId) val eventBobPOV = bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)
try {
// .. might need to wait a bit for stability? // .. might need to wait a bit for stability?
testHelper.runBlockingTest { testHelper.runBlockingTest {
bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "") mustFail(
} message = "This session should not be able to decrypt",
Assert.fail("This session should not be able to decrypt") failureBlock = { failure ->
} catch (failure: Throwable) {
val type = (failure as MXCryptoError.Base).errorType val type = (failure as MXCryptoError.Base).errorType
val technicalMessage = failure.technicalMessage val technicalMessage = failure.technicalMessage
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
Assert.assertEquals("Cause should be unverified", WithHeldCode.NO_OLM.value, technicalMessage) Assert.assertEquals("Cause should be unverified", WithHeldCode.NO_OLM.value, technicalMessage)
}) {
bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "")
}
} }
// Ensure that alice has marked the session to be shared with bob // Ensure that alice has marked the session to be shared with bob

View File

@ -24,6 +24,7 @@ import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.FixMethodOrder import org.junit.FixMethodOrder
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.MethodSorters import org.junit.runners.MethodSorters
@ -45,6 +46,7 @@ import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestHelper import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.RetryTestRule
import org.matrix.android.sdk.common.TestConstants import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.common.TestMatrixCallback import org.matrix.android.sdk.common.TestMatrixCallback
import java.util.Collections import java.util.Collections
@ -55,6 +57,8 @@ import java.util.concurrent.CountDownLatch
@LargeTest @LargeTest
class KeysBackupTest : InstrumentedTest { class KeysBackupTest : InstrumentedTest {
@get:Rule val rule = RetryTestRule(3)
/** /**
* - From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys * - From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys
* - Check backup keys after having marked one as backed up * - Check backup keys after having marked one as backed up

View File

@ -0,0 +1,115 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.replayattack
import androidx.test.filters.LargeTest
import org.amshove.kluent.internal.assertFailsWith
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.fail
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestHelper
@RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@LargeTest
class ReplayAttackTest : InstrumentedTest {
@Test
fun replayAttackAlreadyDecryptedEventTest() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val e2eRoomID = cryptoTestData.roomId
// Alice
val aliceSession = cryptoTestData.firstSession
val aliceRoomPOV = aliceSession.roomService().getRoom(e2eRoomID)!!
// Bob
val bobSession = cryptoTestData.secondSession
val bobRoomPOV = bobSession!!.roomService().getRoom(e2eRoomID)!!
assertEquals(bobRoomPOV.roomSummary()?.joinedMembersCount, 2)
// Alice will send a message
val sentEvents = testHelper.sendTextMessage(aliceRoomPOV, "Hello I will be decrypted twice", 1)
assertEquals(1, sentEvents.size)
val fakeEventId = sentEvents[0].eventId + "_fake"
val fakeEventWithTheSameIndex =
sentEvents[0].copy(eventId = fakeEventId, root = sentEvents[0].root.copy(eventId = fakeEventId))
testHelper.runBlockingTest {
// Lets assume we are from the main timelineId
val timelineId = "timelineId"
// Lets decrypt the original event
aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId)
// Lets decrypt the fake event that will have the same message index
val exception = assertFailsWith<MXCryptoError.Base> {
// An exception should be thrown while the same index would have been used for the previous decryption
aliceSession.cryptoService().decryptEvent(fakeEventWithTheSameIndex.root, timelineId)
}
assertEquals(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, exception.errorType)
}
cryptoTestData.cleanUp(testHelper)
}
@Test
fun replayAttackSameEventTest() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val e2eRoomID = cryptoTestData.roomId
// Alice
val aliceSession = cryptoTestData.firstSession
val aliceRoomPOV = aliceSession.roomService().getRoom(e2eRoomID)!!
// Bob
val bobSession = cryptoTestData.secondSession
val bobRoomPOV = bobSession!!.roomService().getRoom(e2eRoomID)!!
assertEquals(bobRoomPOV.roomSummary()?.joinedMembersCount, 2)
// Alice will send a message
val sentEvents = testHelper.sendTextMessage(aliceRoomPOV, "Hello I will be decrypted twice", 1)
Assert.assertTrue("Message should be sent", sentEvents.size == 1)
assertEquals(sentEvents.size, 1)
testHelper.runBlockingTest {
// Lets assume we are from the main timelineId
val timelineId = "timelineId"
// Lets decrypt the original event
aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId)
try {
// Lets try to decrypt the same event
aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId)
} catch (ex: Throwable) {
fail("Shouldn't throw a decryption error for same event")
}
}
cryptoTestData.cleanUp(testHelper)
}
}

View File

@ -33,7 +33,7 @@ interface ContentScannerService {
/** /**
* Get the current public curve25519 key that the AV server is advertising. * Get the current public curve25519 key that the AV server is advertising.
* @param callback on success callback containing the server public key * @param forceDownload true to force the SDK to download again the server public key
*/ */
suspend fun getServerPublicKey(forceDownload: Boolean = false): String? suspend fun getServerPublicKey(forceDownload: Boolean = false): String?
suspend fun getScanResultForAttachment(mxcUrl: String, fileInfo: ElementToDecrypt? = null): ScanStatusInfo suspend fun getScanResultForAttachment(mxcUrl: String, fileInfo: ElementToDecrypt? = null): ScanStatusInfo

View File

@ -48,8 +48,7 @@ data class IncomingRoomKeyRequest(
/** /**
* Factory. * Factory.
* *
* @param event the event * @param trail the AuditTrail data
* @param currentTimeMillis the current time in milliseconds
*/ */
fun fromEvent(trail: AuditTrail): IncomingRoomKeyRequest? { fun fromEvent(trail: AuditTrail): IncomingRoomKeyRequest? {
return trail return trail

View File

@ -46,8 +46,8 @@ class MXUsersDevicesMap<E> {
/** /**
* Provides the object for a device id and a user Id. * Provides the object for a device id and a user Id.
* *
* @param userId the user id
* @param deviceId the device id * @param deviceId the device id
* @param userId the object id
* @return the object * @return the object
*/ */
fun getObject(userId: String?, deviceId: String?): E? { fun getObject(userId: String?, deviceId: String?): E? {
@ -73,8 +73,8 @@ class MXUsersDevicesMap<E> {
/** /**
* Defines the objects map for a user Id. * Defines the objects map for a user Id.
* *
* @param objectsPerDevices the objects maps
* @param userId the user id * @param userId the user id
* @param objectsPerDevices the objects maps
*/ */
fun setObjects(userId: String?, objectsPerDevices: Map<String, E>?) { fun setObjects(userId: String?, objectsPerDevices: Map<String, E>?) {
if (!userId.isNullOrBlank()) { if (!userId.isNullOrBlank()) {

View File

@ -33,7 +33,7 @@ interface FileService {
/** /**
* The original file is in cache, but the decrypted files can be deleted for security reason. * The original file is in cache, but the decrypted files can be deleted for security reason.
* To decrypt the file again, call [downloadFile], the encrypted file will not be downloaded again * To decrypt the file again, call [downloadFile], the encrypted file will not be downloaded again
* @param decryptedFileInCache true if the decrypted file is available. Always true for clear files. * @property decryptedFileInCache true if the decrypted file is available. Always true for clear files.
*/ */
data class InCache(val decryptedFileInCache: Boolean) : FileState() data class InCache(val decryptedFileInCache: Boolean) : FileState()
object Downloading : FileState() object Downloading : FileState()

View File

@ -74,6 +74,7 @@ interface IdentityService {
/** /**
* Submit the code that the identity server has sent to the user (in email or SMS). * Submit the code that the identity server has sent to the user (in email or SMS).
* Once successful, you will have to call [finalizeBindThreePid] * Once successful, you will have to call [finalizeBindThreePid]
* @param threePid the three pid
* @param code the code sent to the user * @param code the code sent to the user
*/ */
suspend fun submitValidationToken(threePid: ThreePid, code: String) suspend fun submitValidationToken(threePid: ThreePid, code: String)

View File

@ -99,6 +99,7 @@ interface IntegrationManagerService {
* Offers to allow or disallow a native widget domain. * Offers to allow or disallow a native widget domain.
* @param widgetType the widget type to check for * @param widgetType the widget type to check for
* @param domain the domain to check for * @param domain the domain to check for
* @param allowed true or false
*/ */
suspend fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean) suspend fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean)

View File

@ -29,6 +29,7 @@ object MatrixLinkify {
* Find the matrix spans i.e matrix id , user id ... to display them as URL. * Find the matrix spans i.e matrix id , user id ... to display them as URL.
* *
* @param spannable the text in which the matrix items has to be clickable. * @param spannable the text in which the matrix items has to be clickable.
* @param callback listener to be notified when the span is clicked
*/ */
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
fun addLinks(spannable: Spannable, callback: MatrixPermalinkSpan.Callback?): Boolean { fun addLinks(spannable: Spannable, callback: MatrixPermalinkSpan.Callback?): Boolean {

View File

@ -22,8 +22,8 @@ import org.matrix.android.sdk.api.session.permalinks.MatrixPermalinkSpan.Callbac
/** /**
* This MatrixPermalinkSpan is a clickable span which use a [Callback] to communicate back. * This MatrixPermalinkSpan is a clickable span which use a [Callback] to communicate back.
* @param url the permalink url tied to the span * @property url the permalink url tied to the span
* @param callback the callback to use. * @property callback the callback to use.
*/ */
class MatrixPermalinkSpan(private val url: String, class MatrixPermalinkSpan(private val url: String,
private val callback: Callback? = null) : ClickableSpan() { private val callback: Callback? = null) : ClickableSpan() {

View File

@ -60,6 +60,7 @@ interface PermalinkService {
* Creates a permalink for a roomId, including the via parameters. * Creates a permalink for a roomId, including the via parameters.
* *
* @param roomId the room id * @param roomId the room id
* @param viaServers the via parameter
* @param forceMatrixTo whether we should force using matrix.to base URL * @param forceMatrixTo whether we should force using matrix.to base URL
* *
* @return the permalink, or null in case of error * @return the permalink, or null in case of error
@ -90,7 +91,7 @@ interface PermalinkService {
* Creates a HTML or Markdown mention span template. Can be used to replace a mention with a permalink to mentioned user. * Creates a HTML or Markdown mention span template. Can be used to replace a mention with a permalink to mentioned user.
* Ex: "<a href=\"https://matrix.to/#/%1\$s\">%2\$s</a>" or "[%2\$s](https://matrix.to/#/%1\$s)" * Ex: "<a href=\"https://matrix.to/#/%1\$s\">%2\$s</a>" or "[%2\$s](https://matrix.to/#/%1\$s)"
* *
* @param type: type of template to create * @param type type of template to create
* @param forceMatrixTo whether we should force using matrix.to base URL * @param forceMatrixTo whether we should force using matrix.to base URL
* *
* @return the created template * @return the created template

View File

@ -34,10 +34,11 @@ interface PushRuleService {
/** /**
* Enables/Disables a push rule and updates the actions if necessary. * Enables/Disables a push rule and updates the actions if necessary.
* @param kind the rule kind
* @param ruleId the rule id
* @param enable Enables/Disables the rule * @param enable Enables/Disables the rule
* @param actions Actions to update if not null * @param actions Actions to update if not null
*/ */
suspend fun updatePushRuleActions(kind: RuleKind, ruleId: String, enable: Boolean, actions: List<Action>?) suspend fun updatePushRuleActions(kind: RuleKind, ruleId: String, enable: Boolean, actions: List<Action>?)
suspend fun removePushRule(kind: RuleKind, ruleId: String) suspend fun removePushRule(kind: RuleKind, ruleId: String)

View File

@ -28,7 +28,8 @@ interface RoomCryptoService {
/** /**
* Enable encryption of the room. * Enable encryption of the room.
* @param Use force to ensure that this algorithm will be used. Otherwise this call * @param algorithm the algorithm to set, default to [MXCRYPTO_ALGORITHM_MEGOLM]
* @param force Use force to ensure that this algorithm will be used. Otherwise this call
* will throw if encryption is already setup or if the algorithm is not supported. Only to * will throw if encryption is already setup or if the algorithm is not supported. Only to
* be used by admins to fix misconfigured encryption. * be used by admins to fix misconfigured encryption.
*/ */

View File

@ -22,6 +22,9 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocati
* Aggregation info concerning a live location share. * Aggregation info concerning a live location share.
*/ */
data class LiveLocationShareAggregatedSummary( data class LiveLocationShareAggregatedSummary(
/**
* Indicate whether the live is currently running.
*/
val isActive: Boolean?, val isActive: Boolean?,
val endOfLiveTimestampMillis: Long?, val endOfLiveTimestampMillis: Long?,
val lastLocationDataContent: MessageBeaconLocationDataContent?, val lastLocationDataContent: MessageBeaconLocationDataContent?,

View File

@ -71,8 +71,8 @@ interface RelationService {
/** /**
* Edit a poll. * Edit a poll.
* @param pollType indicates open or closed polls
* @param targetEvent The poll event to edit * @param targetEvent The poll event to edit
* @param pollType indicates open or closed polls
* @param question The edited question * @param question The edited question
* @param options The edited options * @param options The edited options
*/ */
@ -84,7 +84,9 @@ interface RelationService {
/** /**
* Edit a text message body. Limited to "m.text" contentType. * Edit a text message body. Limited to "m.text" contentType.
* @param targetEvent The event to edit * @param targetEvent The event to edit
* @param msgType the message type
* @param newBodyText The edited body * @param newBodyText The edited body
* @param newBodyAutoMarkdown true to parse markdown on the new body
* @param compatibilityBodyText The text that will appear on clients that don't support yet edition * @param compatibilityBodyText The text that will appear on clients that don't support yet edition
*/ */
fun editTextMessage(targetEvent: TimelineEvent, fun editTextMessage(targetEvent: TimelineEvent,
@ -153,8 +155,8 @@ interface RelationService {
* @param rootThreadEventId the root thread eventId * @param rootThreadEventId the root thread eventId
* @param replyInThreadText the reply text * @param replyInThreadText the reply text
* @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE * @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE
* @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML
* @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present * @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
* @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML
* @param eventReplied the event referenced by the reply within a thread * @param eventReplied the event referenced by the reply within a thread
*/ */
fun replyInThread(rootThreadEventId: String, fun replyInThread(rootThreadEventId: String,

View File

@ -71,7 +71,7 @@ interface ReadService {
/** /**
* Returns a live list of read receipts for a given event. * Returns a live list of read receipts for a given event.
* @param eventId: the event * @param eventId the event
*/ */
fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>> fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>>
} }

View File

@ -62,6 +62,7 @@ interface SendService {
* @param quotedEvent The event to which we will quote it's content. * @param quotedEvent The event to which we will quote it's content.
* @param text the text message to send * @param text the text message to send
* @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present * @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
* @param rootThreadEventId when this param is not null, the message will be sent in this specific thread
* @return a [Cancelable] * @return a [Cancelable]
*/ */
fun sendQuotedTextMessage(quotedEvent: TimelineEvent, text: String, autoMarkdown: Boolean, rootThreadEventId: String? = null): Cancelable fun sendQuotedTextMessage(quotedEvent: TimelineEvent, text: String, autoMarkdown: Boolean, rootThreadEventId: String? = null): Cancelable

View File

@ -90,23 +90,29 @@ interface StateService {
/** /**
* Get a state event of the room. * Get a state event of the room.
* @param eventType An eventType.
* @param stateKey the query which will be done on the stateKey
*/ */
fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event? fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event?
/** /**
* Get a live state event of the room. * Get a live state event of the room.
* @param eventType An eventType.
* @param stateKey the query which will be done on the stateKey
*/ */
fun getStateEventLive(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): LiveData<Optional<Event>> fun getStateEventLive(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): LiveData<Optional<Event>>
/** /**
* Get state events of the room. * Get state events of the room.
* @param eventTypes Set of eventType. If empty, all state events will be returned * @param eventTypes Set of eventType. If empty, all state events will be returned
* @param stateKey the query which will be done on the stateKey
*/ */
fun getStateEvents(eventTypes: Set<String>, stateKey: QueryStringValue = QueryStringValue.NoCondition): List<Event> fun getStateEvents(eventTypes: Set<String>, stateKey: QueryStringValue = QueryStringValue.NoCondition): List<Event>
/** /**
* Get live state events of the room. * Get live state events of the room.
* @param eventTypes Set of eventType to observe. If empty, all state events will be observed * @param eventTypes Set of eventType to observe. If empty, all state events will be observed
* @param stateKey the query which will be done on the stateKey
*/ */
fun getStateEventsLive(eventTypes: Set<String>, stateKey: QueryStringValue = QueryStringValue.NoCondition): LiveData<List<Event>> fun getStateEventsLive(eventTypes: Set<String>, stateKey: QueryStringValue = QueryStringValue.NoCondition): LiveData<List<Event>>

View File

@ -90,6 +90,7 @@ data class TimelineEvent(
/** /**
* Get the metadata associated with a key. * Get the metadata associated with a key.
* @param T type to cast the metadata to
* @param key the key to get the metadata * @param key the key to get the metadata
* @return the metadata * @return the metadata
*/ */

View File

@ -92,7 +92,7 @@ interface SharedSecretStorageService {
* Clients MUST ensure that the key is trusted before using it to encrypt secrets. * Clients MUST ensure that the key is trusted before using it to encrypt secrets.
* *
* @param name The name of the secret * @param name The name of the secret
* @param secret The secret contents. * @param secretBase64 The secret contents.
* @param keys The list of (ID,privateKey) of the keys to use to encrypt the secret. * @param keys The list of (ID,privateKey) of the keys to use to encrypt the secret.
*/ */
suspend fun storeSecret(name: String, secretBase64: String, keys: List<KeyRef>) suspend fun storeSecret(name: String, secretBase64: String, keys: List<KeyRef>)

View File

@ -44,8 +44,8 @@ interface SpaceService {
roomAliasLocalPart: String? = null): String roomAliasLocalPart: String? = null): String
/** /**
* Get a space from a roomId. * Get a space from a spaceId.
* @param spaceId the roomId to look for. * @param spaceId the spaceId to look for.
* @return a space with spaceId or null if room type is not space * @return a space with spaceId or null if room type is not space
*/ */
fun getSpace(spaceId: String): Space? fun getSpace(spaceId: String): Space?
@ -54,21 +54,24 @@ interface SpaceService {
* Try to resolve (peek) rooms and subspace in this space. * Try to resolve (peek) rooms and subspace in this space.
* Use this call get preview of children of this space, particularly useful to get a * Use this call get preview of children of this space, particularly useful to get a
* preview of rooms that you did not join yet. * preview of rooms that you did not join yet.
* @param spaceId the spaceId to look for.
*/ */
suspend fun peekSpace(spaceId: String): SpacePeekResult suspend fun peekSpace(spaceId: String): SpacePeekResult
/** /**
* Get's information of a space by querying the server. * Get's information of a space by querying the server.
*
* @param spaceId the spaceId to look for.
* @param suggestedOnly If true, return only child events and rooms where the m.space.child event has suggested: true. * @param suggestedOnly If true, return only child events and rooms where the m.space.child event has suggested: true.
* @param limit a client-defined limit to the maximum number of rooms to return per page. Must be a non-negative integer. * @param limit a client-defined limit to the maximum number of rooms to return per page. Must be a non-negative integer.
* @param from: Optional. Pagination token given to retrieve the next set of rooms. Note that if a pagination token is provided, * @param from Optional. Pagination token given to retrieve the next set of rooms. Note that if a pagination token is provided,
* then the parameters given for suggested_only and max_depth must be the same. * then the parameters given for suggested_only and max_depth must be the same.
* @param knownStateList when paginating, pass back the m.space.child state events
*/ */
suspend fun querySpaceChildren(spaceId: String, suspend fun querySpaceChildren(spaceId: String,
suggestedOnly: Boolean? = null, suggestedOnly: Boolean? = null,
limit: Int? = null, limit: Int? = null,
from: String? = null, from: String? = null,
// when paginating, pass back the m.space.child state events
knownStateList: List<Event>? = null): SpaceHierarchyData knownStateList: List<Event>? = null): SpaceHierarchyData
/** /**
@ -98,7 +101,10 @@ interface SpaceService {
/** /**
* Let this room declare that it has a parent. * Let this room declare that it has a parent.
* @param childRoomId the space to set as a child
* @param parentSpaceId the parentId which will be set
* @param canonical true if it should be the main parent of this room * @param canonical true if it should be the main parent of this room
* @param viaServers list of candidate servers that can be used to set the parent
* In practice, well behaved rooms should only have one canonical parent, but given this is not enforced: * In practice, well behaved rooms should only have one canonical parent, but given this is not enforced:
* if multiple are present the client should select the one with the lowest room ID, as determined via a lexicographic utf-8 ordering. * if multiple are present the client should select the one with the lowest room ID, as determined via a lexicographic utf-8 ordering.
*/ */

View File

@ -65,7 +65,8 @@ interface WidgetPostAPIMediator {
/** /**
* Send an object response. * Send an object response.
* *
* @param klass the class of the response * @param T the generic type
* @param type the type of the response
* @param response the response * @param response the response
* @param eventData the modular data * @param eventData the modular data
*/ */

View File

@ -111,8 +111,8 @@ interface WidgetService {
/** /**
* Deactivate a widget in a room. It makes sure you have the rights to handle this. * Deactivate a widget in a room. It makes sure you have the rights to handle this.
* *
* @param roomId: the room where you want to deactivate the widget. * @param roomId the room where you want to deactivate the widget.
* @param widgetId: the widget to deactivate. * @param widgetId the widget to deactivate.
*/ */
suspend fun destroyRoomWidget(roomId: String, widgetId: String) suspend fun destroyRoomWidget(roomId: String, widgetId: String)

View File

@ -46,6 +46,7 @@ internal class CryptoSessionInfoProvider @Inject constructor(
} }
/** /**
* @param roomId the room Id
* @param allActive if true return joined as well as invited, if false, only joined * @param allActive if true return joined as well as invited, if false, only joined
*/ */
fun getRoomUserIds(roomId: String, allActive: Boolean): List<String> { fun getRoomUserIds(roomId: String, allActive: Boolean): List<String> {

View File

@ -905,6 +905,7 @@ internal class DefaultCryptoService @Inject constructor(
/** /**
* Handle an m.room.encryption event. * Handle an m.room.encryption event.
* *
* @param roomId the room Id
* @param event the encryption event. * @param event the encryption event.
*/ */
private fun onRoomEncryptionEvent(roomId: String, event: Event) { private fun onRoomEncryptionEvent(roomId: String, event: Event) {
@ -928,6 +929,7 @@ internal class DefaultCryptoService @Inject constructor(
/** /**
* Handle a change in the membership state of a member of a room. * Handle a change in the membership state of a member of a room.
* *
* @param roomId the room Id
* @param event the membership event causing the change * @param event the membership event causing the change
*/ */
private fun onRoomMembershipEvent(roomId: String, event: Event) { private fun onRoomMembershipEvent(roomId: String, event: Event) {

View File

@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
@ -42,7 +43,7 @@ import javax.inject.Inject
private const val SEND_TO_DEVICE_RETRY_COUNT = 3 private const val SEND_TO_DEVICE_RETRY_COUNT = 3
private val loggerTag = LoggerTag("CryptoSyncHandler", LoggerTag.CRYPTO) private val loggerTag = LoggerTag("EventDecryptor", LoggerTag.CRYPTO)
@SessionScope @SessionScope
internal class EventDecryptor @Inject constructor( internal class EventDecryptor @Inject constructor(
@ -110,6 +111,16 @@ internal class EventDecryptor @Inject constructor(
if (eventContent == null) { if (eventContent == null) {
Timber.tag(loggerTag.value).e("decryptEvent : empty event content") Timber.tag(loggerTag.value).e("decryptEvent : empty event content")
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON) throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
} else if (event.isRedacted()) {
// we shouldn't attempt to decrypt a redacted event because the content is cleared and decryption will fail because of null algorithm
return MXEventDecryptionResult(
clearEvent = mapOf(
"room_id" to event.roomId.orEmpty(),
"type" to EventType.MESSAGE,
"content" to emptyMap<String, Any>(),
"unsigned" to event.unsignedData.toContent()
)
)
} else { } else {
val algorithm = eventContent["algorithm"]?.toString() val algorithm = eventContent["algorithm"]?.toString()
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm) val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)

View File

@ -140,7 +140,7 @@ internal object MXMegolmExportEncryption {
* *
* @param data the data to encrypt. * @param data the data to encrypt.
* @param password the password * @param password the password
* @param kdf_rounds the iteration count * @param kdfRounds the iteration count
* @return the encrypted data * @return the encrypted data
* @throws Exception the failure reason * @throws Exception the failure reason
*/ */

View File

@ -96,8 +96,9 @@ internal class MXOlmDevice @Inject constructor(
// So, store these message indexes per timeline id. // So, store these message indexes per timeline id.
// //
// The first level keys are timeline ids. // The first level keys are timeline ids.
// The second level keys are strings of form "<senderKey>|<session_id>|<message_index>" // The second level values is a Map that represents:
private val inboundGroupSessionMessageIndexes: MutableMap<String, MutableSet<String>> = HashMap() // "<senderKey>|<session_id>|<roomId>|<message_index>" --> eventId
private val inboundGroupSessionMessageIndexes: MutableMap<String, MutableMap<String, String>> = HashMap()
init { init {
// Retrieve the account from the store // Retrieve the account from the store
@ -429,8 +430,8 @@ internal class MXOlmDevice @Inject constructor(
* *
* @param ciphertext the base64-encoded body from the received message. * @param ciphertext the base64-encoded body from the received message.
* @param messageType message_type field from the received message. * @param messageType message_type field from the received message.
* @param theirDeviceIdentityKey the Curve25519 identity key for the remote device.
* @param sessionId the id of the active session. * @param sessionId the id of the active session.
* @param theirDeviceIdentityKey the Curve25519 identity key for the remote device.
* @return the decrypted payload. * @return the decrypted payload.
*/ */
@kotlin.jvm.Throws @kotlin.jvm.Throws
@ -755,23 +756,29 @@ internal class MXOlmDevice @Inject constructor(
* @param body the base64-encoded body of the encrypted message. * @param body the base64-encoded body of the encrypted message.
* @param roomId the room in which the message was received. * @param roomId the room in which the message was received.
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
* @param eventId the eventId of the message that will be decrypted
* @param sessionId the session identifier. * @param sessionId the session identifier.
* @param senderKey the base64-encoded curve25519 key of the sender. * @param senderKey the base64-encoded curve25519 key of the sender.
* @return the decrypting result. Nil if the sessionId is unknown. * @return the decrypting result. Null if the sessionId is unknown.
*/ */
@Throws(MXCryptoError::class) @Throws(MXCryptoError::class)
suspend fun decryptGroupMessage(body: String, suspend fun decryptGroupMessage(body: String,
roomId: String, roomId: String,
timeline: String?, timeline: String?,
eventId: String,
sessionId: String, sessionId: String,
senderKey: String): OlmDecryptionResult { senderKey: String): OlmDecryptionResult {
val sessionHolder = getInboundGroupSession(sessionId, senderKey, roomId) val sessionHolder = getInboundGroupSession(sessionId, senderKey, roomId)
val wrapper = sessionHolder.wrapper val wrapper = sessionHolder.wrapper
val inboundGroupSession = wrapper.olmInboundGroupSession val inboundGroupSession = wrapper.olmInboundGroupSession
?: throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, "Session is null") ?: throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, "Session is null")
if (roomId != wrapper.roomId) {
// Check that the room id matches the original one for the session. This stops // Check that the room id matches the original one for the session. This stops
// the HS pretending a message was targeting a different room. // the HS pretending a message was targeting a different room.
if (roomId == wrapper.roomId) { val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, wrapper.roomId)
Timber.tag(loggerTag.value).e("## decryptGroupMessage() : $reason")
throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, reason)
}
val decryptResult = try { val decryptResult = try {
sessionHolder.mutex.withLock { sessionHolder.mutex.withLock {
inboundGroupSession.decryptMessage(body) inboundGroupSession.decryptMessage(body)
@ -781,20 +788,24 @@ internal class MXOlmDevice @Inject constructor(
throw MXCryptoError.OlmError(e) throw MXCryptoError.OlmError(e)
} }
val messageIndexKey = senderKey + "|" + sessionId + "|" + roomId + "|" + decryptResult.mIndex
Timber.tag(loggerTag.value).v("##########################################################")
Timber.tag(loggerTag.value).v("## decryptGroupMessage() timeline: $timeline")
Timber.tag(loggerTag.value).v("## decryptGroupMessage() senderKey: $senderKey")
Timber.tag(loggerTag.value).v("## decryptGroupMessage() sessionId: $sessionId")
Timber.tag(loggerTag.value).v("## decryptGroupMessage() roomId: $roomId")
Timber.tag(loggerTag.value).v("## decryptGroupMessage() eventId: $eventId")
Timber.tag(loggerTag.value).v("## decryptGroupMessage() mIndex: ${decryptResult.mIndex}")
if (timeline?.isNotBlank() == true) { if (timeline?.isNotBlank() == true) {
val timelineSet = inboundGroupSessionMessageIndexes.getOrPut(timeline) { mutableSetOf() } val replayAttackMap = inboundGroupSessionMessageIndexes.getOrPut(timeline) { mutableMapOf() }
if (replayAttackMap.contains(messageIndexKey) && replayAttackMap[messageIndexKey] != eventId) {
val messageIndexKey = senderKey + "|" + sessionId + "|" + decryptResult.mIndex
if (timelineSet.contains(messageIndexKey)) {
val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex) val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex)
Timber.tag(loggerTag.value).e("## decryptGroupMessage() timelineId=$timeline: $reason") Timber.tag(loggerTag.value).e("## decryptGroupMessage() timelineId=$timeline: $reason")
throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason) throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason)
} }
replayAttackMap[messageIndexKey] = eventId
timelineSet.add(messageIndexKey)
} }
inboundGroupSessionStore.storeInBoundGroupSession(sessionHolder, sessionId, senderKey) inboundGroupSessionStore.storeInBoundGroupSession(sessionHolder, sessionId, senderKey)
val payload = try { val payload = try {
val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE) val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
@ -811,11 +822,6 @@ internal class MXOlmDevice @Inject constructor(
senderKey, senderKey,
wrapper.forwardingCurve25519KeyChain wrapper.forwardingCurve25519KeyChain
) )
} else {
val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, wrapper.roomId)
Timber.tag(loggerTag.value).e("## decryptGroupMessage() : $reason")
throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, reason)
}
} }
/** /**
@ -873,9 +879,9 @@ internal class MXOlmDevice @Inject constructor(
* Extract an InboundGroupSession from the session store and do some check. * Extract an InboundGroupSession from the session store and do some check.
* inboundGroupSessionWithIdError describes the failure reason. * inboundGroupSessionWithIdError describes the failure reason.
* *
* @param roomId the room where the session is used.
* @param sessionId the session identifier. * @param sessionId the session identifier.
* @param senderKey the base64-encoded curve25519 key of the sender. * @param senderKey the base64-encoded curve25519 key of the sender.
* @param roomId the room where the session is used.
* @return the inbound group session. * @return the inbound group session.
*/ */
fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): InboundGroupSessionHolder { fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): InboundGroupSessionHolder {

View File

@ -40,6 +40,7 @@ internal interface IMXDecrypting {
* Handle a key event. * Handle a key event.
* *
* @param event the key event. * @param event the key event.
* @param defaultKeysBackupService the keys backup service
*/ */
fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {} fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {}
} }

View File

@ -38,7 +38,7 @@ internal interface IMXGroupEncryption {
* Re-shares a session key with devices if the key has already been * Re-shares a session key with devices if the key has already been
* sent to them. * sent to them.
* *
* @param sessionId The id of the outbound session to share. * @param groupSessionId The id of the outbound session to share.
* @param userId The id of the user who owns the target device. * @param userId The id of the user who owns the target device.
* @param deviceId The id of the target device. * @param deviceId The id of the target device.
* @param senderKey The key of the originating device for the session. * @param senderKey The key of the originating device for the session.

View File

@ -78,6 +78,7 @@ internal class MXMegolmDecryption(
encryptedEventContent.ciphertext, encryptedEventContent.ciphertext,
event.roomId, event.roomId,
timeline, timeline,
eventId = event.eventId.orEmpty(),
encryptedEventContent.sessionId, encryptedEventContent.sessionId,
encryptedEventContent.senderKey encryptedEventContent.senderKey
) )
@ -176,6 +177,7 @@ internal class MXMegolmDecryption(
* Handle a key event. * Handle a key event.
* *
* @param event the key event. * @param event the key event.
* @param defaultKeysBackupService the keys backup service
*/ */
override fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) { override fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {
Timber.tag(loggerTag.value).v("onRoomKeyEvent()") Timber.tag(loggerTag.value).v("onRoomKeyEvent()")

View File

@ -180,8 +180,8 @@ internal class MXOlmDecryption(
/** /**
* Attempt to decrypt an Olm message. * Attempt to decrypt an Olm message.
* *
* @param theirDeviceIdentityKey the Curve25519 identity key of the sender.
* @param message message object, with 'type' and 'body' fields. * @param message message object, with 'type' and 'body' fields.
* @param theirDeviceIdentityKey the Curve25519 identity key of the sender.
* @return payload, if decrypted successfully. * @return payload, if decrypted successfully.
*/ */
private suspend fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? { private suspend fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? {

View File

@ -103,7 +103,7 @@ internal interface CryptoApi {
* Claim one-time keys. * Claim one-time keys.
* Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-claim * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-claim
* *
* @param params the params. * @param body the Json body.
*/ */
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/claim") @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/claim")
suspend fun claimOneTimeKeysForUsersDevices(@Body body: KeysClaimBody): KeysClaimResponse suspend fun claimOneTimeKeysForUsersDevices(@Body body: KeysClaimBody): KeysClaimResponse

View File

@ -159,6 +159,7 @@ internal object MXEncryptedAttachments {
* Encrypt an attachment stream. * Encrypt an attachment stream.
* DO NOT USE for big files, it will load all in memory * DO NOT USE for big files, it will load all in memory
* @param attachmentStream the attachment stream. Will be closed after this method call. * @param attachmentStream the attachment stream. Will be closed after this method call.
* @param clock a clock to retrieve current time
* @return the encryption file info * @return the encryption file info
*/ */
fun encryptAttachment(attachmentStream: InputStream, clock: Clock): EncryptionResult { fun encryptAttachment(attachmentStream: InputStream, clock: Clock): EncryptionResult {
@ -232,6 +233,7 @@ internal object MXEncryptedAttachments {
* @param attachmentStream the attachment stream. Will be closed after this method call. * @param attachmentStream the attachment stream. Will be closed after this method call.
* @param elementToDecrypt the elementToDecrypt info * @param elementToDecrypt the elementToDecrypt info
* @param outputStream the outputStream where the decrypted attachment will be write. * @param outputStream the outputStream where the decrypted attachment will be write.
* @param clock a clock to retrieve current time
* @return true in case of success, false in case of error * @return true in case of success, false in case of error
*/ */
fun decryptAttachment(attachmentStream: InputStream?, fun decryptAttachment(attachmentStream: InputStream?,

View File

@ -1105,6 +1105,7 @@ internal class DefaultKeysBackupService @Inject constructor(
* *
* @param password the password. * @param password the password.
* @param keysBackupData the backup and its auth data. * @param keysBackupData the backup and its auth data.
* @param progressListener listener to track progress
* *
* @return the recovery key if successful, null in other cases * @return the recovery key if successful, null in other cases
*/ */

View File

@ -44,6 +44,7 @@ internal data class GeneratePrivateKeyResult(
* Compute a private key from a password. * Compute a private key from a password.
* *
* @param password the password to use. * @param password the password to use.
* @param progressListener a listener to track progress
* *
* @return a {privateKey, salt, iterations} tuple. * @return a {privateKey, salt, iterations} tuple.
*/ */

View File

@ -72,7 +72,7 @@ internal interface RoomKeysApi {
*/ */
@PUT(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/version/{version}") @PUT(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/version/{version}")
suspend fun updateKeysBackupVersion(@Path("version") version: String, suspend fun updateKeysBackupVersion(@Path("version") version: String,
@Body keysBackupVersionBody: UpdateKeysBackupVersionBody) @Body updateKeysBackupVersionBody: UpdateKeysBackupVersionBody)
/* ========================================================================================== /* ==========================================================================================
* Storing keys * Storing keys

View File

@ -164,16 +164,14 @@ internal interface IMXCryptoStore {
/** /**
* Store the end to end account for the logged-in user. * Store the end to end account for the logged-in user.
*
* @param account the account to save
*/ */
fun saveOlmAccount() fun saveOlmAccount()
/** /**
* Retrieve a device for a user. * Retrieve a device for a user.
* *
* @param deviceId the device id.
* @param userId the user's id. * @param userId the user's id.
* @param deviceId the device id.
* @return the device * @return the device
*/ */
fun getUserDevice(userId: String, deviceId: String): CryptoDeviceInfo? fun getUserDevice(userId: String, deviceId: String): CryptoDeviceInfo?
@ -331,7 +329,7 @@ internal interface IMXCryptoStore {
/** /**
* Mark inbound group sessions as backed up on the user homeserver. * Mark inbound group sessions as backed up on the user homeserver.
* *
* @param sessions the sessions * @param olmInboundGroupSessionWrappers the sessions
*/ */
fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List<OlmInboundGroupSessionWrapper2>) fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List<OlmInboundGroupSessionWrapper2>)
@ -380,7 +378,9 @@ internal interface IMXCryptoStore {
/** /**
* Look for an existing outgoing room key request, and if none is found, add a new one. * Look for an existing outgoing room key request, and if none is found, add a new one.
* *
* @param request the request * @param requestBody the request
* @param recipients list of recipients
* @param fromIndex start index
* @return either the same instance as passed in, or the existing one. * @return either the same instance as passed in, or the existing one.
*/ */
fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>, fromIndex: Int): OutgoingKeyRequest fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>, fromIndex: Int): OutgoingKeyRequest

View File

@ -35,6 +35,11 @@ internal interface VerificationTransport {
onDone: (() -> Unit)?) onDone: (() -> Unit)?)
/** /**
* @param supportedMethods list of supported method by this client
* @param localId a local Id
* @param otherUserId the user id to send the verification request to
* @param roomId a room Id to use to send verification message
* @param toDevices list of device Ids
* @param callback will be called with eventId and ValidVerificationInfoRequest in case of success * @param callback will be called with eventId and ValidVerificationInfoRequest in case of success
*/ */
fun sendVerificationRequest(supportedMethods: List<String>, fun sendVerificationRequest(supportedMethods: List<String>,

View File

@ -96,7 +96,9 @@ internal fun EventEntity.markEventAsRoot(
/** /**
* Count the number of threads for the provided root thread eventId, and finds the latest event message. * Count the number of threads for the provided root thread eventId, and finds the latest event message.
* Note: Redactions are handled by RedactionEventProcessor. * Note: Redactions are handled by RedactionEventProcessor.
* @param realm the realm database
* @param rootThreadEventId The root eventId that will find the number of threads * @param rootThreadEventId The root eventId that will find the number of threads
* @param chunkEntity the chunk entity
* @return A ThreadSummary containing the counted threads and the latest event message * @return A ThreadSummary containing the counted threads and the latest event message
*/ */
internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: String, chunkEntity: ChunkEntity?): Summary { internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: String, chunkEntity: ChunkEntity?): Summary {
@ -184,6 +186,7 @@ private fun findLatestSortedChunkEvent(chunk: ChunkEntity, rootThreadEventId: St
/** /**
* Find all TimelineEventEntity that are root threads for the specified room. * Find all TimelineEventEntity that are root threads for the specified room.
* @param realm the realm instance
* @param roomId The room that all stored root threads will be returned * @param roomId The room that all stored root threads will be returned
*/ */
internal fun TimelineEventEntity.Companion.findAllThreadsForRoomId(realm: Realm, roomId: String): RealmQuery<TimelineEventEntity> = internal fun TimelineEventEntity.Companion.findAllThreadsForRoomId(realm: Realm, roomId: String): RealmQuery<TimelineEventEntity> =
@ -218,6 +221,7 @@ internal fun List<TimelineEvent>.mapEventsWithEdition(realm: Realm, roomId: Stri
/** /**
* Returns a list of all the marked unread threads that exists for the specified room. * Returns a list of all the marked unread threads that exists for the specified room.
* @param realm the realm instance
* @param roomId The roomId that the user is currently in * @param roomId The roomId that the user is currently in
*/ */
internal fun TimelineEventEntity.Companion.findAllLocalThreadNotificationsForRoomId(realm: Realm, roomId: String): RealmQuery<TimelineEventEntity> = internal fun TimelineEventEntity.Companion.findAllLocalThreadNotificationsForRoomId(realm: Realm, roomId: String): RealmQuery<TimelineEventEntity> =
@ -232,6 +236,7 @@ internal fun TimelineEventEntity.Companion.findAllLocalThreadNotificationsForRoo
/** /**
* Returns whether or not the given user is participating in a current thread. * Returns whether or not the given user is participating in a current thread.
* @param realm the realm instance
* @param roomId the room that the thread exists * @param roomId the room that the thread exists
* @param rootThreadEventId the thread that the search will be done * @param rootThreadEventId the thread that the search will be done
* @param senderId the user that will try to find participation * @param senderId the user that will try to find participation
@ -247,6 +252,7 @@ internal fun TimelineEventEntity.Companion.isUserParticipatingInThread(realm: Re
/** /**
* Returns whether or not the given user is mentioned in a current thread. * Returns whether or not the given user is mentioned in a current thread.
* @param realm the realm instance
* @param roomId the room that the thread exists * @param roomId the room that the thread exists
* @param rootThreadEventId the thread that the search will be done * @param rootThreadEventId the thread that the search will be done
* @param userId the user that will try to find if there is a mention * @param userId the user that will try to find if there is a mention

View File

@ -303,6 +303,7 @@ private fun getLatestEvent(rootThreadEvent: Event): Event? {
/** /**
* Find all ThreadSummaryEntity for the specified roomId, sorted by origin server. * Find all ThreadSummaryEntity for the specified roomId, sorted by origin server.
* note: Sorting cannot be provided by server, so we have to use that unstable property. * note: Sorting cannot be provided by server, so we have to use that unstable property.
* @param realm the realm instance
* @param roomId The id of the room * @param roomId The id of the room
*/ */
internal fun ThreadSummaryEntity.Companion.findAllThreadsForRoomId(realm: Realm, roomId: String): RealmQuery<ThreadSummaryEntity> = internal fun ThreadSummaryEntity.Companion.findAllThreadsForRoomId(realm: Realm, roomId: String): RealmQuery<ThreadSummaryEntity> =

View File

@ -31,6 +31,9 @@ internal open class LiveLocationShareAggregatedSummaryEntity(
var roomId: String = "", var roomId: String = "",
/**
* Indicate whether the live is currently running.
*/
var isActive: Boolean? = null, var isActive: Boolean? = null,
var endOfLiveTimestampMillis: Long? = null, var endOfLiveTimestampMillis: Long? = null,

View File

@ -55,3 +55,11 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.getOrCreate(
return LiveLocationShareAggregatedSummaryEntity.where(realm, roomId, eventId).findFirst() return LiveLocationShareAggregatedSummaryEntity.where(realm, roomId, eventId).findFirst()
?: LiveLocationShareAggregatedSummaryEntity.create(realm, roomId, eventId) ?: LiveLocationShareAggregatedSummaryEntity.create(realm, roomId, eventId)
} }
internal fun LiveLocationShareAggregatedSummaryEntity.Companion.get(
realm: Realm,
roomId: String,
eventId: String,
): LiveLocationShareAggregatedSummaryEntity? {
return LiveLocationShareAggregatedSummaryEntity.where(realm, roomId, eventId).findFirst()
}

View File

@ -32,6 +32,7 @@ import java.io.IOException
* Execute a request from the requestBlock and handle some of the Exception it could generate * Execute a request from the requestBlock and handle some of the Exception it could generate
* Ref: https://github.com/matrix-org/matrix-js-sdk/blob/develop/src/scheduler.js#L138-L175 * Ref: https://github.com/matrix-org/matrix-js-sdk/blob/develop/src/scheduler.js#L138-L175
* *
* @param DATA type of data return by the [requestBlock]
* @param globalErrorReceiver will be use to notify error such as invalid token error. See [GlobalError] * @param globalErrorReceiver will be use to notify error such as invalid token error. See [GlobalError]
* @param canRetry if set to true, the request will be executed again in case of error, after a delay * @param canRetry if set to true, the request will be executed again in case of error, after a delay
* @param maxDelayBeforeRetry the max delay to wait before a retry. Note that in the case of a 429, if the provided delay exceeds this value, the error will * @param maxDelayBeforeRetry the max delay to wait before a retry. Note that in the case of a 429, if the provided delay exceeds this value, the error will

View File

@ -119,9 +119,11 @@ internal class RuntimeJsonAdapterFactory<T>(
companion object { companion object {
/** /**
* @param T the generic type to pass to [RuntimeJsonAdapterFactory]
* @param baseType The base type for which this factory will create adapters. Cannot be Object. * @param baseType The base type for which this factory will create adapters. Cannot be Object.
* @param labelKey The key in the JSON object whose value determines the type to which to map the * @param labelKey The key in the JSON object whose value determines the type to which to map the
* JSON object. * JSON object.
* @param fallbackType alternative Type to try in case of the serialization fails
*/ */
@CheckReturnValue @CheckReturnValue
fun <T> of(baseType: Class<T>, labelKey: String, fallbackType: Class<out T>): RuntimeJsonAdapterFactory<T> { fun <T> of(baseType: Class<T>, labelKey: String, fallbackType: Class<out T>): RuntimeJsonAdapterFactory<T> {

View File

@ -94,6 +94,7 @@ internal object CertUtil {
* Convert the fingerprint to an hexa string. * Convert the fingerprint to an hexa string.
* *
* @param fingerprint the fingerprint * @param fingerprint the fingerprint
* @param sep the separator character, default to space
* @return the hexa string. * @return the hexa string.
*/ */
fun fingerprintToHexString(fingerprint: ByteArray, sep: Char = ' '): String { fun fingerprintToHexString(fingerprint: ByteArray, sep: Char = ' '): String {

View File

@ -24,11 +24,9 @@ import javax.net.ssl.X509TrustManager
/** /**
* Implements a TrustManager that checks Certificates against an explicit list of known * Implements a TrustManager that checks Certificates against an explicit list of known
* fingerprints. * fingerprints.
*/ *
* @property fingerprints Not empty array of SHA256 cert fingerprints
/** * @property defaultTrustManager Optional trust manager to fall back on if cert does not match
* @param fingerprints Not empty array of SHA256 cert fingerprints
* @param defaultTrustManager Optional trust manager to fall back on if cert does not match
* any of the fingerprints. Can be null. * any of the fingerprints. Can be null.
*/ */
internal class PinnedTrustManager(private val fingerprints: List<Fingerprint>, internal class PinnedTrustManager(private val fingerprints: List<Fingerprint>,

View File

@ -28,11 +28,9 @@ import javax.net.ssl.X509ExtendedTrustManager
/** /**
* Implements a TrustManager that checks Certificates against an explicit list of known * Implements a TrustManager that checks Certificates against an explicit list of known
* fingerprints. * fingerprints.
*/ *
* @property fingerprints An array of SHA256 cert fingerprints
/** * @property defaultTrustManager Optional trust manager to fall back on if cert does not match
* @param fingerprints An array of SHA256 cert fingerprints
* @param defaultTrustManager Optional trust manager to fall back on if cert does not match
* any of the fingerprints. Can be null. * any of the fingerprints. Can be null.
*/ */
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)

View File

@ -46,6 +46,7 @@ import org.matrix.android.sdk.internal.session.profile.ProfileModule
import org.matrix.android.sdk.internal.session.pushers.AddPusherWorker import org.matrix.android.sdk.internal.session.pushers.AddPusherWorker
import org.matrix.android.sdk.internal.session.pushers.PushersModule import org.matrix.android.sdk.internal.session.pushers.PushersModule
import org.matrix.android.sdk.internal.session.room.RoomModule import org.matrix.android.sdk.internal.session.room.RoomModule
import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.DeactivateLiveLocationShareWorker
import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker
import org.matrix.android.sdk.internal.session.room.send.RedactEventWorker import org.matrix.android.sdk.internal.session.room.send.RedactEventWorker
import org.matrix.android.sdk.internal.session.room.send.SendEventWorker import org.matrix.android.sdk.internal.session.room.send.SendEventWorker
@ -131,6 +132,8 @@ internal interface SessionComponent {
fun inject(worker: UpdateTrustWorker) fun inject(worker: UpdateTrustWorker)
fun inject(worker: DeactivateLiveLocationShareWorker)
@Component.Factory @Component.Factory
interface Factory { interface Factory {
fun create( fun create(

View File

@ -87,6 +87,8 @@ import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationMan
import org.matrix.android.sdk.internal.session.openid.DefaultOpenIdService import org.matrix.android.sdk.internal.session.openid.DefaultOpenIdService
import org.matrix.android.sdk.internal.session.permalinks.DefaultPermalinkService import org.matrix.android.sdk.internal.session.permalinks.DefaultPermalinkService
import org.matrix.android.sdk.internal.session.room.EventRelationsAggregationProcessor import org.matrix.android.sdk.internal.session.room.EventRelationsAggregationProcessor
import org.matrix.android.sdk.internal.session.room.aggregation.poll.DefaultPollAggregationProcessor
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollAggregationProcessor
import org.matrix.android.sdk.internal.session.room.create.RoomCreateEventProcessor import org.matrix.android.sdk.internal.session.room.create.RoomCreateEventProcessor
import org.matrix.android.sdk.internal.session.room.prune.RedactionEventProcessor import org.matrix.android.sdk.internal.session.room.prune.RedactionEventProcessor
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
@ -385,4 +387,7 @@ internal abstract class SessionModule {
@Binds @Binds
abstract fun bindEventSenderProcessor(processor: EventSenderProcessorCoroutine): EventSenderProcessor abstract fun bindEventSenderProcessor(processor: EventSenderProcessorCoroutine): EventSenderProcessor
@Binds
abstract fun bindPollAggregationProcessor(processor: DefaultPollAggregationProcessor): PollAggregationProcessor
} }

View File

@ -55,6 +55,7 @@ internal interface DirectoryAPI {
/** /**
* Add alias to the room. * Add alias to the room.
* @param roomAlias the room alias. * @param roomAlias the room alias.
* @param body the Json body
*/ */
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}") @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}")
suspend fun addRoomAlias(@Path("roomAlias") roomAlias: String, suspend fun addRoomAlias(@Path("roomAlias") roomAlias: String,

View File

@ -32,6 +32,7 @@ internal interface OpenIdAPI {
* Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-user-userid-openid-request-token * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-user-userid-openid-request-token
* *
* @param userId the user id * @param userId the user id
* @param body an empty json body
*/ */
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/openid/request_token") @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/openid/request_token")
suspend fun openIdToken(@Path("userId") userId: String, suspend fun openIdToken(@Path("userId") userId: String,

View File

@ -35,7 +35,7 @@ internal interface PushRulesApi {
* *
* @param kind the notification kind (sender, room...) * @param kind the notification kind (sender, room...)
* @param ruleId the ruleId * @param ruleId the ruleId
* @param enable the new enable status * @param enabledBody the new enable status
*/ */
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}/enabled") @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}/enabled")
suspend fun updateEnableRuleStatus(@Path("kind") kind: String, suspend fun updateEnableRuleStatus(@Path("kind") kind: String,

View File

@ -16,7 +16,6 @@
package org.matrix.android.sdk.internal.session.room package org.matrix.android.sdk.internal.session.room
import io.realm.Realm import io.realm.Realm
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.crypto.verification.VerificationState import org.matrix.android.sdk.api.session.crypto.verification.VerificationState
import org.matrix.android.sdk.api.session.events.model.AggregatedAnnotation import org.matrix.android.sdk.api.session.events.model.AggregatedAnnotation
@ -28,23 +27,16 @@ import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventCon
import org.matrix.android.sdk.api.session.events.model.getRelationContent import org.matrix.android.sdk.api.session.events.model.getRelationContent
import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.getTimelineEvent
import org.matrix.android.sdk.api.session.room.model.PollSummaryContent
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedContent import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedContent
import org.matrix.android.sdk.api.session.room.model.VoteInfo
import org.matrix.android.sdk.api.session.room.model.VoteSummary
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageEndPollContent
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent
import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent
import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
import org.matrix.android.sdk.internal.SessionManager import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.crypto.verification.toState import org.matrix.android.sdk.internal.crypto.verification.toState
import org.matrix.android.sdk.internal.database.helper.findRootThreadEvent import org.matrix.android.sdk.internal.database.helper.findRootThreadEvent
@ -55,7 +47,6 @@ import org.matrix.android.sdk.internal.database.model.EditionOfEvent
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventInsertType import org.matrix.android.sdk.internal.database.model.EventInsertType
import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntity
import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntity import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntity
import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntityFields import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.ReferencesAggregatedSummaryEntity import org.matrix.android.sdk.internal.database.model.ReferencesAggregatedSummaryEntity
@ -68,6 +59,7 @@ import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.LiveLocationAggregationProcessor import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.LiveLocationAggregationProcessor
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollAggregationProcessor
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber import timber.log.Timber
@ -79,6 +71,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
@SessionId private val sessionId: String, @SessionId private val sessionId: String,
private val sessionManager: SessionManager, private val sessionManager: SessionManager,
private val liveLocationAggregationProcessor: LiveLocationAggregationProcessor, private val liveLocationAggregationProcessor: LiveLocationAggregationProcessor,
private val pollAggregationProcessor: PollAggregationProcessor,
private val clock: Clock, private val clock: Clock,
) : EventInsertLiveProcessor { ) : EventInsertLiveProcessor {
@ -162,9 +155,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
// A replace! // A replace!
handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
} else if (event.getClearType() in EventType.POLL_RESPONSE) { } else if (event.getClearType() in EventType.POLL_RESPONSE) {
event.getClearContent().toModel<MessagePollResponseContent>(catchError = true)?.let { pollResponseContent -> sessionManager.getSessionComponent(sessionId)?.session()?.let { session ->
Timber.v("###RESPONSE in room $roomId for event ${event.eventId}") pollAggregationProcessor.handlePollResponseEvent(session, realm, event)
handleResponse(realm, event, pollResponseContent, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
} }
} }
} }
@ -184,12 +176,16 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
} }
in EventType.POLL_RESPONSE -> { in EventType.POLL_RESPONSE -> {
event.getClearContent().toModel<MessagePollResponseContent>(catchError = true)?.let { event.getClearContent().toModel<MessagePollResponseContent>(catchError = true)?.let {
handleResponse(realm, event, it, roomId, isLocalEcho, event.getRelationContent()?.eventId) sessionManager.getSessionComponent(sessionId)?.session()?.let { session ->
pollAggregationProcessor.handlePollResponseEvent(session, realm, event)
}
} }
} }
in EventType.POLL_END -> { in EventType.POLL_END -> {
event.content.toModel<MessageEndPollContent>(catchError = true)?.let { sessionManager.getSessionComponent(sessionId)?.session()?.let { session ->
handleEndPoll(realm, event, it, roomId, isLocalEcho) getPowerLevelsHelper(event.roomId)?.let {
pollAggregationProcessor.handlePollEndEvent(session, it, realm, event)
}
} }
} }
in EventType.BEACON_LOCATION_DATA -> { in EventType.BEACON_LOCATION_DATA -> {
@ -245,12 +241,16 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
} }
in EventType.POLL_RESPONSE -> { in EventType.POLL_RESPONSE -> {
event.content.toModel<MessagePollResponseContent>(catchError = true)?.let { event.content.toModel<MessagePollResponseContent>(catchError = true)?.let {
handleResponse(realm, event, it, roomId, isLocalEcho) sessionManager.getSessionComponent(sessionId)?.session()?.let { session ->
pollAggregationProcessor.handlePollResponseEvent(session, realm, event)
}
} }
} }
in EventType.POLL_END -> { in EventType.POLL_END -> {
event.content.toModel<MessageEndPollContent>(catchError = true)?.let { sessionManager.getSessionComponent(sessionId)?.session()?.let { session ->
handleEndPoll(realm, event, it, roomId, isLocalEcho) getPowerLevelsHelper(event.roomId)?.let {
pollAggregationProcessor.handlePollEndEvent(session, it, realm, event)
}
} }
} }
in EventType.STATE_ROOM_BEACON_INFO -> { in EventType.STATE_ROOM_BEACON_INFO -> {
@ -318,22 +318,6 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
return return
} }
ContentMapper
.map(eventAnnotationsSummaryEntity.pollResponseSummary?.aggregatedContent)
?.toModel<PollSummaryContent>()
?.let { existingPollSummaryContent ->
eventAnnotationsSummaryEntity.pollResponseSummary?.aggregatedContent = ContentMapper.map(
PollSummaryContent(
myVote = existingPollSummaryContent.myVote,
votes = emptyList(),
votesSummary = emptyMap(),
totalVotes = 0,
winnerVoteCount = 0,
)
.toContent()
)
}
val txId = event.unsignedData?.transactionId val txId = event.unsignedData?.transactionId
// is it a remote echo? // is it a remote echo?
if (!isLocalEcho && existingSummary.editions.any { it.eventId == txId }) { if (!isLocalEcho && existingSummary.editions.any { it.eventId == txId }) {
@ -363,6 +347,10 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
} }
} }
if (event.getClearType() in EventType.POLL_START) {
pollAggregationProcessor.handlePollStartEvent(realm, event)
}
if (!isLocalEcho) { if (!isLocalEcho) {
val replaceEvent = TimelineEventEntity val replaceEvent = TimelineEventEntity
.where(realm, roomId, eventId) .where(realm, roomId, eventId)
@ -376,6 +364,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
* Check if the edition is on the latest thread event, and update it accordingly. * Check if the edition is on the latest thread event, and update it accordingly.
* @param editedEvent The event that will be changed * @param editedEvent The event that will be changed
* @param replaceEvent The new event * @param replaceEvent The new event
* @param editions list of edition of event
*/ */
private fun handleThreadSummaryEdition(editedEvent: EventEntity?, private fun handleThreadSummaryEdition(editedEvent: EventEntity?,
replaceEvent: TimelineEventEntity?, replaceEvent: TimelineEventEntity?,
@ -392,173 +381,10 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
} }
} }
private fun handleResponse(realm: Realm, private fun getPowerLevelsHelper(roomId: String): PowerLevelsHelper? {
event: Event, return stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)
content: MessagePollResponseContent,
roomId: String,
isLocalEcho: Boolean,
relatedEventId: String? = null) {
val eventId = event.eventId ?: return
val senderId = event.senderId ?: return
val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return
val eventTimestamp = event.originServerTs ?: return
val targetPollContent = getPollContent(roomId, targetEventId) ?: return
// ok, this is a poll response
var existing = EventAnnotationsSummaryEntity.where(realm, roomId, targetEventId).findFirst()
if (existing == null) {
Timber.v("## POLL creating new relation summary for $targetEventId")
existing = EventAnnotationsSummaryEntity.create(realm, roomId, targetEventId)
}
// we have it
val existingPollSummary = existing.pollResponseSummary
?: realm.createObject(PollResponseAggregatedSummaryEntity::class.java).also {
existing.pollResponseSummary = it
}
val closedTime = existingPollSummary.closedTime
if (closedTime != null && eventTimestamp > closedTime) {
Timber.v("## POLL is closed ignore event poll:$targetEventId, event :${event.eventId}")
return
}
val currentModel = ContentMapper.map(existingPollSummary.aggregatedContent).toModel<PollSummaryContent>()
if (existingPollSummary.sourceEvents.contains(eventId)) {
// ignore this event, we already know it (??)
Timber.v("## POLL ignoring event for summary, it's known eventId:$eventId")
return
}
val txId = event.unsignedData?.transactionId
// is it a remote echo?
if (!isLocalEcho && existingPollSummary.sourceLocalEchoEvents.contains(txId)) {
// ok it has already been managed
Timber.v("## POLL Receiving remote echo of response eventId:$eventId")
existingPollSummary.sourceLocalEchoEvents.remove(txId)
existingPollSummary.sourceEvents.add(event.eventId)
return
}
val option = content.getBestResponse()?.answers?.first() ?: return Unit.also {
Timber.d("## POLL Ignoring malformed response no option eventId:$eventId content: ${event.content}")
}
// Check if this option is in available options
if (!targetPollContent.getBestPollCreationInfo()?.answers?.map { it.id }?.contains(option).orFalse()) {
Timber.v("## POLL $targetEventId doesn't contain option $option")
return
}
val votes = currentModel?.votes.orEmpty().toMutableList()
var myVote: String? = null
val existingVoteIndex = votes.indexOfFirst { it.userId == senderId }
if (existingVoteIndex != -1) {
// Is the vote newer?
val existingVote = votes[existingVoteIndex]
if (existingVote.voteTimestamp < eventTimestamp) {
// Take the new one
votes[existingVoteIndex] = VoteInfo(senderId, option, eventTimestamp)
if (userId == senderId) {
myVote = option
}
Timber.v("## POLL adding vote $option for user $senderId in poll :$targetEventId ")
} else {
Timber.v("## POLL Ignoring vote (older than known one) eventId:$eventId ")
}
} else {
votes.add(VoteInfo(senderId, option, eventTimestamp))
if (userId == senderId) {
myVote = option
}
Timber.v("## POLL adding vote $option for user $senderId in poll :$targetEventId ")
}
// Precompute the percentage of votes for all options
val totalVotes = votes.size
val newVotesSummary = votes
.groupBy({ it.option }, { it.userId })
.mapValues {
VoteSummary(
total = it.value.size,
percentage = if (totalVotes == 0 && it.value.isEmpty()) 0.0 else it.value.size.toDouble() / totalVotes
)
}
val newWinnerVoteCount = newVotesSummary.maxOf { it.value.total }
if (isLocalEcho) {
existingPollSummary.sourceLocalEchoEvents.add(eventId)
} else {
existingPollSummary.sourceEvents.add(eventId)
}
val newSumModel = PollSummaryContent(
myVote = myVote,
votes = votes,
votesSummary = newVotesSummary,
totalVotes = totalVotes,
winnerVoteCount = newWinnerVoteCount
)
existingPollSummary.aggregatedContent = ContentMapper.map(newSumModel.toContent())
}
private fun handleEndPoll(realm: Realm,
event: Event,
content: MessageEndPollContent,
roomId: String,
isLocalEcho: Boolean) {
val pollEventId = content.relatesTo?.eventId ?: return
val pollOwnerId = getPollEvent(roomId, pollEventId)?.root?.senderId
val isPollOwner = pollOwnerId == event.senderId
val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)
?.content?.toModel<PowerLevelsContent>() ?.content?.toModel<PowerLevelsContent>()
?.let { PowerLevelsHelper(it) } ?.let { PowerLevelsHelper(it) }
if (!isPollOwner && !powerLevelsHelper?.isUserAbleToRedact(event.senderId ?: "").orFalse()) {
Timber.v("## Received poll.end event $pollEventId but user ${event.senderId} doesn't have enough power level in room $roomId")
return
}
var existingPoll = EventAnnotationsSummaryEntity.where(realm, roomId, pollEventId).findFirst()
if (existingPoll == null) {
Timber.v("## POLL creating new relation summary for $pollEventId")
existingPoll = EventAnnotationsSummaryEntity.create(realm, roomId, pollEventId)
}
// we have it
val existingPollSummary = existingPoll.pollResponseSummary
?: realm.createObject(PollResponseAggregatedSummaryEntity::class.java).also {
existingPoll.pollResponseSummary = it
}
val txId = event.unsignedData?.transactionId
existingPollSummary.closedTime = event.originServerTs
// is it a remote echo?
if (!isLocalEcho && existingPollSummary.sourceLocalEchoEvents.contains(txId)) {
// ok it has already been managed
Timber.v("## POLL Receiving remote echo of response eventId:$pollEventId")
existingPollSummary.sourceLocalEchoEvents.remove(txId)
existingPollSummary.sourceEvents.add(event.eventId)
}
}
private fun getPollEvent(roomId: String, eventId: String): TimelineEvent? {
val session = sessionManager.getSessionComponent(sessionId)?.session()
return session?.roomService()?.getRoom(roomId)?.getTimelineEvent(eventId) ?: return null.also {
Timber.v("## POLL target poll event $eventId not found in room $roomId")
}
}
private fun getPollContent(roomId: String, eventId: String): MessagePollContent? {
val pollEvent = getPollEvent(roomId, eventId) ?: return null
return pollEvent.getLastMessageContent() as? MessagePollContent ?: return null.also {
Timber.v("## POLL target poll event $eventId content is malformed")
}
} }
private fun handleInitialAggregatedRelations(realm: Realm, private fun handleInitialAggregatedRelations(realm: Realm,

View File

@ -179,6 +179,7 @@ internal interface RoomAPI {
* Invite a user to a room, using a ThreePid * Invite a user to a room, using a ThreePid
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#id101 * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#id101
* @param roomId Required. The room identifier (not alias) to which to invite the user. * @param roomId Required. The room identifier (not alias) to which to invite the user.
* @param body the Json body
*/ */
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/invite") @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/invite")
suspend fun invite3pid(@Path("roomId") roomId: String, suspend fun invite3pid(@Path("roomId") roomId: String,
@ -221,8 +222,13 @@ internal interface RoomAPI {
/** /**
* Paginate relations for event based in normal topological order. * Paginate relations for event based in normal topological order.
* @param roomId the room Id
* @param eventId the event Id
* @param relationType filter for this relation type * @param relationType filter for this relation type
* @param eventType filter for this event type * @param eventType filter for this event type
* @param from from token
* @param to to token
* @param limit max number of Event to retrieve
*/ */
@GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}/{eventType}") @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}/{eventType}")
suspend fun getRelations(@Path("roomId") roomId: String, suspend fun getRelations(@Path("roomId") roomId: String,
@ -236,7 +242,13 @@ internal interface RoomAPI {
/** /**
* Paginate relations for thread events based in normal topological order. * Paginate relations for thread events based in normal topological order.
*
* @param roomId the room Id
* @param eventId the event Id
* @param relationType filter for this relation type * @param relationType filter for this relation type
* @param from from token
* @param to to token
* @param limit max number of Event to retrieve
*/ */
@GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}") @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}")
suspend fun getThreadsRelations(@Path("roomId") roomId: String, suspend fun getThreadsRelations(@Path("roomId") roomId: String,

View File

@ -35,7 +35,7 @@ internal class RoomAvatarResolver @Inject constructor(@UserId private val userId
/** /**
* Compute the room avatar url. * Compute the room avatar url.
* @param realm: the current instance of realm * @param realm the current instance of realm
* @param roomId the roomId of the room to resolve avatar * @param roomId the roomId of the room to resolve avatar
* @return the room avatar url, can be a fallback to a room member avatar or null * @return the room avatar url, can be a fallback to a room member avatar or null
*/ */

View File

@ -0,0 +1,97 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room.aggregation.livelocation
import android.content.Context
import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass
import io.realm.RealmConfiguration
import org.matrix.android.sdk.api.util.md5
import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.database.awaitTransaction
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
import org.matrix.android.sdk.internal.database.query.get
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import timber.log.Timber
import javax.inject.Inject
/**
* Worker dedicated to update live location summary data so that it is considered as deactivated.
* For the context: it is needed since a live location share should be deactivated after a certain timeout.
*/
internal class DeactivateLiveLocationShareWorker(context: Context, params: WorkerParameters, sessionManager: SessionManager) :
SessionSafeCoroutineWorker<DeactivateLiveLocationShareWorker.Params>(
context,
params,
sessionManager,
Params::class.java
) {
@JsonClass(generateAdapter = true)
internal data class Params(
override val sessionId: String,
override val lastFailureMessage: String? = null,
val eventId: String,
val roomId: String
) : SessionWorkerParams
@SessionDatabase
@Inject lateinit var realmConfiguration: RealmConfiguration
override fun injectWith(injector: SessionComponent) {
injector.inject(this)
}
override suspend fun doSafeWork(params: Params): Result {
return runCatching {
deactivateLiveLocationShare(params)
}.fold(
onSuccess = {
Result.success()
},
onFailure = {
Timber.e("failed to deactivate live, eventId: ${params.eventId}, roomId: ${params.roomId}")
Result.failure()
}
)
}
private suspend fun deactivateLiveLocationShare(params: Params) {
awaitTransaction(realmConfiguration) { realm ->
val aggregatedSummary = LiveLocationShareAggregatedSummaryEntity.get(
realm = realm,
roomId = params.roomId,
eventId = params.eventId
)
aggregatedSummary?.isActive = false
}
}
override fun buildErrorParams(params: Params, message: String): Params {
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
}
companion object {
fun getWorkName(eventId: String, roomId: String): String {
val hash = "$eventId$roomId".md5()
return "DeactivateLiveLocationWork-$hash"
}
}
}

View File

@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.session.room.aggregation.livelocation package org.matrix.android.sdk.internal.session.room.aggregation.livelocation
import androidx.work.ExistingWorkPolicy
import io.realm.Realm import io.realm.Realm
import org.matrix.android.sdk.api.extensions.orTrue import org.matrix.android.sdk.api.extensions.orTrue
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
@ -26,17 +27,27 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocati
import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.mapper.ContentMapper
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.di.WorkManagerProvider
import org.matrix.android.sdk.internal.util.time.Clock
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
internal class LiveLocationAggregationProcessor @Inject constructor() { internal class LiveLocationAggregationProcessor @Inject constructor(
@SessionId private val sessionId: String,
private val workManagerProvider: WorkManagerProvider,
private val clock: Clock,
) {
fun handleBeaconInfo(realm: Realm, event: Event, content: MessageBeaconInfoContent, roomId: String, isLocalEcho: Boolean) { fun handleBeaconInfo(realm: Realm, event: Event, content: MessageBeaconInfoContent, roomId: String, isLocalEcho: Boolean) {
if (event.senderId.isNullOrEmpty() || isLocalEcho) { if (event.senderId.isNullOrEmpty() || isLocalEcho) {
return return
} }
val targetEventId = if (content.isLive.orTrue()) { val isLive = content.isLive.orTrue()
val targetEventId = if (isLive) {
event.eventId event.eventId
} else { } else {
// when live is set to false, we use the id of the event that should have been replaced // when live is set to false, we use the id of the event that should have been replaced
@ -56,8 +67,39 @@ internal class LiveLocationAggregationProcessor @Inject constructor() {
Timber.d("updating summary of id=$targetEventId with isLive=${content.isLive}") Timber.d("updating summary of id=$targetEventId with isLive=${content.isLive}")
aggregatedSummary.endOfLiveTimestampMillis = content.getBestTimestampMillis()?.let { it + (content.timeout ?: 0) } val endOfLiveTimestampMillis = content.getBestTimestampMillis()?.let { it + (content.timeout ?: 0) }
aggregatedSummary.isActive = content.isLive aggregatedSummary.endOfLiveTimestampMillis = endOfLiveTimestampMillis
aggregatedSummary.isActive = isLive
if (isLive) {
scheduleDeactivationAfterTimeout(targetEventId, roomId, endOfLiveTimestampMillis)
} else {
cancelDeactivationAfterTimeout(targetEventId, roomId)
}
}
private fun scheduleDeactivationAfterTimeout(eventId: String, roomId: String, endOfLiveTimestampMillis: Long?) {
endOfLiveTimestampMillis ?: return
val workParams = DeactivateLiveLocationShareWorker.Params(sessionId = sessionId, eventId = eventId, roomId = roomId)
val workData = WorkerParamsFactory.toData(workParams)
val workName = DeactivateLiveLocationShareWorker.getWorkName(eventId = eventId, roomId = roomId)
val workDelayMillis = (endOfLiveTimestampMillis - clock.epochMillis()).coerceAtLeast(0)
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<DeactivateLiveLocationShareWorker>()
.setInitialDelay(workDelayMillis, TimeUnit.MILLISECONDS)
.setInputData(workData)
.build()
workManagerProvider.workManager.enqueueUniqueWork(
workName,
ExistingWorkPolicy.REPLACE,
workRequest
)
}
private fun cancelDeactivationAfterTimeout(eventId: String, roomId: String) {
val workName = DeactivateLiveLocationShareWorker.getWorkName(eventId = eventId, roomId = roomId)
workManagerProvider.workManager.cancelUniqueWork(workName)
} }
fun handleBeaconLocationData( fun handleBeaconLocationData(

View File

@ -0,0 +1,203 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room.aggregation.poll
import io.realm.Realm
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.LocalEcho
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.getRelationContent
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.getTimelineEvent
import org.matrix.android.sdk.api.session.room.model.PollSummaryContent
import org.matrix.android.sdk.api.session.room.model.VoteInfo
import org.matrix.android.sdk.api.session.room.model.VoteSummary
import org.matrix.android.sdk.api.session.room.model.message.MessageEndPollContent
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntity
import org.matrix.android.sdk.internal.database.query.create
import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.where
import javax.inject.Inject
class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationProcessor {
override fun handlePollStartEvent(realm: Realm, event: Event): Boolean {
val content = event.getClearContent()?.toModel<MessagePollContent>()
if (content?.relatesTo?.type != RelationType.REPLACE) {
return false
}
val roomId = event.roomId ?: return false
val targetEventId = content.relatesTo.eventId ?: return false
EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, targetEventId).let { eventAnnotationsSummaryEntity ->
ContentMapper
.map(eventAnnotationsSummaryEntity.pollResponseSummary?.aggregatedContent)
?.toModel<PollSummaryContent>()
?.let { existingPollSummaryContent ->
eventAnnotationsSummaryEntity.pollResponseSummary?.aggregatedContent = ContentMapper.map(
PollSummaryContent(
myVote = existingPollSummaryContent.myVote,
votes = emptyList(),
votesSummary = emptyMap(),
totalVotes = 0,
winnerVoteCount = 0,
)
.toContent()
)
}
}
return true
}
override fun handlePollResponseEvent(session: Session, realm: Realm, event: Event): Boolean {
val content = event.getClearContent()?.toModel<MessagePollResponseContent>() ?: return false
val roomId = event.roomId ?: return false
val senderId = event.senderId ?: return false
val targetEventId = (event.getRelationContent() ?: content.relatesTo)?.eventId ?: return false
val targetPollContent = getPollContent(session, roomId, targetEventId) ?: return false
val annotationsSummaryEntity = getAnnotationsSummaryEntity(realm, roomId, targetEventId)
val aggregatedPollSummaryEntity = getAggregatedPollSummaryEntity(realm, annotationsSummaryEntity)
val closedTime = aggregatedPollSummaryEntity.closedTime
val responseTime = event.originServerTs ?: return false
if (closedTime != null && responseTime > closedTime) {
return false
}
if (aggregatedPollSummaryEntity.sourceEvents.contains(event.eventId)) {
return false
}
val txId = event.unsignedData?.transactionId
val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "")
if (!isLocalEcho && aggregatedPollSummaryEntity.sourceLocalEchoEvents.contains(txId)) {
aggregatedPollSummaryEntity.sourceLocalEchoEvents.remove(txId)
aggregatedPollSummaryEntity.sourceEvents.add(event.eventId)
return false
}
val vote = content.getBestResponse()?.answers?.first() ?: return false
if (!targetPollContent.getBestPollCreationInfo()?.answers?.map { it.id }?.contains(vote).orFalse()) {
return false
}
val pollSummaryModel = ContentMapper.map(aggregatedPollSummaryEntity.aggregatedContent).toModel<PollSummaryContent>()
val existingVotes = pollSummaryModel?.votes.orEmpty().toMutableList()
val existingVoteIndex = existingVotes.indexOfFirst { it.userId == senderId }
if (existingVoteIndex != -1) {
val existingVote = existingVotes[existingVoteIndex]
if (existingVote.voteTimestamp > responseTime) {
return false
}
existingVotes[existingVoteIndex] = VoteInfo(senderId, vote, responseTime)
} else {
existingVotes.add(VoteInfo(senderId, vote, responseTime))
}
// Precompute the percentage of votes for all options
val totalVotes = existingVotes.size
val newVotesSummary = existingVotes
.groupBy({ it.option }, { it.userId })
.mapValues {
VoteSummary(
total = it.value.size,
percentage = if (totalVotes == 0 && it.value.isEmpty()) 0.0 else it.value.size.toDouble() / totalVotes
)
}
val newWinnerVoteCount = newVotesSummary.maxOf { it.value.total }
if (isLocalEcho) {
aggregatedPollSummaryEntity.sourceLocalEchoEvents.add(event.eventId)
} else {
aggregatedPollSummaryEntity.sourceEvents.add(event.eventId)
}
val myVote = existingVotes.find { it.userId == session.myUserId }?.option
val newSumModel = PollSummaryContent(
myVote = myVote,
votes = existingVotes,
votesSummary = newVotesSummary,
totalVotes = totalVotes,
winnerVoteCount = newWinnerVoteCount
)
aggregatedPollSummaryEntity.aggregatedContent = ContentMapper.map(newSumModel.toContent())
return true
}
override fun handlePollEndEvent(session: Session, powerLevelsHelper: PowerLevelsHelper, realm: Realm, event: Event): Boolean {
val content = event.getClearContent()?.toModel<MessageEndPollContent>() ?: return false
val roomId = event.roomId ?: return false
val pollEventId = content.relatesTo?.eventId ?: return false
val pollOwnerId = getPollEvent(session, roomId, pollEventId)?.root?.senderId
val isPollOwner = pollOwnerId == event.senderId
if (!isPollOwner && !powerLevelsHelper.isUserAbleToRedact(event.senderId ?: "")) {
return false
}
val annotationsSummaryEntity = getAnnotationsSummaryEntity(realm, roomId, pollEventId)
val aggregatedPollSummaryEntity = getAggregatedPollSummaryEntity(realm, annotationsSummaryEntity)
val txId = event.unsignedData?.transactionId
aggregatedPollSummaryEntity.closedTime = event.originServerTs
val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "")
if (!isLocalEcho && aggregatedPollSummaryEntity.sourceLocalEchoEvents.contains(txId)) {
aggregatedPollSummaryEntity.sourceLocalEchoEvents.remove(txId)
aggregatedPollSummaryEntity.sourceEvents.add(event.eventId)
}
return true
}
private fun getPollEvent(session: Session, roomId: String, eventId: String): TimelineEvent? {
return session.roomService().getRoom(roomId)?.getTimelineEvent(eventId)
}
private fun getPollContent(session: Session, roomId: String, eventId: String): MessagePollContent? {
val pollEvent = getPollEvent(session, roomId, eventId)
return pollEvent?.getLastMessageContent() as? MessagePollContent
}
private fun getAnnotationsSummaryEntity(realm: Realm, roomId: String, eventId: String): EventAnnotationsSummaryEntity {
return EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst()
?: EventAnnotationsSummaryEntity.create(realm, roomId, eventId)
}
private fun getAggregatedPollSummaryEntity(realm: Realm,
eventAnnotationsSummaryEntity: EventAnnotationsSummaryEntity): PollResponseAggregatedSummaryEntity {
return eventAnnotationsSummaryEntity.pollResponseSummary
?: realm.createObject(PollResponseAggregatedSummaryEntity::class.java).also {
eventAnnotationsSummaryEntity.pollResponseSummary = it
}
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room.aggregation.poll
import io.realm.Realm
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
interface PollAggregationProcessor {
/**
* Poll start events don't need to be processed by the aggregator.
* This function will only handle if the poll is edited and will update the poll summary entity.
* Returns true if the event is aggregated.
*/
fun handlePollStartEvent(
realm: Realm,
event: Event
): Boolean
/**
* Aggregates poll response event after many conditional checks like if the poll is ended, if the user is changing his/her vote etc.
* Returns true if the event is aggregated.
*/
fun handlePollResponseEvent(
session: Session,
realm: Realm,
event: Event
): Boolean
/**
* Updates poll summary entity and mark it is ended after many conditional checks like if the poll is already ended etc.
* Returns true if the event is aggregated.
*/
fun handlePollEndEvent(
session: Session,
powerLevelsHelper: PowerLevelsHelper,
realm: Realm,
event: Event
): Boolean
}

View File

@ -52,8 +52,8 @@ internal class RoomDisplayNameResolver @Inject constructor(
/** /**
* Compute the room display name. * Compute the room display name.
* *
* @param realm: the current instance of realm * @param realm the current instance of realm
* @param roomId: the roomId to resolve the name of. * @param roomId the roomId to resolve the name of.
* @return the room display name * @return the room display name
*/ */
fun resolve(realm: Realm, roomId: String): RoomName { fun resolve(realm: Realm, roomId: String): RoomName {

View File

@ -21,8 +21,8 @@ import timber.log.Timber
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
/** /**
* @param queueIdentifier String value to identify a unique Queue * @property queueIdentifier String value to identify a unique Queue
* @param taskIdentifier String value to identify a unique Task. Should be different from queueIdentifier * @property taskIdentifier String value to identify a unique Task. Should be different from queueIdentifier
*/ */
internal abstract class QueuedTask( internal abstract class QueuedTask(
val queueIdentifier: String, val queueIdentifier: String,

View File

@ -24,11 +24,12 @@ import retrofit2.http.Query
internal interface SpaceApi { internal interface SpaceApi {
/** /**
* @param spaceId the space Id
* @param suggestedOnly Optional. If true, return only child events and rooms where the m.space.child event has suggested: true. * @param suggestedOnly Optional. If true, return only child events and rooms where the m.space.child event has suggested: true.
* @param limit: Optional: a client-defined limit to the maximum number of rooms to return per page. Must be a non-negative integer. * @param limit Optional: a client-defined limit to the maximum number of rooms to return per page. Must be a non-negative integer.
* @param maxDepth: Optional: The maximum depth in the tree (from the root room) to return. * @param maxDepth Optional: The maximum depth in the tree (from the root room) to return.
* The deepest depth returned will not include children events. Defaults to no-limit. Must be a non-negative integer. * The deepest depth returned will not include children events. Defaults to no-limit. Must be a non-negative integer.
* @param from: Optional. Pagination token given to retrieve the next set of rooms. * @param from Optional. Pagination token given to retrieve the next set of rooms.
* Note that if a pagination token is provided, then the parameters given for suggested_only and max_depth must be the same. * Note that if a pagination token is provided, then the parameters given for suggested_only and max_depth must be the same.
*/ */
@GET(NetworkConstants.URI_API_PREFIX_PATH_V1 + "rooms/{roomId}/hierarchy") @GET(NetworkConstants.URI_API_PREFIX_PATH_V1 + "rooms/{roomId}/hierarchy")

View File

@ -520,9 +520,10 @@ internal class RoomSyncHandler @Inject constructor(
private fun decryptIfNeeded(event: Event, roomId: String) { private fun decryptIfNeeded(event: Event, roomId: String) {
try { try {
val timelineId = generateTimelineId(roomId)
// Event from sync does not have roomId, so add it to the event first // Event from sync does not have roomId, so add it to the event first
// note: runBlocking should be used here while we are in realm single thread executor, to avoid thread switching // note: runBlocking should be used here while we are in realm single thread executor, to avoid thread switching
val result = runBlocking { cryptoService.decryptEvent(event.copy(roomId = roomId), "") } val result = runBlocking { cryptoService.decryptEvent(event.copy(roomId = roomId), timelineId) }
event.mxDecryptionResult = OlmDecryptionResult( event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent, payload = result.clearEvent,
senderKey = result.senderCurve25519Key, senderKey = result.senderCurve25519Key,
@ -537,6 +538,10 @@ internal class RoomSyncHandler @Inject constructor(
} }
} }
private fun generateTimelineId(roomId: String): String {
return "RoomSyncHandler$roomId"
}
data class EphemeralResult( data class EphemeralResult(
val typingUserIds: List<String> = emptyList() val typingUserIds: List<String> = emptyList()
) )

View File

@ -206,6 +206,8 @@ internal class ThreadsAwarenessHandler @Inject constructor(
/** /**
* Handle for not thread events that we have marked them as root. * Handle for not thread events that we have marked them as root.
* Find relations and inject them accordingly * Find relations and inject them accordingly
* @param realm the realm instance
* @param roomId the current room Id
* @param eventEntity the current eventEntity received * @param eventEntity the current eventEntity received
* @param event the current event received * @param event the current event received
* @return The content to inject in the roomSyncHandler live events * @return The content to inject in the roomSyncHandler live events
@ -229,9 +231,12 @@ internal class ThreadsAwarenessHandler @Inject constructor(
* This function is responsible to check if there is any event that relates to our current event. * This function is responsible to check if there is any event that relates to our current event.
* This is useful when we receive an event that relates to a missing parent, so when later we receive the parent * This is useful when we receive an event that relates to a missing parent, so when later we receive the parent
* we can update the child as well. * we can update the child as well.
* @param realm the realm instance
* @param roomId the current room Id
* @param event the current event that we examine * @param event the current event that we examine
* @param eventBody the current body of the event * @param eventBody the current body of the event
* @param isFromCache determines whether or not we already know this is root thread event * @param isFromCache determines whether or not we already know this is root thread event
* @param threadRelation the information about thread
* @return The content to inject in the roomSyncHandler live events * @return The content to inject in the roomSyncHandler live events
*/ */
private fun handleEventsThatRelatesTo( private fun handleEventsThatRelatesTo(
@ -291,9 +296,12 @@ internal class ThreadsAwarenessHandler @Inject constructor(
} }
/** /**
* Injecting $eventToInject decrypted content as a reply to $event. * Injecting [eventToInject] decrypted content as a reply to event.
* @param eventToInject the event that will inject * @param roomId the room id
* @param eventBody the actual event body * @param eventBody the actual event body
* @param eventToInject the event that will inject
* @param eventToInjectBody the event body to inject
* @param threadRelation the information about thread
* @return The final content with the injected event * @return The final content with the injected event
*/ */
private fun injectEvent(roomId: String, private fun injectEvent(roomId: String,

View File

@ -116,6 +116,8 @@ internal class DefaultWidgetPostAPIMediator @Inject constructor(private val mosh
/** /**
* Send an object response. * Send an object response.
* *
* @param T the Json type
* @param type the type
* @param response the response * @param response the response
* @param eventData the modular data * @param eventData the modular data
*/ */

View File

@ -27,6 +27,7 @@ internal interface WidgetsAPI {
* Register to the server. * Register to the server.
* *
* @param body the body content (Ref: https://github.com/matrix-org/matrix-doc/pull/1961) * @param body the body content (Ref: https://github.com/matrix-org/matrix-doc/pull/1961)
* @param version the widget API version
*/ */
@POST("register") @POST("register")
suspend fun register(@Body body: OpenIdToken, suspend fun register(@Body body: OpenIdToken,

View File

@ -24,6 +24,7 @@ import kotlinx.coroutines.sync.withPermit
*/ */
internal interface CoroutineSequencer { internal interface CoroutineSequencer {
/** /**
* @param T generic type
* @param block the suspendable block to execute * @param block the suspendable block to execute
* @return the result of the block * @return the result of the block
*/ */

View File

@ -53,7 +53,7 @@ internal object JsonCanonicalizer {
/** /**
* Canonicalize a JSON element. * Canonicalize a JSON element.
* *
* @param src the src * @param any the src
* @return the canonicalize element * @return the canonicalize element
*/ */
private fun canonicalizeRecursive(any: Any): String { private fun canonicalizeRecursive(any: Any): String {

View File

@ -75,7 +75,8 @@ internal class DefaultGetWellknownTask @Inject constructor(
* - validate homeserver url and identity server url if provide in .well-known result * - validate homeserver url and identity server url if provide in .well-known result
* - return action and .well-known data * - return action and .well-known data
* *
* @param domain: homeserver domain, deduced from mx userId (ex: "matrix.org" from userId "@user:matrix.org") * @param domain homeserver domain, deduced from mx userId (ex: "matrix.org" from userId "@user:matrix.org")
* @param client Http client to perform the request
*/ */
private suspend fun findClientConfig(domain: String, client: OkHttpClient): WellknownResult { private suspend fun findClientConfig(domain: String, client: OkHttpClient): WellknownResult {
val wellKnownAPI = retrofitFactory.create(client, "https://dummy.org") val wellKnownAPI = retrofitFactory.create(client, "https://dummy.org")

View File

@ -27,6 +27,7 @@ import org.matrix.android.sdk.internal.di.MatrixScope
import org.matrix.android.sdk.internal.session.content.UploadContentWorker import org.matrix.android.sdk.internal.session.content.UploadContentWorker
import org.matrix.android.sdk.internal.session.group.GetGroupDataWorker import org.matrix.android.sdk.internal.session.group.GetGroupDataWorker
import org.matrix.android.sdk.internal.session.pushers.AddPusherWorker import org.matrix.android.sdk.internal.session.pushers.AddPusherWorker
import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.DeactivateLiveLocationShareWorker
import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker
import org.matrix.android.sdk.internal.session.room.send.RedactEventWorker import org.matrix.android.sdk.internal.session.room.send.RedactEventWorker
import org.matrix.android.sdk.internal.session.room.send.SendEventWorker import org.matrix.android.sdk.internal.session.room.send.SendEventWorker
@ -66,6 +67,8 @@ internal class MatrixWorkerFactory @Inject constructor(private val sessionManage
UpdateTrustWorker(appContext, workerParameters, sessionManager) UpdateTrustWorker(appContext, workerParameters, sessionManager)
UploadContentWorker::class.java.name -> UploadContentWorker::class.java.name ->
UploadContentWorker(appContext, workerParameters, sessionManager) UploadContentWorker(appContext, workerParameters, sessionManager)
DeactivateLiveLocationShareWorker::class.java.name ->
DeactivateLiveLocationShareWorker(appContext, workerParameters, sessionManager)
else -> { else -> {
Timber.w("No worker defined on MatrixWorkerFactory for $workerClassName will delegate to default.") Timber.w("No worker defined on MatrixWorkerFactory for $workerClassName will delegate to default.")
// Return null to delegate to the default WorkerFactory. // Return null to delegate to the default WorkerFactory.

Some files were not shown because too many files have changed in this diff Show More