mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 02:15:35 +03:00
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:
commit
8647400dda
43 changed files with 691 additions and 112 deletions
1
changelog.d/5783.wip
Normal file
1
changelog.d/5783.wip
Normal file
|
@ -0,0 +1 @@
|
||||||
|
FTUE - Overrides sign up flow ordering for matrix.org only
|
1
changelog.d/5856.bugfix
Normal file
1
changelog.d/5856.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Use fixed text size in read receipt counter
|
1
changelog.d/6012.wip
Normal file
1
changelog.d/6012.wip
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Live location sharing: navigation from timeline to map screen
|
1
changelog.d/6077.sdk
Normal file
1
changelog.d/6077.sdk
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Improve replay attacks and reduce duplicate message index errors
|
1
changelog.d/6100.misc
Normal file
1
changelog.d/6100.misc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Excludes transitive optional non FOSS google location dependency from fdroid builds
|
1
changelog.d/6123.wip
Normal file
1
changelog.d/6123.wip
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[Live location sharing] Update entity in DB when a live is timed out
|
1
changelog.d/6140.bugfix
Normal file
1
changelog.d/6140.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Prevent widget web view from reloading on screen / orientation change
|
1
changelog.d/6141.misc
Normal file
1
changelog.d/6141.misc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Downgrade gradle from 7.2.0 to 7.1.3
|
1
changelog.d/6148.bugfix
Normal file
1
changelog.d/6148.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Fix decrypting redacted event from sending errors
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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?,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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(
|
||||||
|
|
|
@ -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()
|
||||||
)
|
)
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 -->
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
5
vector/src/main/res/layout/fragment_simple_container.xml
Normal file
5
vector/src/main/res/layout/fragment_simple_container.xml
Normal 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" />
|
|
@ -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"
|
||||||
|
|
|
@ -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(),
|
Loading…
Reference in a new issue