diff --git a/changelog.d/5783.wip b/changelog.d/5783.wip
new file mode 100644
index 0000000000..6f44813d81
--- /dev/null
+++ b/changelog.d/5783.wip
@@ -0,0 +1 @@
+FTUE - Overrides sign up flow ordering for matrix.org only
diff --git a/changelog.d/5856.bugfix b/changelog.d/5856.bugfix
new file mode 100644
index 0000000000..87f10ac9b2
--- /dev/null
+++ b/changelog.d/5856.bugfix
@@ -0,0 +1 @@
+Use fixed text size in read receipt counter
diff --git a/changelog.d/6012.wip b/changelog.d/6012.wip
new file mode 100644
index 0000000000..9c67d562fe
--- /dev/null
+++ b/changelog.d/6012.wip
@@ -0,0 +1 @@
+Live location sharing: navigation from timeline to map screen
diff --git a/changelog.d/6077.sdk b/changelog.d/6077.sdk
new file mode 100644
index 0000000000..80310a28f5
--- /dev/null
+++ b/changelog.d/6077.sdk
@@ -0,0 +1 @@
+Improve replay attacks and reduce duplicate message index errors
diff --git a/changelog.d/6100.misc b/changelog.d/6100.misc
new file mode 100644
index 0000000000..2fb5ecf34d
--- /dev/null
+++ b/changelog.d/6100.misc
@@ -0,0 +1 @@
+Excludes transitive optional non FOSS google location dependency from fdroid builds
diff --git a/changelog.d/6123.wip b/changelog.d/6123.wip
new file mode 100644
index 0000000000..680498280f
--- /dev/null
+++ b/changelog.d/6123.wip
@@ -0,0 +1 @@
+[Live location sharing] Update entity in DB when a live is timed out
diff --git a/changelog.d/6140.bugfix b/changelog.d/6140.bugfix
new file mode 100644
index 0000000000..247e69f837
--- /dev/null
+++ b/changelog.d/6140.bugfix
@@ -0,0 +1 @@
+Prevent widget web view from reloading on screen / orientation change
diff --git a/changelog.d/6141.misc b/changelog.d/6141.misc
new file mode 100644
index 0000000000..2f0a91b451
--- /dev/null
+++ b/changelog.d/6141.misc
@@ -0,0 +1 @@
+Downgrade gradle from 7.2.0 to 7.1.3
diff --git a/changelog.d/6148.bugfix b/changelog.d/6148.bugfix
new file mode 100644
index 0000000000..3aa623315a
--- /dev/null
+++ b/changelog.d/6148.bugfix
@@ -0,0 +1 @@
+Fix decrypting redacted event from sending errors
diff --git a/dependencies.gradle b/dependencies.gradle
index 10f9539e5a..49abec137c 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -7,7 +7,10 @@ ext.versions = [
'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
def kotlin = "1.6.21"
def kotlinCoroutines = "1.6.1"
@@ -23,7 +26,7 @@ def mavericks = "2.6.1"
def glide = "4.13.2"
def bigImageViewer = "1.8.1"
def jjwt = "0.11.5"
-def vanniktechEmoji = "0.9.0"
+def vanniktechEmoji = "0.13.0"
// Testing
def mockk = "1.12.4"
@@ -50,7 +53,7 @@ ext.libs = [
'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1",
'exifinterface' : "androidx.exifinterface:exifinterface:1.3.3",
'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",
'autoFill' : "androidx.autofill:autofill:1.1.0",
'preferenceKtx' : "androidx.preference:preference-ktx:1.2.0",
@@ -107,6 +110,10 @@ ext.libs = [
'mavericks' : "com.airbnb.android:mavericks:$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' : "io.mockk:mockk:$mockk",
'mockkAndroid' : "io.mockk:mockk-android:$mockk"
diff --git a/library/ui-styles/src/main/res/values/styles_timeline.xml b/library/ui-styles/src/main/res/values/styles_timeline.xml
index c86eeb8efb..20c375c2d6 100644
--- a/library/ui-styles/src/main/res/values/styles_timeline.xml
+++ b/library/ui-styles/src/main/res/values/styles_timeline.xml
@@ -1,5 +1,5 @@
-
+
+
-
\ No newline at end of file
+
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/RetryTestRule.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/RetryTestRule.kt
index b16ab98e6c..39f49a9ccc 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/RetryTestRule.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/RetryTestRule.kt
@@ -40,6 +40,9 @@ class RetryTestRule(val retryCount: Int = 3) : TestRule {
for (i in 0 until retryCount) {
try {
base.evaluate()
+ if (i > 0) {
+ println("Retried test $i times")
+ }
return
} catch (t: Throwable) {
caughtThrowable = t
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/DecryptRedactedEventTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/DecryptRedactedEventTest.kt
new file mode 100644
index 0000000000..de5fa41581
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/DecryptRedactedEventTest.kt
@@ -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()?.unsignedData?.redactedEvent?.content?.get("reason")
+ )
+ Assert.assertEquals(
+ "Unexpected Redacted event id",
+ timelineEvent.eventId,
+ result.clearEvent.toModel()?.unsignedData?.redactedEvent?.redacts
+ )
+ } catch (failure: Throwable) {
+ Assert.fail("Should not throw when decrypting a redacted event")
+ }
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt
index 552936971f..74c218af58 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt
@@ -23,6 +23,7 @@ import org.amshove.kluent.fail
import org.amshove.kluent.internal.assertEquals
import org.junit.Assert
import org.junit.FixMethodOrder
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -68,6 +69,7 @@ import java.util.concurrent.CountDownLatch
@RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@LargeTest
+@Ignore("This test fails with an unhandled exception thrown from a coroutine which terminates the entire test run.")
class E2eeSanityTests : InstrumentedTest {
@get:Rule val rule = RetryTestRule(3)
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt
index 9136272b1e..d5fd299f1f 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt
@@ -24,6 +24,7 @@ import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.FixMethodOrder
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
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.common.CommonTestHelper
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.TestMatrixCallback
import java.util.Collections
@@ -55,6 +57,8 @@ import java.util.concurrent.CountDownLatch
@LargeTest
class KeysBackupTest : InstrumentedTest {
+ @get:Rule val rule = RetryTestRule(3)
+
/**
* - From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys
* - Check backup keys after having marked one as backed up
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/replayattack/ReplayAttackTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/replayattack/ReplayAttackTest.kt
new file mode 100644
index 0000000000..09c340a14f
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/replayattack/ReplayAttackTest.kt
@@ -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 {
+ // 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)
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationShareAggregatedSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationShareAggregatedSummary.kt
index 0b28d62f56..059fe21471 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationShareAggregatedSummary.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationShareAggregatedSummary.kt
@@ -22,6 +22,9 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocati
* Aggregation info concerning a live location share.
*/
data class LiveLocationShareAggregatedSummary(
+ /**
+ * Indicate whether the live is currently running.
+ */
val isActive: Boolean?,
val endOfLiveTimestampMillis: Long?,
val lastLocationDataContent: MessageBeaconLocationDataContent?,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
index cb61bbe1de..c1d04eb22b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
@@ -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.EventType
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.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
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 val loggerTag = LoggerTag("CryptoSyncHandler", LoggerTag.CRYPTO)
+private val loggerTag = LoggerTag("EventDecryptor", LoggerTag.CRYPTO)
@SessionScope
internal class EventDecryptor @Inject constructor(
@@ -110,6 +111,16 @@ internal class EventDecryptor @Inject constructor(
if (eventContent == null) {
Timber.tag(loggerTag.value).e("decryptEvent : empty event content")
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(),
+ "unsigned" to event.unsignedData.toContent()
+ )
+ )
} else {
val algorithm = eventContent["algorithm"]?.toString()
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt
index 8fe200a349..48c588fd29 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt
@@ -96,8 +96,9 @@ internal class MXOlmDevice @Inject constructor(
// So, store these message indexes per timeline id.
//
// The first level keys are timeline ids.
- // The second level keys are strings of form "||"
- private val inboundGroupSessionMessageIndexes: MutableMap> = HashMap()
+ // The second level values is a Map that represents:
+ // "|||" --> eventId
+ private val inboundGroupSessionMessageIndexes: MutableMap> = HashMap()
init {
// Retrieve the account from the store
@@ -757,69 +758,72 @@ internal class MXOlmDevice @Inject constructor(
* @param body the base64-encoded body of the encrypted message.
* @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 eventId the eventId of the message that will be decrypted
* @param sessionId the session identifier.
* @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)
- suspend fun decryptGroupMessage(
- body: String,
- roomId: String,
- timeline: String?,
- sessionId: String,
- senderKey: String
- ): OlmDecryptionResult {
+ suspend fun decryptGroupMessage(body: String,
+ roomId: String,
+ timeline: String?,
+ eventId: String,
+ sessionId: String,
+ senderKey: String): OlmDecryptionResult {
val sessionHolder = getInboundGroupSession(sessionId, senderKey, roomId)
val wrapper = sessionHolder.wrapper
val inboundGroupSession = wrapper.olmInboundGroupSession
?: throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, "Session is null")
- // Check that the room id matches the original one for the session. This stops
- // the HS pretending a message was targeting a different room.
- if (roomId == wrapper.roomId) {
- val decryptResult = try {
- sessionHolder.mutex.withLock {
- inboundGroupSession.decryptMessage(body)
- }
- } catch (e: OlmException) {
- Timber.tag(loggerTag.value).e(e, "## decryptGroupMessage () : decryptMessage failed")
- throw MXCryptoError.OlmError(e)
- }
-
- if (timeline?.isNotBlank() == true) {
- val timelineSet = inboundGroupSessionMessageIndexes.getOrPut(timeline) { mutableSetOf() }
-
- val messageIndexKey = senderKey + "|" + sessionId + "|" + decryptResult.mIndex
-
- if (timelineSet.contains(messageIndexKey)) {
- val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex)
- Timber.tag(loggerTag.value).e("## decryptGroupMessage() timelineId=$timeline: $reason")
- throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason)
- }
-
- timelineSet.add(messageIndexKey)
- }
-
- inboundGroupSessionStore.storeInBoundGroupSession(sessionHolder, sessionId, senderKey)
- val payload = try {
- val adapter = MoshiProvider.providesMoshi().adapter(JSON_DICT_PARAMETERIZED_TYPE)
- val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage)
- adapter.fromJson(payloadString)
- } catch (e: Exception) {
- Timber.tag(loggerTag.value).e("## decryptGroupMessage() : fails to parse the payload")
- throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)
- }
-
- return OlmDecryptionResult(
- payload,
- wrapper.keysClaimed,
- senderKey,
- wrapper.forwardingCurve25519KeyChain
- )
- } else {
+ if (roomId != wrapper.roomId) {
+ // Check that the room id matches the original one for the session. This stops
+ // the HS pretending a message was targeting a different room.
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 {
+ sessionHolder.mutex.withLock {
+ inboundGroupSession.decryptMessage(body)
+ }
+ } catch (e: OlmException) {
+ Timber.tag(loggerTag.value).e(e, "## decryptGroupMessage () : decryptMessage failed")
+ 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) {
+ val replayAttackMap = inboundGroupSessionMessageIndexes.getOrPut(timeline) { mutableMapOf() }
+ if (replayAttackMap.contains(messageIndexKey) && replayAttackMap[messageIndexKey] != eventId) {
+ val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex)
+ Timber.tag(loggerTag.value).e("## decryptGroupMessage() timelineId=$timeline: $reason")
+ throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason)
+ }
+ replayAttackMap[messageIndexKey] = eventId
+ }
+ inboundGroupSessionStore.storeInBoundGroupSession(sessionHolder, sessionId, senderKey)
+ val payload = try {
+ val adapter = MoshiProvider.providesMoshi().adapter(JSON_DICT_PARAMETERIZED_TYPE)
+ val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage)
+ adapter.fromJson(payloadString)
+ } catch (e: Exception) {
+ Timber.tag(loggerTag.value).e("## decryptGroupMessage() : fails to parse the payload")
+ throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)
+ }
+
+ return OlmDecryptionResult(
+ payload,
+ wrapper.keysClaimed,
+ senderKey,
+ wrapper.forwardingCurve25519KeyChain
+ )
}
/**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt
index 92cff5d9fa..141d6f74cd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt
@@ -78,6 +78,7 @@ internal class MXMegolmDecryption(
encryptedEventContent.ciphertext,
event.roomId,
timeline,
+ eventId = event.eventId.orEmpty(),
encryptedEventContent.sessionId,
encryptedEventContent.senderKey
)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt
index 4d0d2c5c64..e84337693f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt
@@ -31,6 +31,9 @@ internal open class LiveLocationShareAggregatedSummaryEntity(
var roomId: String = "",
+ /**
+ * Indicate whether the live is currently running.
+ */
var isActive: Boolean? = null,
var endOfLiveTimestampMillis: Long? = null,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt
index 2e2e939fa2..816b5f4392 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt
@@ -55,3 +55,11 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.getOrCreate(
return LiveLocationShareAggregatedSummaryEntity.where(realm, roomId, eventId).findFirst()
?: LiveLocationShareAggregatedSummaryEntity.create(realm, roomId, eventId)
}
+
+internal fun LiveLocationShareAggregatedSummaryEntity.Companion.get(
+ realm: Realm,
+ roomId: String,
+ eventId: String,
+): LiveLocationShareAggregatedSummaryEntity? {
+ return LiveLocationShareAggregatedSummaryEntity.where(realm, roomId, eventId).findFirst()
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt
index f53a89130a..f01451b688 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt
@@ -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.PushersModule
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.RedactEventWorker
import org.matrix.android.sdk.internal.session.room.send.SendEventWorker
@@ -131,6 +132,8 @@ internal interface SessionComponent {
fun inject(worker: UpdateTrustWorker)
+ fun inject(worker: DeactivateLiveLocationShareWorker)
+
@Component.Factory
interface Factory {
fun create(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/DeactivateLiveLocationShareWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/DeactivateLiveLocationShareWorker.kt
new file mode 100644
index 0000000000..2b83c8028b
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/DeactivateLiveLocationShareWorker.kt
@@ -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(
+ 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"
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt
index 76b7a4ec8e..42dfc7ba9f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt
@@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.session.room.aggregation.livelocation
+import androidx.work.ExistingWorkPolicy
import io.realm.Realm
import org.matrix.android.sdk.api.extensions.orTrue
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.model.livelocation.LiveLocationShareAggregatedSummaryEntity
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 java.util.concurrent.TimeUnit
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) {
if (event.senderId.isNullOrEmpty() || isLocalEcho) {
return
}
- val targetEventId = if (content.isLive.orTrue()) {
+ val isLive = content.isLive.orTrue()
+ val targetEventId = if (isLive) {
event.eventId
} else {
// 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}")
- aggregatedSummary.endOfLiveTimestampMillis = content.getBestTimestampMillis()?.let { it + (content.timeout ?: 0) }
- aggregatedSummary.isActive = content.isLive
+ val endOfLiveTimestampMillis = content.getBestTimestampMillis()?.let { it + (content.timeout ?: 0) }
+ 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()
+ .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(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt
index cb4e1db46e..3629bcc678 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt
@@ -536,9 +536,10 @@ internal class RoomSyncHandler @Inject constructor(
private fun decryptIfNeeded(event: Event, roomId: String) {
try {
+ val timelineId = generateTimelineId(roomId)
// 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
- val result = runBlocking { cryptoService.decryptEvent(event.copy(roomId = roomId), "") }
+ val result = runBlocking { cryptoService.decryptEvent(event.copy(roomId = roomId), timelineId) }
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
@@ -553,6 +554,10 @@ internal class RoomSyncHandler @Inject constructor(
}
}
+ private fun generateTimelineId(roomId: String): String {
+ return "RoomSyncHandler$roomId"
+ }
+
data class EphemeralResult(
val typingUserIds: List = emptyList()
)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/MatrixWorkerFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/MatrixWorkerFactory.kt
index 497825b450..9e464a80dc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/MatrixWorkerFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/MatrixWorkerFactory.kt
@@ -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.group.GetGroupDataWorker
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.RedactEventWorker
import org.matrix.android.sdk.internal.session.room.send.SendEventWorker
@@ -64,9 +65,11 @@ internal class MatrixWorkerFactory @Inject constructor(private val sessionManage
SyncWorker(appContext, workerParameters, sessionManager)
UpdateTrustWorker::class.java.name ->
UpdateTrustWorker(appContext, workerParameters, sessionManager)
- UploadContentWorker::class.java.name ->
+ UploadContentWorker::class.java.name ->
UploadContentWorker(appContext, workerParameters, sessionManager)
- else -> {
+ DeactivateLiveLocationShareWorker::class.java.name ->
+ DeactivateLiveLocationShareWorker(appContext, workerParameters, sessionManager)
+ else -> {
Timber.w("No worker defined on MatrixWorkerFactory for $workerClassName will delegate to default.")
// Return null to delegate to the default WorkerFactory.
null
diff --git a/vector/build.gradle b/vector/build.gradle
index 4ae6527e0b..51dfc2f962 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -507,9 +507,14 @@ dependencies {
implementation 'commons-codec:commons-codec:1.15'
// MapTiler
- implementation 'org.maplibre.gl:android-sdk:9.5.2'
- implementation 'org.maplibre.gl:android-plugin-annotation-v9:1.0.0'
-
+ fdroidImplementation(libs.maplibre.androidSdk) {
+ exclude group: 'com.google.android.gms', module: 'play-services-location'
+ }
+ fdroidImplementation(libs.maplibre.pluginAnnotation) {
+ exclude group: 'com.google.android.gms', module: 'play-services-location'
+ }
+ gplayImplementation libs.maplibre.androidSdk
+ gplayImplementation libs.maplibre.pluginAnnotation
// TESTS
testImplementation libs.tests.junit
diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml
index 20b7c4908a..8c2e25bc7e 100644
--- a/vector/src/main/AndroidManifest.xml
+++ b/vector/src/main/AndroidManifest.xml
@@ -306,7 +306,9 @@
android:supportsPictureInPicture="true" />
-
+
+
@@ -343,6 +345,7 @@
+
diff --git a/vector/src/main/java/im/vector/app/core/extensions/Fragment.kt b/vector/src/main/java/im/vector/app/core/extensions/Fragment.kt
index dfbd2eba97..61c4fe2174 100644
--- a/vector/src/main/java/im/vector/app/core/extensions/Fragment.kt
+++ b/vector/src/main/java/im/vector/app/core/extensions/Fragment.kt
@@ -36,9 +36,10 @@ fun Fragment.registerStartForActivityResult(onResult: (ActivityResult) -> Unit):
fun Fragment.addFragment(
frameId: Int,
fragment: Fragment,
+ tag: String? = null,
allowStateLoss: Boolean = false
) {
- parentFragmentManager.commitTransaction(allowStateLoss) { add(frameId, fragment) }
+ parentFragmentManager.commitTransaction(allowStateLoss) { add(frameId, fragment, tag) }
}
fun Fragment.addFragment(
diff --git a/vector/src/main/java/im/vector/app/core/utils/Emoji.kt b/vector/src/main/java/im/vector/app/core/utils/Emoji.kt
index d6a63dca10..3e82ecd5f2 100644
--- a/vector/src/main/java/im/vector/app/core/utils/Emoji.kt
+++ b/vector/src/main/java/im/vector/app/core/utils/Emoji.kt
@@ -16,7 +16,7 @@
package im.vector.app.core.utils
-import com.vanniktech.emoji.EmojiUtils
+import com.vanniktech.emoji.isOnlyEmojis
/**
* Test if a string contains emojis.
@@ -28,7 +28,7 @@ import com.vanniktech.emoji.EmojiUtils
*/
fun containsOnlyEmojis(str: String?): Boolean {
// Now rely on vanniktech library
- return EmojiUtils.isOnlyEmojis(str)
+ return str.isOnlyEmojis()
}
/**
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
index b48a06d927..2434867fe8 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
@@ -221,6 +221,7 @@ import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
+import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent
@@ -647,6 +648,13 @@ class TimelineFragment @Inject constructor(
)
}
+ private fun navigateToLocationLiveMap() {
+ navigator.openLocationLiveMap(
+ context = requireContext(),
+ roomId = timelineArgs.roomId
+ )
+ }
+
private fun handleChangeLocationIndicator(event: RoomDetailViewEvents.ChangeLocationIndicator) {
views.locationLiveStatusIndicator.isVisible = event.isVisible
}
@@ -706,31 +714,31 @@ class TimelineFragment @Inject constructor(
}
private fun createEmojiPopup(): EmojiPopup {
- return EmojiPopup
- .Builder
- .fromRootView(views.rootConstraintLayout)
- .setKeyboardAnimationStyle(R.style.emoji_fade_animation_style)
- .setOnEmojiPopupShownListener {
+ return EmojiPopup(
+ rootView = views.rootConstraintLayout,
+ keyboardAnimationStyle = R.style.emoji_fade_animation_style,
+ onEmojiPopupShownListener = {
views.composerLayout.views.composerEmojiButton.apply {
contentDescription = getString(R.string.a11y_close_emoji_picker)
setImageResource(R.drawable.ic_keyboard)
}
- }
- .setOnEmojiPopupDismissListenerLifecycleAware {
+ },
+ onEmojiPopupDismissListener = lifecycleAwareDismissAction {
views.composerLayout.views.composerEmojiButton.apply {
contentDescription = getString(R.string.a11y_open_emoji_picker)
setImageResource(R.drawable.ic_insert_emoji)
}
- }
- .build(views.composerLayout.views.composerEditText)
+ },
+ editText = views.composerLayout.views.composerEditText
+ )
}
/**
* Ensure dismiss actions only trigger when the fragment is in the started state.
* EmojiPopup by default dismisses onViewDetachedFromWindow, this can cause race conditions with onDestroyView.
*/
- private fun EmojiPopup.Builder.setOnEmojiPopupDismissListenerLifecycleAware(action: () -> Unit): EmojiPopup.Builder {
- return setOnEmojiPopupDismissListener {
+ private fun lifecycleAwareDismissAction(action: () -> Unit): () -> Unit {
+ return {
if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
action()
}
@@ -2019,6 +2027,9 @@ class TimelineFragment @Inject constructor(
is MessageLocationContent -> {
handleShowLocationPreview(messageContent, informationData.senderId)
}
+ is MessageBeaconInfoContent -> {
+ navigateToLocationLiveMap()
+ }
else -> {
val handled = onThreadSummaryClicked(informationData.eventId, isRootThreadEvent)
if (!handled) {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationShareMessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationShareMessageItemFactory.kt
index 479a742369..3c7b6c32e1 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationShareMessageItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationShareMessageItemFactory.kt
@@ -33,7 +33,6 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageLiveLocation
import im.vector.app.features.location.INITIAL_MAP_ZOOM_IN_TIMELINE
import im.vector.app.features.location.UrlMapProvider
import im.vector.app.features.location.toLocationData
-import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.threeten.bp.LocalDateTime
@@ -75,7 +74,8 @@ class LiveLocationShareMessageItemFactory @Inject constructor(
val height = dimensionConverter.dpToPx(MessageItemFactory.MESSAGE_LOCATION_ITEM_HEIGHT_IN_DP)
return MessageLiveLocationInactiveItem_()
- .attributes(attributes)
+ // disable the click on this state item
+ .attributes(attributes.copy(itemClickListener = null))
.mapWidth(width)
.mapHeight(height)
.highlighted(highlight)
@@ -90,7 +90,8 @@ class LiveLocationShareMessageItemFactory @Inject constructor(
val height = dimensionConverter.dpToPx(MessageItemFactory.MESSAGE_LOCATION_ITEM_HEIGHT_IN_DP)
return MessageLiveLocationStartItem_()
- .attributes(attributes)
+ // disable the click on this state item
+ .attributes(attributes.copy(itemClickListener = null))
.mapWidth(width)
.mapHeight(height)
.highlighted(highlight)
@@ -127,7 +128,7 @@ class LiveLocationShareMessageItemFactory @Inject constructor(
private fun getViewState(liveLocationShareSummaryData: LiveLocationShareSummaryData?): LiveLocationShareViewState {
return when {
liveLocationShareSummaryData?.isActive == null -> LiveLocationShareViewState.Unkwown
- liveLocationShareSummaryData.isActive.not() || isLiveTimedOut(liveLocationShareSummaryData) -> LiveLocationShareViewState.Inactive
+ liveLocationShareSummaryData.isActive.not() -> LiveLocationShareViewState.Inactive
liveLocationShareSummaryData.isActive && liveLocationShareSummaryData.lastGeoUri.isNullOrEmpty() -> LiveLocationShareViewState.Loading
else ->
LiveLocationShareViewState.Running(
@@ -137,16 +138,6 @@ class LiveLocationShareMessageItemFactory @Inject constructor(
}.also { viewState -> Timber.d("computed viewState: $viewState") }
}
- private fun isLiveTimedOut(liveLocationShareSummaryData: LiveLocationShareSummaryData): Boolean {
- return getEndOfLiveDateTime(liveLocationShareSummaryData)
- ?.let { endOfLive ->
- // this will only cover users with different timezones but not users with manually time set
- val now = LocalDateTime.now()
- now.isAfter(endOfLive)
- }
- .orFalse()
- }
-
private fun getEndOfLiveDateTime(liveLocationShareSummaryData: LiveLocationShareSummaryData): LocalDateTime? {
return liveLocationShareSummaryData.endOfLiveTimestampMillis?.let { DateProvider.toLocalDateTime(timestamp = it) }
}
diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewActivity.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewActivity.kt
new file mode 100644
index 0000000000..c0f07dba57
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewActivity.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.location.live.map
+
+import android.content.Context
+import android.content.Intent
+import android.os.Parcelable
+import dagger.hilt.android.AndroidEntryPoint
+import im.vector.app.R
+import im.vector.app.core.extensions.addFragment
+import im.vector.app.core.platform.VectorBaseActivity
+import im.vector.app.databinding.ActivityLocationSharingBinding
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class LocationLiveMapViewArgs(
+ val roomId: String
+) : Parcelable
+
+@AndroidEntryPoint
+class LocationLiveMapViewActivity : VectorBaseActivity() {
+
+ override fun getBinding() = ActivityLocationSharingBinding.inflate(layoutInflater)
+
+ override fun initUiAndData() {
+ val mapViewArgs: LocationLiveMapViewArgs? = intent?.extras?.getParcelable(EXTRA_LOCATION_LIVE_MAP_VIEW_ARGS)
+ if (mapViewArgs == null) {
+ finish()
+ return
+ }
+ setupToolbar(views.toolbar)
+ .setTitle(getString(R.string.location_activity_title_preview))
+ .allowBack()
+
+ if (isFirstCreation()) {
+ addFragment(
+ views.fragmentContainer,
+ LocationLiveMapViewFragment::class.java,
+ mapViewArgs
+ )
+ }
+ }
+
+ companion object {
+
+ private const val EXTRA_LOCATION_LIVE_MAP_VIEW_ARGS = "EXTRA_LOCATION_LIVE_MAP_VIEW_ARGS"
+
+ fun getIntent(context: Context, locationLiveMapViewArgs: LocationLiveMapViewArgs): Intent {
+ return Intent(context, LocationLiveMapViewActivity::class.java).apply {
+ putExtra(EXTRA_LOCATION_LIVE_MAP_VIEW_ARGS, locationLiveMapViewArgs)
+ }
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt
new file mode 100644
index 0000000000..32b87727d8
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.location.live.map
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.lifecycle.lifecycleScope
+import com.airbnb.mvrx.args
+import com.mapbox.mapboxsdk.maps.MapboxMapOptions
+import com.mapbox.mapboxsdk.maps.SupportMapFragment
+import dagger.hilt.android.AndroidEntryPoint
+import im.vector.app.R
+import im.vector.app.core.extensions.addChildFragment
+import im.vector.app.core.platform.VectorBaseFragment
+import im.vector.app.databinding.FragmentSimpleContainerBinding
+import im.vector.app.features.location.UrlMapProvider
+import javax.inject.Inject
+
+/**
+ * Screen showing a map with all the current users sharing their live location in room.
+ */
+@AndroidEntryPoint
+class LocationLiveMapViewFragment : VectorBaseFragment() {
+
+ @Inject
+ lateinit var urlMapProvider: UrlMapProvider
+
+ private val args: LocationLiveMapViewArgs by args()
+
+ override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSimpleContainerBinding {
+ return FragmentSimpleContainerBinding.inflate(layoutInflater, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ setupMap()
+ }
+
+ private fun setupMap() {
+ val mapFragment = getOrCreateSupportMapFragment()
+
+ mapFragment.getMapAsync { mapBoxMap ->
+ lifecycleScope.launchWhenCreated {
+ mapBoxMap.setStyle(urlMapProvider.getMapUrl())
+ }
+ }
+ }
+
+ private fun getOrCreateSupportMapFragment() =
+ childFragmentManager.findFragmentByTag(MAP_FRAGMENT_TAG) as? SupportMapFragment
+ ?: run {
+ val options = MapboxMapOptions.createFromAttributes(requireContext(), null)
+ SupportMapFragment.newInstance(options)
+ .also { addChildFragment(R.id.fragmentContainer, it, tag = MAP_FRAGMENT_TAG) }
+ }
+
+ companion object {
+ private const val MAP_FRAGMENT_TAG = "im.vector.app.features.location.live.map"
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt
index 1491b46fe9..47f76bfc3c 100644
--- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt
+++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt
@@ -68,6 +68,8 @@ import im.vector.app.features.location.LocationData
import im.vector.app.features.location.LocationSharingActivity
import im.vector.app.features.location.LocationSharingArgs
import im.vector.app.features.location.LocationSharingMode
+import im.vector.app.features.location.live.map.LocationLiveMapViewActivity
+import im.vector.app.features.location.live.map.LocationLiveMapViewArgs
import im.vector.app.features.login.LoginActivity
import im.vector.app.features.login.LoginConfig
import im.vector.app.features.matrixto.MatrixToBottomSheet
@@ -606,6 +608,14 @@ class DefaultNavigator @Inject constructor(
context.startActivity(intent)
}
+ override fun openLocationLiveMap(context: Context, roomId: String) {
+ val intent = LocationLiveMapViewActivity.getIntent(
+ context = context,
+ locationLiveMapViewArgs = LocationLiveMapViewArgs(roomId = roomId)
+ )
+ context.startActivity(intent)
+ }
+
private fun startActivity(context: Context, intent: Intent, buildTask: Boolean) {
if (buildTask) {
val stackBuilder = TaskStackBuilder.create(context)
diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt
index f2728dce2b..7b6a81db52 100644
--- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt
+++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt
@@ -186,6 +186,8 @@ interface Navigator {
locationOwnerId: String?
)
+ fun openLocationLiveMap(context: Context, roomId: String)
+
fun openThread(context: Context, threadTimelineArgs: ThreadTimelineArgs, eventIdToNavigate: String? = null)
fun openThreadList(context: Context, threadTimelineArgs: ThreadTimelineArgs)
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
index a7caa9d0c2..cf730a0266 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
@@ -43,6 +43,7 @@ import im.vector.app.features.login.ReAuthHelper
import im.vector.app.features.login.ServerType
import im.vector.app.features.login.SignMode
import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase.StartAuthenticationResult
+import im.vector.app.features.onboarding.ftueauth.MatrixOrgRegistrationStagesComparator
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
@@ -293,9 +294,19 @@ class OnboardingViewModel @AssistedInject constructor(
}
private fun emitFlowResultViewEvent(flowResult: FlowResult) {
- _viewEvents.post(OnboardingViewEvents.RegistrationFlowResult(flowResult, isRegistrationStarted))
+ withState { state ->
+ val orderedResult = when {
+ state.hasSelectedMatrixOrg() && vectorFeatures.isOnboardingCombinedRegisterEnabled() -> flowResult.copy(
+ missingStages = flowResult.missingStages.sortedWith(MatrixOrgRegistrationStagesComparator())
+ )
+ else -> flowResult
+ }
+ _viewEvents.post(OnboardingViewEvents.RegistrationFlowResult(orderedResult, isRegistrationStarted))
+ }
}
+ private fun OnboardingViewState.hasSelectedMatrixOrg() = selectedHomeserver.userFacingUrl == matrixOrgUrl
+
private fun handleRegisterWith(action: OnboardingAction.Register) {
reAuthHelper.data = action.password
handleRegisterAction(
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt
index 036f8eb9a2..8430b483d2 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt
@@ -54,7 +54,6 @@ import im.vector.app.features.onboarding.OnboardingViewState
import im.vector.app.features.onboarding.ftueauth.terms.FtueAuthLegacyStyleTermsFragment
import im.vector.app.features.onboarding.ftueauth.terms.FtueAuthTermsFragment
import im.vector.app.features.onboarding.ftueauth.terms.FtueAuthTermsLegacyStyleFragmentArgument
-import org.matrix.android.sdk.api.auth.registration.FlowResult
import org.matrix.android.sdk.api.auth.registration.Stage
import org.matrix.android.sdk.api.auth.toLocalizedLoginTerms
import org.matrix.android.sdk.api.extensions.tryOrNull
@@ -235,17 +234,12 @@ class FtueAuthVariant(
private fun onRegistrationFlow(viewEvents: OnboardingViewEvents.RegistrationFlowResult) {
when {
registrationShouldFallback(viewEvents) -> displayFallbackWebDialog()
- viewEvents.isRegistrationStarted -> handleRegistrationNavigation(viewEvents.flowResult.orderedStages())
+ viewEvents.isRegistrationStarted -> handleRegistrationNavigation(viewEvents.flowResult.missingStages)
vectorFeatures.isOnboardingCombinedRegisterEnabled() -> openStartCombinedRegister()
else -> openAuthLoginFragmentWithTag(FRAGMENT_REGISTRATION_STAGE_TAG)
}
}
- private fun FlowResult.orderedStages() = when {
- vectorFeatures.isOnboardingCombinedRegisterEnabled() -> missingStages.sortedWith(FtueMissingRegistrationStagesComparator())
- else -> missingStages
- }
-
private fun openStartCombinedRegister() {
addRegistrationStageFragmentToBackstack(FtueAuthCombinedRegisterFragment::class.java)
}
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueMissingRegistrationStagesComparator.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/MatrixOrgRegistrationStagesComparator.kt
similarity index 84%
rename from vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueMissingRegistrationStagesComparator.kt
rename to vector/src/main/java/im/vector/app/features/onboarding/ftueauth/MatrixOrgRegistrationStagesComparator.kt
index 6a6326625e..527c20987a 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueMissingRegistrationStagesComparator.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/MatrixOrgRegistrationStagesComparator.kt
@@ -18,10 +18,10 @@ package im.vector.app.features.onboarding.ftueauth
import org.matrix.android.sdk.api.auth.registration.Stage
-class FtueMissingRegistrationStagesComparator : Comparator {
+class MatrixOrgRegistrationStagesComparator : Comparator {
- override fun compare(a: Stage?, b: Stage?): Int {
- return (a?.toPriority() ?: 0) - (b?.toPriority() ?: 0)
+ override fun compare(a: Stage, b: Stage): Int {
+ return a.toPriority().compareTo(b.toPriority())
}
private fun Stage.toPriority() = when (this) {
diff --git a/vector/src/main/res/layout/fragment_simple_container.xml b/vector/src/main/res/layout/fragment_simple_container.xml
new file mode 100644
index 0000000000..fb9e6d38c5
--- /dev/null
+++ b/vector/src/main/res/layout/fragment_simple_container.xml
@@ -0,0 +1,5 @@
+
+
diff --git a/vector/src/main/res/layout/view_read_receipts.xml b/vector/src/main/res/layout/view_read_receipts.xml
index 907f1ec0e3..11fdeb74d5 100644
--- a/vector/src/main/res/layout/view_read_receipts.xml
+++ b/vector/src/main/res/layout/view_read_receipts.xml
@@ -7,7 +7,7 @@