Merge remote-tracking branch 'origin/develop' into task/eric/code-style-parenthesis

# Conflicts:
#	matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt
This commit is contained in:
ericdecanini 2022-05-25 17:35:31 +02:00
commit 8647400dda
43 changed files with 691 additions and 112 deletions

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/6077.sdk Normal file
View file

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

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/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"
@ -50,7 +53,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",
@ -107,6 +110,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

@ -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

@ -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
@ -68,6 +69,7 @@ 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)

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

@ -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

@ -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

@ -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
@ -757,25 +758,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( suspend fun decryptGroupMessage(body: String,
body: String,
roomId: String, roomId: String,
timeline: String?, timeline: String?,
eventId: String,
sessionId: String, sessionId: String,
senderKey: String senderKey: String): OlmDecryptionResult {
): 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)
@ -785,20 +790,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)
@ -815,11 +824,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)
}
} }
/** /**

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
) )

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

@ -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

@ -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

@ -536,9 +536,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,
@ -553,6 +554,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

@ -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.

View file

@ -507,9 +507,14 @@ dependencies {
implementation 'commons-codec:commons-codec:1.15' implementation 'commons-codec:commons-codec:1.15'
// MapTiler // MapTiler
implementation 'org.maplibre.gl:android-sdk:9.5.2' fdroidImplementation(libs.maplibre.androidSdk) {
implementation 'org.maplibre.gl:android-plugin-annotation-v9:1.0.0' 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 // TESTS
testImplementation libs.tests.junit testImplementation libs.tests.junit

View file

@ -306,7 +306,9 @@
android:supportsPictureInPicture="true" /> android:supportsPictureInPicture="true" />
<activity android:name=".features.terms.ReviewTermsActivity" /> <activity android:name=".features.terms.ReviewTermsActivity" />
<activity android:name=".features.widgets.WidgetActivity" /> <activity android:name=".features.widgets.WidgetActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" />
<activity android:name=".features.pin.PinActivity" /> <activity android:name=".features.pin.PinActivity" />
<activity android:name=".features.analytics.ui.consent.AnalyticsOptInActivity" /> <activity android:name=".features.analytics.ui.consent.AnalyticsOptInActivity" />
<activity android:name=".features.home.room.detail.search.SearchActivity" /> <activity android:name=".features.home.room.detail.search.SearchActivity" />
@ -343,6 +345,7 @@
<activity android:name=".features.spaces.leave.SpaceLeaveAdvancedActivity" /> <activity android:name=".features.spaces.leave.SpaceLeaveAdvancedActivity" />
<activity android:name=".features.poll.create.CreatePollActivity" /> <activity android:name=".features.poll.create.CreatePollActivity" />
<activity android:name=".features.location.LocationSharingActivity" /> <activity android:name=".features.location.LocationSharingActivity" />
<activity android:name=".features.location.live.map.LocationLiveMapViewActivity" />
<!-- Services --> <!-- Services -->

View file

@ -36,9 +36,10 @@ fun Fragment.registerStartForActivityResult(onResult: (ActivityResult) -> Unit):
fun Fragment.addFragment( fun Fragment.addFragment(
frameId: Int, frameId: Int,
fragment: Fragment, fragment: Fragment,
tag: String? = null,
allowStateLoss: Boolean = false allowStateLoss: Boolean = false
) { ) {
parentFragmentManager.commitTransaction(allowStateLoss) { add(frameId, fragment) } parentFragmentManager.commitTransaction(allowStateLoss) { add(frameId, fragment, tag) }
} }
fun <T : Fragment> Fragment.addFragment( fun <T : Fragment> Fragment.addFragment(

View file

@ -16,7 +16,7 @@
package im.vector.app.core.utils package im.vector.app.core.utils
import com.vanniktech.emoji.EmojiUtils import com.vanniktech.emoji.isOnlyEmojis
/** /**
* Test if a string contains emojis. * Test if a string contains emojis.
@ -28,7 +28,7 @@ import com.vanniktech.emoji.EmojiUtils
*/ */
fun containsOnlyEmojis(str: String?): Boolean { fun containsOnlyEmojis(str: String?): Boolean {
// Now rely on vanniktech library // Now rely on vanniktech library
return EmojiUtils.isOnlyEmojis(str) return str.isOnlyEmojis()
} }
/** /**

View file

@ -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.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary 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.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.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageFormat import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent 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) { private fun handleChangeLocationIndicator(event: RoomDetailViewEvents.ChangeLocationIndicator) {
views.locationLiveStatusIndicator.isVisible = event.isVisible views.locationLiveStatusIndicator.isVisible = event.isVisible
} }
@ -706,31 +714,31 @@ class TimelineFragment @Inject constructor(
} }
private fun createEmojiPopup(): EmojiPopup { private fun createEmojiPopup(): EmojiPopup {
return EmojiPopup return EmojiPopup(
.Builder rootView = views.rootConstraintLayout,
.fromRootView(views.rootConstraintLayout) keyboardAnimationStyle = R.style.emoji_fade_animation_style,
.setKeyboardAnimationStyle(R.style.emoji_fade_animation_style) onEmojiPopupShownListener = {
.setOnEmojiPopupShownListener {
views.composerLayout.views.composerEmojiButton.apply { views.composerLayout.views.composerEmojiButton.apply {
contentDescription = getString(R.string.a11y_close_emoji_picker) contentDescription = getString(R.string.a11y_close_emoji_picker)
setImageResource(R.drawable.ic_keyboard) setImageResource(R.drawable.ic_keyboard)
} }
} },
.setOnEmojiPopupDismissListenerLifecycleAware { onEmojiPopupDismissListener = lifecycleAwareDismissAction {
views.composerLayout.views.composerEmojiButton.apply { views.composerLayout.views.composerEmojiButton.apply {
contentDescription = getString(R.string.a11y_open_emoji_picker) contentDescription = getString(R.string.a11y_open_emoji_picker)
setImageResource(R.drawable.ic_insert_emoji) 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. * 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. * EmojiPopup by default dismisses onViewDetachedFromWindow, this can cause race conditions with onDestroyView.
*/ */
private fun EmojiPopup.Builder.setOnEmojiPopupDismissListenerLifecycleAware(action: () -> Unit): EmojiPopup.Builder { private fun lifecycleAwareDismissAction(action: () -> Unit): () -> Unit {
return setOnEmojiPopupDismissListener { return {
if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
action() action()
} }
@ -2019,6 +2027,9 @@ class TimelineFragment @Inject constructor(
is MessageLocationContent -> { is MessageLocationContent -> {
handleShowLocationPreview(messageContent, informationData.senderId) handleShowLocationPreview(messageContent, informationData.senderId)
} }
is MessageBeaconInfoContent -> {
navigateToLocationLiveMap()
}
else -> { else -> {
val handled = onThreadSummaryClicked(informationData.eventId, isRootThreadEvent) val handled = onThreadSummaryClicked(informationData.eventId, isRootThreadEvent)
if (!handled) { if (!handled) {

View file

@ -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.INITIAL_MAP_ZOOM_IN_TIMELINE
import im.vector.app.features.location.UrlMapProvider import im.vector.app.features.location.UrlMapProvider
import im.vector.app.features.location.toLocationData 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.Session
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.threeten.bp.LocalDateTime import org.threeten.bp.LocalDateTime
@ -75,7 +74,8 @@ class LiveLocationShareMessageItemFactory @Inject constructor(
val height = dimensionConverter.dpToPx(MessageItemFactory.MESSAGE_LOCATION_ITEM_HEIGHT_IN_DP) val height = dimensionConverter.dpToPx(MessageItemFactory.MESSAGE_LOCATION_ITEM_HEIGHT_IN_DP)
return MessageLiveLocationInactiveItem_() return MessageLiveLocationInactiveItem_()
.attributes(attributes) // disable the click on this state item
.attributes(attributes.copy(itemClickListener = null))
.mapWidth(width) .mapWidth(width)
.mapHeight(height) .mapHeight(height)
.highlighted(highlight) .highlighted(highlight)
@ -90,7 +90,8 @@ class LiveLocationShareMessageItemFactory @Inject constructor(
val height = dimensionConverter.dpToPx(MessageItemFactory.MESSAGE_LOCATION_ITEM_HEIGHT_IN_DP) val height = dimensionConverter.dpToPx(MessageItemFactory.MESSAGE_LOCATION_ITEM_HEIGHT_IN_DP)
return MessageLiveLocationStartItem_() return MessageLiveLocationStartItem_()
.attributes(attributes) // disable the click on this state item
.attributes(attributes.copy(itemClickListener = null))
.mapWidth(width) .mapWidth(width)
.mapHeight(height) .mapHeight(height)
.highlighted(highlight) .highlighted(highlight)
@ -127,7 +128,7 @@ class LiveLocationShareMessageItemFactory @Inject constructor(
private fun getViewState(liveLocationShareSummaryData: LiveLocationShareSummaryData?): LiveLocationShareViewState { private fun getViewState(liveLocationShareSummaryData: LiveLocationShareSummaryData?): LiveLocationShareViewState {
return when { return when {
liveLocationShareSummaryData?.isActive == null -> LiveLocationShareViewState.Unkwown liveLocationShareSummaryData?.isActive == null -> LiveLocationShareViewState.Unkwown
liveLocationShareSummaryData.isActive.not() || isLiveTimedOut(liveLocationShareSummaryData) -> LiveLocationShareViewState.Inactive liveLocationShareSummaryData.isActive.not() -> LiveLocationShareViewState.Inactive
liveLocationShareSummaryData.isActive && liveLocationShareSummaryData.lastGeoUri.isNullOrEmpty() -> LiveLocationShareViewState.Loading liveLocationShareSummaryData.isActive && liveLocationShareSummaryData.lastGeoUri.isNullOrEmpty() -> LiveLocationShareViewState.Loading
else -> else ->
LiveLocationShareViewState.Running( LiveLocationShareViewState.Running(
@ -137,16 +138,6 @@ class LiveLocationShareMessageItemFactory @Inject constructor(
}.also { viewState -> Timber.d("computed viewState: $viewState") } }.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? { private fun getEndOfLiveDateTime(liveLocationShareSummaryData: LiveLocationShareSummaryData): LocalDateTime? {
return liveLocationShareSummaryData.endOfLiveTimestampMillis?.let { DateProvider.toLocalDateTime(timestamp = it) } return liveLocationShareSummaryData.endOfLiveTimestampMillis?.let { DateProvider.toLocalDateTime(timestamp = it) }
} }

View file

@ -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<ActivityLocationSharingBinding>() {
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)
}
}
}
}

View file

@ -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<FragmentSimpleContainerBinding>() {
@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"
}
}

View file

@ -68,6 +68,8 @@ import im.vector.app.features.location.LocationData
import im.vector.app.features.location.LocationSharingActivity import im.vector.app.features.location.LocationSharingActivity
import im.vector.app.features.location.LocationSharingArgs import im.vector.app.features.location.LocationSharingArgs
import im.vector.app.features.location.LocationSharingMode 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.LoginActivity
import im.vector.app.features.login.LoginConfig import im.vector.app.features.login.LoginConfig
import im.vector.app.features.matrixto.MatrixToBottomSheet import im.vector.app.features.matrixto.MatrixToBottomSheet
@ -606,6 +608,14 @@ class DefaultNavigator @Inject constructor(
context.startActivity(intent) 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) { private fun startActivity(context: Context, intent: Intent, buildTask: Boolean) {
if (buildTask) { if (buildTask) {
val stackBuilder = TaskStackBuilder.create(context) val stackBuilder = TaskStackBuilder.create(context)

View file

@ -186,6 +186,8 @@ interface Navigator {
locationOwnerId: String? locationOwnerId: String?
) )
fun openLocationLiveMap(context: Context, roomId: String)
fun openThread(context: Context, threadTimelineArgs: ThreadTimelineArgs, eventIdToNavigate: String? = null) fun openThread(context: Context, threadTimelineArgs: ThreadTimelineArgs, eventIdToNavigate: String? = null)
fun openThreadList(context: Context, threadTimelineArgs: ThreadTimelineArgs) fun openThreadList(context: Context, threadTimelineArgs: ThreadTimelineArgs)

View file

@ -43,6 +43,7 @@ import im.vector.app.features.login.ReAuthHelper
import im.vector.app.features.login.ServerType import im.vector.app.features.login.ServerType
import im.vector.app.features.login.SignMode import im.vector.app.features.login.SignMode
import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase.StartAuthenticationResult import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase.StartAuthenticationResult
import im.vector.app.features.onboarding.ftueauth.MatrixOrgRegistrationStagesComparator
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -293,8 +294,18 @@ class OnboardingViewModel @AssistedInject constructor(
} }
private fun emitFlowResultViewEvent(flowResult: FlowResult) { 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) { private fun handleRegisterWith(action: OnboardingAction.Register) {
reAuthHelper.data = action.password reAuthHelper.data = action.password

View file

@ -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.FtueAuthLegacyStyleTermsFragment
import im.vector.app.features.onboarding.ftueauth.terms.FtueAuthTermsFragment import im.vector.app.features.onboarding.ftueauth.terms.FtueAuthTermsFragment
import im.vector.app.features.onboarding.ftueauth.terms.FtueAuthTermsLegacyStyleFragmentArgument 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.registration.Stage
import org.matrix.android.sdk.api.auth.toLocalizedLoginTerms import org.matrix.android.sdk.api.auth.toLocalizedLoginTerms
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
@ -235,17 +234,12 @@ class FtueAuthVariant(
private fun onRegistrationFlow(viewEvents: OnboardingViewEvents.RegistrationFlowResult) { private fun onRegistrationFlow(viewEvents: OnboardingViewEvents.RegistrationFlowResult) {
when { when {
registrationShouldFallback(viewEvents) -> displayFallbackWebDialog() registrationShouldFallback(viewEvents) -> displayFallbackWebDialog()
viewEvents.isRegistrationStarted -> handleRegistrationNavigation(viewEvents.flowResult.orderedStages()) viewEvents.isRegistrationStarted -> handleRegistrationNavigation(viewEvents.flowResult.missingStages)
vectorFeatures.isOnboardingCombinedRegisterEnabled() -> openStartCombinedRegister() vectorFeatures.isOnboardingCombinedRegisterEnabled() -> openStartCombinedRegister()
else -> openAuthLoginFragmentWithTag(FRAGMENT_REGISTRATION_STAGE_TAG) else -> openAuthLoginFragmentWithTag(FRAGMENT_REGISTRATION_STAGE_TAG)
} }
} }
private fun FlowResult.orderedStages() = when {
vectorFeatures.isOnboardingCombinedRegisterEnabled() -> missingStages.sortedWith(FtueMissingRegistrationStagesComparator())
else -> missingStages
}
private fun openStartCombinedRegister() { private fun openStartCombinedRegister() {
addRegistrationStageFragmentToBackstack(FtueAuthCombinedRegisterFragment::class.java) addRegistrationStageFragmentToBackstack(FtueAuthCombinedRegisterFragment::class.java)
} }

View file

@ -18,10 +18,10 @@ package im.vector.app.features.onboarding.ftueauth
import org.matrix.android.sdk.api.auth.registration.Stage import org.matrix.android.sdk.api.auth.registration.Stage
class FtueMissingRegistrationStagesComparator : Comparator<Stage> { class MatrixOrgRegistrationStagesComparator : Comparator<Stage> {
override fun compare(a: Stage?, b: Stage?): Int { override fun compare(a: Stage, b: Stage): Int {
return (a?.toPriority() ?: 0) - (b?.toPriority() ?: 0) return a.toPriority().compareTo(b.toPriority())
} }
private fun Stage.toPriority() = when (this) { private fun Stage.toPriority() = when (this) {

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragmentContainer"
android:layout_width="match_parent"
android:layout_height="match_parent" />

View file

@ -7,7 +7,7 @@
<TextView <TextView
android:id="@+id/receiptMore" android:id="@+id/receiptMore"
style="@style/Widget.Vector.TextView.Caption" style="@style/TimelineFixedSizeCaptionStyle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="@dimen/item_event_message_state_size" android:layout_height="@dimen/item_event_message_state_size"
android:background="@drawable/pill_receipt" android:background="@drawable/pill_receipt"

View file

@ -25,7 +25,7 @@ import im.vector.app.test.fixtures.anOtherStage
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test import org.junit.Test
class FtueMissingRegistrationStagesComparatorTest { class MatrixOrgRegistrationStagesComparatorTest {
@Test @Test
fun `when ordering stages, then prioritizes email`() { fun `when ordering stages, then prioritizes email`() {
@ -38,7 +38,7 @@ class FtueMissingRegistrationStagesComparatorTest {
aTermsStage() aTermsStage()
) )
val result = input.sortedWith(FtueMissingRegistrationStagesComparator()) val result = input.sortedWith(MatrixOrgRegistrationStagesComparator())
result shouldBeEqualTo listOf( result shouldBeEqualTo listOf(
anEmailStage(), anEmailStage(),