mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-03-18 12:18:48 +03:00
Merge branch 'develop' into feature/ons/live_location_bottom_sheet
* develop: (114 commits) Docs: Fix various formatting and spelling issues in notifications.md Fixing non necessary breaking line continuing to the originally supplied url when a rtl override character is detected splitting url detection condition into separate branches Cleaner code Create extension `String?.toActiveSpaceOrOrphanRooms()` to reduce noise. Add changelog Fix test compilation Add some Kdoc Add some Kdoc Create SpaceFilter.OrphanRooms to improve the API. Not 100% of the side effect. There is probably some (fixed?) bugs here. Rename ActiveSpaceFilter to SpaceFilter Remove `ActiveSpaceFilter.None` Prefer nullability for API coherency of `RoomSummaryQueryParams` Add some Kdoc Remove duplicated lines of code (the same code is done a few lines later) Remove `RoomCategoryFilter.ALL` Prefer nullability for API coherency of `RoomSummaryQueryParams` `displayName` default value is now `QueryStringValue.NoCondition`. It was working fine since in the DB we always have a name using `RoomDisplayNameFallbackProvider`, which in our current implementation always return a non empty String. Small rework for nicer code Remove duplicated code lines Remove `roomId` from `RoomSummaryQueryParams.Builder()`. Create a new API in RoomService to observe a room summary from a roomId. ... # Conflicts: # vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt
This commit is contained in:
commit
04679ea21d
121 changed files with 2583 additions and 1115 deletions
1
changelog.d/5283.wip
Normal file
1
changelog.d/5283.wip
Normal file
|
@ -0,0 +1 @@
|
|||
FTUE - Adds the redesigned Sign In screen
|
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/5860.feature
Normal file
1
changelog.d/5860.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Adds space or user id as a subtitle under rooms in search
|
1
changelog.d/6073.feature
Normal file
1
changelog.d/6073.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Adds up navigation in spaces
|
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/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
|
3
changelog.d/6143.sdk
Normal file
3
changelog.d/6143.sdk
Normal file
|
@ -0,0 +1,3 @@
|
|||
Remove `RoomSummaryQueryParams.roomId`. If you need to observe a single room, use the new API `RoomService.getRoomSummaryLive(roomId: String)`
|
||||
- `ActiveSpaceFilter` has been renamed to `SpaceFilter`
|
||||
- `RoomCategoryFilter.ALL` has been removed, just pass `null` to not filter on Room category.
|
1
changelog.d/6148.bugfix
Normal file
1
changelog.d/6148.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Fix decrypting redacted event from sending errors
|
1
changelog.d/6163.feature
Normal file
1
changelog.d/6163.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Security - Asking for user confirmation when tapping URLs which contain unicode directional overrides
|
|
@ -7,10 +7,13 @@ ext.versions = [
|
|||
'targetCompat' : JavaVersion.VERSION_11,
|
||||
]
|
||||
|
||||
def gradle = "7.2.0"
|
||||
|
||||
// Pinned to 7.1.3 because of https://github.com/vector-im/element-android/issues/6142
|
||||
// Please test carefully before upgrading again.
|
||||
def gradle = "7.1.3"
|
||||
// Ref: https://kotlinlang.org/releases.html
|
||||
def kotlin = "1.6.21"
|
||||
def kotlinCoroutines = "1.6.1"
|
||||
def kotlinCoroutines = "1.6.2"
|
||||
def dagger = "2.42"
|
||||
def retrofit = "2.9.0"
|
||||
def arrow = "0.8.2"
|
||||
|
@ -23,7 +26,7 @@ def mavericks = "2.6.1"
|
|||
def glide = "4.13.2"
|
||||
def bigImageViewer = "1.8.1"
|
||||
def jjwt = "0.11.5"
|
||||
def vanniktechEmoji = "0.9.0"
|
||||
def vanniktechEmoji = "0.15.0"
|
||||
|
||||
// Testing
|
||||
def mockk = "1.12.4"
|
||||
|
@ -107,6 +110,10 @@ ext.libs = [
|
|||
'mavericks' : "com.airbnb.android:mavericks:$mavericks",
|
||||
'mavericksTesting' : "com.airbnb.android:mavericks-testing:$mavericks"
|
||||
],
|
||||
maplibre : [
|
||||
'androidSdk' : "org.maplibre.gl:android-sdk:9.5.2",
|
||||
'pluginAnnotation' : "org.maplibre.gl:android-plugin-annotation-v9:1.0.0"
|
||||
],
|
||||
mockk : [
|
||||
'mockk' : "io.mockk:mockk:$mockk",
|
||||
'mockkAndroid' : "io.mockk:mockk-android:$mockk"
|
||||
|
|
|
@ -11,7 +11,7 @@ This document aims to describe how Element android displays notifications to the
|
|||
* [Background processing limitations](#background-processing-limitations)
|
||||
2. [Element Notification implementations](#element-notification-implementations)
|
||||
* [Requirements](#requirements)
|
||||
* [Foreground sync mode (Gplay & F-Droid)](#foreground-sync-mode-gplay-f-droid)
|
||||
* [Foreground sync mode (Gplay & F-Droid)](#foreground-sync-mode-gplay--f-droid)
|
||||
* [Push (FCM) received in background](#push-fcm-received-in-background)
|
||||
* [FCM Fallback mode](#fcm-fallback-mode)
|
||||
* [F-Droid background Mode](#f-droid-background-mode)
|
||||
|
@ -28,10 +28,10 @@ In order to get messages from a homeserver, a matrix client need to perform a ``
|
|||
|
||||
`To read events, the intended flow of operation is for clients to first call the /sync API without a since parameter. This returns the most recent message events for each room, as well as the state of the room at the start of the returned timeline. `
|
||||
|
||||
The client need to call the `sync`API periodically in order to get incremental updates of the server state (new messages).
|
||||
The client need to call the `sync` API periodically in order to get incremental updates of the server state (new messages).
|
||||
This mechanism is known as **HTTP long Polling**.
|
||||
|
||||
Using the **HTTP Long Polling** mechanism a client polls a server requesting new information.
|
||||
Using the **HTTP Long Polling** mechanism a client polls a server requesting new information.
|
||||
The server *holds the request open until new data is available*.
|
||||
Once available, the server responds and sends the new information.
|
||||
When the client receives the new information, it immediately sends another request, and the operation is repeated.
|
||||
|
@ -66,15 +66,15 @@ FCM will only work on android devices that have Google plays services installed
|
|||
(In simple terms, Google Play Services is a background service that runs on Android, which in turn helps in integrating Google’s advanced functionalities to other applications)
|
||||
|
||||
De-Googlified devices need to rely on something else in order to stay up to date with a server.
|
||||
There some cases when devices with google services cannot use FCM (network infrastructure limitations -firewalls- ,
|
||||
privacy and or independency requirement, source code licence)
|
||||
There some cases when devices with google services cannot use FCM (network infrastructure limitations -firewalls-,
|
||||
privacy and or independence requirement, source code licence)
|
||||
|
||||
## Push VS Notification
|
||||
|
||||
This need some disambiguation, because it is the source of common confusion:
|
||||
|
||||
|
||||
*The fact that you see a notification on your screen does not mean that you have successfully configured your PUSH plateform.*
|
||||
*The fact that you see a notification on your screen does not mean that you have successfully configured your PUSH platform.*
|
||||
|
||||
Technically there is a difference between a push and a notification. A notification is what you see on screen and/or in the notification Menu/Drawer (in the top bar of the phone).
|
||||
|
||||
|
@ -118,7 +118,7 @@ Client/Server API + | | | | |
|
|||
```
|
||||
|
||||
Recommended reading:
|
||||
* https://thomask.sdf.org/blog/2016/12/11/riots-magical-push-notifications-in-ios.html
|
||||
* https://thomask.sdf.org/blog/2016/12/11/riots-magical-push-notifications-in-ios.html
|
||||
* https://matrix.org/docs/spec/client_server/r0.4.0.html#id128
|
||||
|
||||
|
||||
|
@ -183,7 +183,7 @@ As this mode does not need to live beyond the scope of the application, and as p
|
|||
|
||||
This mode is turned on when the app enters foreground, and off when enters background.
|
||||
|
||||
In background, and depending on wether push is available or not, Element will use different methods to perform the syncs (Workers / Alarms / Service)
|
||||
In background, and depending on whether push is available or not, Element will use different methods to perform the syncs (Workers / Alarms / Service)
|
||||
|
||||
## Push (FCM) received in background
|
||||
|
||||
|
@ -228,7 +228,7 @@ Element implements several strategies in these cases (TODO document)
|
|||
## FCM Fallback mode
|
||||
|
||||
It is possible that Element is not able to get a FCM push token.
|
||||
Common errors (amoung several others) that can cause that:
|
||||
Common errors (among several others) that can cause that:
|
||||
* Google Play Services is outdated
|
||||
* Google Play Service fails in someways with FCM servers (infamous `SERVICE_NOT_AVAILABLE`)
|
||||
|
||||
|
@ -256,7 +256,7 @@ Only solution left is to use `AlarmManager`, that offers new API to allow launch
|
|||
|
||||
Notice that these alarms, due to their potential impact on battery life, can still be restricted by the system. Documentation says that they will not be triggered more than every minutes under normal system operation, and when in low power mode about every 15 mn.
|
||||
|
||||
These restrictions can be relaxed by requirering the app to be white listed from battery optimization.
|
||||
These restrictions can be relaxed by requiring the app to be white listed from battery optimization.
|
||||
|
||||
F-Droid version will schedule alarms that will then trigger a Broadcast Receiver, that in turn will launch a Service (in the classic android way), and the reschedule an alarm for next time.
|
||||
|
||||
|
|
|
@ -45,6 +45,13 @@ import org.matrix.android.sdk.api.util.toOptional
|
|||
|
||||
class FlowSession(private val session: Session) {
|
||||
|
||||
fun liveRoomSummary(roomId: String): Flow<Optional<RoomSummary>> {
|
||||
return session.roomService().getRoomSummaryLive(roomId).asFlow()
|
||||
.startWith(session.coroutineDispatchers.io) {
|
||||
session.roomService().getRoomSummary(roomId).toOptional()
|
||||
}
|
||||
}
|
||||
|
||||
fun liveRoomSummaries(queryParams: RoomSummaryQueryParams, sortOrder: RoomSortOrder = RoomSortOrder.NONE): Flow<List<RoomSummary>> {
|
||||
return session.roomService().getRoomSummariesLive(queryParams, sortOrder).asFlow()
|
||||
.startWith(session.coroutineDispatchers.io) {
|
||||
|
|
|
@ -198,7 +198,7 @@ dependencies {
|
|||
implementation libs.apache.commonsImaging
|
||||
|
||||
// Phone number https://github.com/google/libphonenumber
|
||||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.48'
|
||||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.49'
|
||||
|
||||
testImplementation libs.tests.junit
|
||||
testImplementation 'org.robolectric:robolectric:4.7.3'
|
||||
|
|
|
@ -24,8 +24,8 @@ 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.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
|
||||
import org.matrix.android.sdk.common.SessionTestParams
|
||||
import org.matrix.android.sdk.common.TestConstants
|
||||
|
||||
|
@ -34,32 +34,22 @@ import org.matrix.android.sdk.common.TestConstants
|
|||
@LargeTest
|
||||
class AccountCreationTest : InstrumentedTest {
|
||||
|
||||
private val commonTestHelper = CommonTestHelper(context())
|
||||
private val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||
|
||||
@Test
|
||||
fun createAccountTest() {
|
||||
val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true))
|
||||
|
||||
commonTestHelper.signOutAndClose(session)
|
||||
fun createAccountTest() = runSessionTest(context()) { commonTestHelper ->
|
||||
commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("This test will be ignored until it is fixed")
|
||||
fun createAccountAndLoginAgainTest() {
|
||||
fun createAccountAndLoginAgainTest() = runSessionTest(context()) { commonTestHelper ->
|
||||
val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true))
|
||||
|
||||
// Log again to the same account
|
||||
val session2 = commonTestHelper.logIntoAccount(session.myUserId, SessionTestParams(withInitialSync = true))
|
||||
|
||||
commonTestHelper.signOutAndClose(session)
|
||||
commonTestHelper.signOutAndClose(session2)
|
||||
commonTestHelper.logIntoAccount(session.myUserId, SessionTestParams(withInitialSync = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun simpleE2eTest() {
|
||||
val res = cryptoTestHelper.doE2ETestWithAliceInARoom()
|
||||
|
||||
res.cleanUp(commonTestHelper)
|
||||
fun simpleE2eTest() = runCryptoTest(context()) { cryptoTestHelper, _ ->
|
||||
cryptoTestHelper.doE2ETestWithAliceInARoom()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ import org.junit.runners.JUnit4
|
|||
import org.junit.runners.MethodSorters
|
||||
import org.matrix.android.sdk.InstrumentedTest
|
||||
import org.matrix.android.sdk.api.failure.isInvalidPassword
|
||||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
|
||||
import org.matrix.android.sdk.common.SessionTestParams
|
||||
import org.matrix.android.sdk.common.TestConstants
|
||||
|
||||
|
@ -34,14 +34,12 @@ import org.matrix.android.sdk.common.TestConstants
|
|||
@Ignore("This test will be ignored until it is fixed")
|
||||
class ChangePasswordTest : InstrumentedTest {
|
||||
|
||||
private val commonTestHelper = CommonTestHelper(context())
|
||||
|
||||
companion object {
|
||||
private const val NEW_PASSWORD = "this is a new password"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun changePasswordTest() {
|
||||
fun changePasswordTest() = runSessionTest(context()) { commonTestHelper ->
|
||||
val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = false))
|
||||
|
||||
// Change password
|
||||
|
@ -54,9 +52,6 @@ class ChangePasswordTest : InstrumentedTest {
|
|||
throwable.isInvalidPassword().shouldBeTrue()
|
||||
|
||||
// Try to login with the new password, should work
|
||||
val session2 = commonTestHelper.logIntoAccount(session.myUserId, NEW_PASSWORD, SessionTestParams(withInitialSync = false))
|
||||
|
||||
commonTestHelper.signOutAndClose(session)
|
||||
commonTestHelper.signOutAndClose(session2)
|
||||
commonTestHelper.logIntoAccount(session.myUserId, NEW_PASSWORD, SessionTestParams(withInitialSync = false))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
|||
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.failure.MatrixError
|
||||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
|
||||
import org.matrix.android.sdk.common.SessionTestParams
|
||||
import org.matrix.android.sdk.common.TestConstants
|
||||
import kotlin.coroutines.Continuation
|
||||
|
@ -39,10 +39,8 @@ import kotlin.coroutines.resume
|
|||
@FixMethodOrder(MethodSorters.JVM)
|
||||
class DeactivateAccountTest : InstrumentedTest {
|
||||
|
||||
private val commonTestHelper = CommonTestHelper(context())
|
||||
|
||||
@Test
|
||||
fun deactivateAccountTest() {
|
||||
fun deactivateAccountTest() = runSessionTest(context(), false /* session will be deactivated */) { commonTestHelper ->
|
||||
val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true))
|
||||
|
||||
// Deactivate the account
|
||||
|
|
|
@ -23,7 +23,7 @@ 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.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
|
||||
import org.matrix.android.sdk.common.SessionTestParams
|
||||
import org.matrix.android.sdk.common.TestConstants
|
||||
import timber.log.Timber
|
||||
|
@ -32,10 +32,8 @@ import timber.log.Timber
|
|||
@FixMethodOrder(MethodSorters.JVM)
|
||||
class ApiInterceptorTest : InstrumentedTest {
|
||||
|
||||
private val commonTestHelper = CommonTestHelper(context())
|
||||
|
||||
@Test
|
||||
fun apiInterceptorTest() {
|
||||
fun apiInterceptorTest() = runSessionTest(context()) { commonTestHelper ->
|
||||
val responses = mutableListOf<String>()
|
||||
|
||||
val listener = object : ApiInterceptorListener {
|
||||
|
|
|
@ -54,12 +54,39 @@ import java.util.concurrent.TimeUnit
|
|||
* This class exposes methods to be used in common cases
|
||||
* Registration, login, Sync, Sending messages...
|
||||
*/
|
||||
class CommonTestHelper(context: Context) {
|
||||
class CommonTestHelper private constructor(context: Context) {
|
||||
|
||||
companion object {
|
||||
internal fun runSessionTest(context: Context, autoSignoutOnClose: Boolean = true, block: (CommonTestHelper) -> Unit) {
|
||||
val testHelper = CommonTestHelper(context)
|
||||
return try {
|
||||
block(testHelper)
|
||||
} finally {
|
||||
if (autoSignoutOnClose) {
|
||||
testHelper.cleanUpOpenedSessions()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun runCryptoTest(context: Context, autoSignoutOnClose: Boolean = true, block: (CryptoTestHelper, CommonTestHelper) -> Unit) {
|
||||
val testHelper = CommonTestHelper(context)
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
return try {
|
||||
block(cryptoTestHelper, testHelper)
|
||||
} finally {
|
||||
if (autoSignoutOnClose) {
|
||||
testHelper.cleanUpOpenedSessions()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal val matrix: TestMatrix
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||
private var accountNumber = 0
|
||||
|
||||
private val trackedSessions = mutableListOf<Session>()
|
||||
|
||||
fun getTestInterceptor(session: Session): MockOkHttpInterceptor? = TestModule.interceptorForSession(session.sessionId) as? MockOkHttpInterceptor
|
||||
|
||||
init {
|
||||
|
@ -84,6 +111,15 @@ class CommonTestHelper(context: Context) {
|
|||
return logIntoAccount(userId, TestConstants.PASSWORD, testParams)
|
||||
}
|
||||
|
||||
fun cleanUpOpenedSessions() {
|
||||
trackedSessions.forEach {
|
||||
runBlockingTest {
|
||||
it.signOutService().signOut(true)
|
||||
}
|
||||
}
|
||||
trackedSessions.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a homeserver configuration, with Http connection allowed for test
|
||||
*/
|
||||
|
@ -245,7 +281,9 @@ class CommonTestHelper(context: Context) {
|
|||
testParams
|
||||
)
|
||||
assertNotNull(session)
|
||||
return session
|
||||
return session.also {
|
||||
trackedSessions.add(session)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -261,7 +299,9 @@ class CommonTestHelper(context: Context) {
|
|||
testParams: SessionTestParams): Session {
|
||||
val session = logAccountAndSync(userId, password, testParams)
|
||||
assertNotNull(session)
|
||||
return session
|
||||
return session.also {
|
||||
trackedSessions.add(session)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -379,8 +419,8 @@ class CommonTestHelper(context: Context) {
|
|||
*/
|
||||
fun await(latch: CountDownLatch, timeout: Long? = TestConstants.timeOutMillis) {
|
||||
assertTrue(
|
||||
"Timed out after " + timeout + "ms waiting for something to happen. See stacktrace for cause.",
|
||||
latch.await(timeout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS)
|
||||
"Timed out after " + timeout + "ms waiting for something to happen. See stacktrace for cause.",
|
||||
latch.await(timeout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -436,6 +476,7 @@ class CommonTestHelper(context: Context) {
|
|||
fun Iterable<Session>.signOutAndClose() = forEach { signOutAndClose(it) }
|
||||
|
||||
fun signOutAndClose(session: Session) {
|
||||
trackedSessions.remove(session)
|
||||
runBlockingTest(timeout = 60_000) {
|
||||
session.signOutService().signOut(true)
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ import java.util.UUID
|
|||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class CryptoTestHelper(private val testHelper: CommonTestHelper) {
|
||||
class CryptoTestHelper(val testHelper: CommonTestHelper) {
|
||||
|
||||
private val messagesFromAlice: List<String> = listOf("0 - Hello I'm Alice!", "4 - Go!")
|
||||
private val messagesFromBob: List<String> = listOf("1 - Hello I'm Bob!", "2 - Isn't life grand?", "3 - Let's go to the opera.")
|
||||
|
|
|
@ -40,6 +40,9 @@ class RetryTestRule(val retryCount: Int = 3) : TestRule {
|
|||
for (i in 0 until retryCount) {
|
||||
try {
|
||||
base.evaluate()
|
||||
if (i > 0) {
|
||||
println("Retried test $i times")
|
||||
}
|
||||
return
|
||||
} catch (t: Throwable) {
|
||||
caughtThrowable = t
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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.Companion.runCryptoTest
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
class DecryptRedactedEventTest : InstrumentedTest {
|
||||
|
||||
@Test
|
||||
fun doNotFailToDecryptRedactedEvent() = runCryptoTest(context()) { 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.junit.Assert
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
@ -57,7 +58,8 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
|||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
|
||||
import org.matrix.android.sdk.common.RetryTestRule
|
||||
import org.matrix.android.sdk.common.SessionTestParams
|
||||
import org.matrix.android.sdk.common.TestConstants
|
||||
|
@ -68,6 +70,7 @@ import java.util.concurrent.CountDownLatch
|
|||
@RunWith(JUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
@LargeTest
|
||||
@Ignore("This test fails with an unhandled exception thrown from a coroutine which terminates the entire test run.")
|
||||
class E2eeSanityTests : InstrumentedTest {
|
||||
|
||||
@get:Rule val rule = RetryTestRule(3)
|
||||
|
@ -82,9 +85,7 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
* Alice sends a new message, then check that the new one can be decrypted
|
||||
*/
|
||||
@Test
|
||||
fun testSendingE2EEMessages() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun testSendingE2EEMessages() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
|
@ -198,21 +199,12 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
otherAccounts.forEach {
|
||||
testHelper.signOutAndClose(it)
|
||||
}
|
||||
newAccount.forEach { testHelper.signOutAndClose(it) }
|
||||
|
||||
cryptoTestData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testKeyGossipingIsEnabledByDefault() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
fun testKeyGossipingIsEnabledByDefault() = runSessionTest(context()) { testHelper ->
|
||||
val session = testHelper.createAccount("alice", SessionTestParams(true))
|
||||
Assert.assertTrue("Key gossiping should be enabled by default", session.cryptoService().isKeyGossipingEnabled())
|
||||
testHelper.signOutAndClose(session)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -230,9 +222,7 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
* 9. Check that new session can decrypt
|
||||
*/
|
||||
@Test
|
||||
fun testBasicBackupImport() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun testBasicBackupImport() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
|
@ -344,8 +334,6 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
|
||||
// ensure bob can now decrypt
|
||||
cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
|
||||
|
||||
testHelper.signOutAndClose(newBobSession)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -353,9 +341,7 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
* get them from an older one.
|
||||
*/
|
||||
@Test
|
||||
fun testSimpleGossip() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun testSimpleGossip() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
|
@ -449,18 +435,13 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
}
|
||||
|
||||
cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
|
||||
|
||||
cryptoTestData.cleanUp(testHelper)
|
||||
testHelper.signOutAndClose(newBobSession)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that if a better key is forwarded (lower index, it is then used)
|
||||
*/
|
||||
@Test
|
||||
fun testForwardBetterKey() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun testForwardBetterKey() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
|
@ -576,10 +557,6 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
canDecryptFirst && canDecryptSecond
|
||||
}
|
||||
}
|
||||
|
||||
testHelper.signOutAndClose(aliceSession)
|
||||
testHelper.signOutAndClose(bobSessionWithBetterKey)
|
||||
testHelper.signOutAndClose(newBobSession)
|
||||
}
|
||||
|
||||
private fun sendMessageInRoom(testHelper: CommonTestHelper, aliceRoomPOV: Room, text: String): String? {
|
||||
|
@ -610,9 +587,7 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
* Test that if a better key is forwared (lower index, it is then used)
|
||||
*/
|
||||
@Test
|
||||
fun testSelfInteractiveVerificationAndGossip() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun testASelfInteractiveVerificationAndGossip() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
|
||||
val aliceSession = testHelper.createAccount("alice", SessionTestParams(true))
|
||||
cryptoTestHelper.bootstrapSecurity(aliceSession)
|
||||
|
@ -751,9 +726,6 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
aliceSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.version,
|
||||
aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.version
|
||||
)
|
||||
|
||||
testHelper.signOutAndClose(aliceSession)
|
||||
testHelper.signOutAndClose(aliceNewSession)
|
||||
}
|
||||
|
||||
private fun ensureMembersHaveJoined(testHelper: CommonTestHelper, aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String) {
|
||||
|
|
|
@ -30,18 +30,14 @@ import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventCon
|
|||
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
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
class PreShareKeysTest : InstrumentedTest {
|
||||
|
||||
private val testHelper = CommonTestHelper(context())
|
||||
private val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
|
||||
@Test
|
||||
fun ensure_outbound_session_happy_path() {
|
||||
fun ensure_outbound_session_happy_path() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||
val e2eRoomID = testData.roomId
|
||||
val aliceSession = testData.firstSession
|
||||
|
@ -94,7 +90,5 @@ class PreShareKeysTest : InstrumentedTest {
|
|||
bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEvent.eventId)?.root?.getClearType() == EventType.MESSAGE
|
||||
}
|
||||
}
|
||||
|
||||
testData.cleanUp(testHelper)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,8 +38,7 @@ import org.matrix.android.sdk.api.session.getRoom
|
|||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
|
||||
import org.matrix.android.sdk.common.TestConstants
|
||||
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm
|
||||
|
@ -63,8 +62,6 @@ import kotlin.coroutines.resume
|
|||
class UnwedgingTest : InstrumentedTest {
|
||||
|
||||
private lateinit var messagesReceivedByBob: List<TimelineEvent>
|
||||
private val testHelper = CommonTestHelper(context())
|
||||
private val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
|
||||
@Before
|
||||
fun init() {
|
||||
|
@ -85,7 +82,7 @@ class UnwedgingTest : InstrumentedTest {
|
|||
* -> This is automatically fixed after SDKs restarted the olm session
|
||||
*/
|
||||
@Test
|
||||
fun testUnwedging() {
|
||||
fun testUnwedging() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
|
@ -240,8 +237,6 @@ class UnwedgingTest : InstrumentedTest {
|
|||
}
|
||||
|
||||
bobTimeline.dispose()
|
||||
|
||||
cryptoTestData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
private fun createEventListener(latch: CountDownLatch, expectedNumberOfMessages: Int): Timeline.Listener {
|
||||
|
|
|
@ -37,8 +37,8 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.isCrossSignedVerif
|
|||
import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified
|
||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
||||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
|
||||
import org.matrix.android.sdk.common.SessionTestParams
|
||||
import org.matrix.android.sdk.common.TestConstants
|
||||
import kotlin.coroutines.Continuation
|
||||
|
@ -49,11 +49,8 @@ import kotlin.coroutines.resume
|
|||
@LargeTest
|
||||
class XSigningTest : InstrumentedTest {
|
||||
|
||||
private val testHelper = CommonTestHelper(context())
|
||||
private val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
|
||||
@Test
|
||||
fun test_InitializeAndStoreKeys() {
|
||||
fun test_InitializeAndStoreKeys() = runSessionTest(context()) { testHelper ->
|
||||
val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
|
||||
|
||||
testHelper.doSync<Unit> {
|
||||
|
@ -87,7 +84,7 @@ class XSigningTest : InstrumentedTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun test_CrossSigningCheckBobSeesTheKeys() {
|
||||
fun test_CrossSigningCheckBobSeesTheKeys() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
|
@ -137,12 +134,10 @@ class XSigningTest : InstrumentedTest {
|
|||
)
|
||||
|
||||
assertFalse("Bob keys from alice pov should not be trusted", bobKeysFromAlicePOV.isTrusted())
|
||||
|
||||
cryptoTestData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_CrossSigningTestAliceTrustBobNewDevice() {
|
||||
fun test_CrossSigningTestAliceTrustBobNewDevice() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
|
@ -216,9 +211,5 @@ class XSigningTest : InstrumentedTest {
|
|||
|
||||
val result = aliceSession.cryptoService().crossSigningService().checkDeviceTrust(bobUserId, bobSecondDeviceId, null)
|
||||
assertTrue("Bob second device should be trusted from alice POV", result.isCrossSignedVerified())
|
||||
|
||||
testHelper.signOutAndClose(aliceSession)
|
||||
testHelper.signOutAndClose(bobSession)
|
||||
testHelper.signOutAndClose(bobSession2)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
|||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
|
||||
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
|
@ -42,35 +43,36 @@ import java.util.concurrent.CountDownLatch
|
|||
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||
class EncryptionTest : InstrumentedTest {
|
||||
|
||||
private val testHelper = CommonTestHelper(context())
|
||||
private val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
|
||||
@Test
|
||||
fun test_EncryptionEvent() {
|
||||
performTest(roomShouldBeEncrypted = false) { room ->
|
||||
// Send an encryption Event as an Event (and not as a state event)
|
||||
room.sendService().sendEvent(
|
||||
eventType = EventType.STATE_ROOM_ENCRYPTION,
|
||||
content = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_EncryptionStateEvent() {
|
||||
performTest(roomShouldBeEncrypted = true) { room ->
|
||||
runBlocking {
|
||||
// Send an encryption Event as a State Event
|
||||
room.stateService().sendStateEvent(
|
||||
runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
performTest(cryptoTestHelper, testHelper, roomShouldBeEncrypted = false) { room ->
|
||||
// Send an encryption Event as an Event (and not as a state event)
|
||||
room.sendService().sendEvent(
|
||||
eventType = EventType.STATE_ROOM_ENCRYPTION,
|
||||
stateKey = "",
|
||||
body = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent()
|
||||
content = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun performTest(roomShouldBeEncrypted: Boolean, action: (Room) -> Unit) {
|
||||
@Test
|
||||
fun test_EncryptionStateEvent() {
|
||||
runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
performTest(cryptoTestHelper, testHelper, roomShouldBeEncrypted = true) { room ->
|
||||
runBlocking {
|
||||
// Send an encryption Event as a State Event
|
||||
room.stateService().sendStateEvent(
|
||||
eventType = EventType.STATE_ROOM_ENCRYPTION,
|
||||
stateKey = "",
|
||||
body = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun performTest(cryptoTestHelper: CryptoTestHelper, testHelper: CommonTestHelper, roomShouldBeEncrypted: Boolean, action: (Room) -> Unit) {
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(encryptedRoom = false)
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
|
@ -109,6 +111,5 @@ class EncryptionTest : InstrumentedTest {
|
|||
room.roomCryptoService().isEncrypted() shouldBe roomShouldBeEncrypted
|
||||
it.countDown()
|
||||
}
|
||||
cryptoTestData.cleanUp(testHelper)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,8 +41,7 @@ import org.matrix.android.sdk.api.session.room.getTimelineEvent
|
|||
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
|
||||
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
||||
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
||||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
|
||||
import org.matrix.android.sdk.common.RetryTestRule
|
||||
import org.matrix.android.sdk.common.SessionTestParams
|
||||
import org.matrix.android.sdk.common.TestConstants
|
||||
|
@ -56,9 +55,7 @@ class KeyShareTests : InstrumentedTest {
|
|||
@get:Rule val rule = RetryTestRule(3)
|
||||
|
||||
@Test
|
||||
fun test_DoNotSelfShareIfNotTrusted() {
|
||||
val commonTestHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||
fun test_DoNotSelfShareIfNotTrusted() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
|
||||
|
||||
val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
|
||||
Log.v("TEST", "=======> AliceSession 1 is ${aliceSession.sessionParams.deviceId}")
|
||||
|
@ -194,9 +191,7 @@ class KeyShareTests : InstrumentedTest {
|
|||
* if the key was originally shared with him
|
||||
*/
|
||||
@Test
|
||||
fun test_reShareIfWasIntendedToBeShared() {
|
||||
val commonTestHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||
fun test_reShareIfWasIntendedToBeShared() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
|
||||
|
||||
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||
val aliceSession = testData.firstSession
|
||||
|
@ -227,9 +222,7 @@ class KeyShareTests : InstrumentedTest {
|
|||
* if the key was originally shared with him
|
||||
*/
|
||||
@Test
|
||||
fun test_reShareToUnverifiedIfWasIntendedToBeShared() {
|
||||
val commonTestHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||
fun test_reShareToUnverifiedIfWasIntendedToBeShared() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
|
||||
|
||||
val testData = cryptoTestHelper.doE2ETestWithAliceInARoom(true)
|
||||
val aliceSession = testData.firstSession
|
||||
|
@ -266,9 +259,7 @@ class KeyShareTests : InstrumentedTest {
|
|||
* Tests that keys reshared with own verified session are done from the earliest known index
|
||||
*/
|
||||
@Test
|
||||
fun test_reShareFromTheEarliestKnownIndexWithOwnVerifiedSession() {
|
||||
val commonTestHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||
fun test_reShareFromTheEarliestKnownIndexWithOwnVerifiedSession() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
|
||||
|
||||
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||
val aliceSession = testData.firstSession
|
||||
|
@ -388,10 +379,7 @@ class KeyShareTests : InstrumentedTest {
|
|||
* Tests that we don't cancel a request to early on first forward if the index is not good enough
|
||||
*/
|
||||
@Test
|
||||
fun test_dontCancelToEarly() {
|
||||
val commonTestHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||
|
||||
fun test_dontCancelToEarly() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
|
||||
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||
val aliceSession = testData.firstSession
|
||||
val bobSession = testData.secondSession!!
|
||||
|
@ -442,7 +430,7 @@ class KeyShareTests : InstrumentedTest {
|
|||
// Should get a reply from bob and not from alice
|
||||
commonTestHelper.waitWithLatch { latch ->
|
||||
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
// Log.d("#TEST", "outgoing key requests :${aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().joinToString { it.sessionId ?: "?" }}")
|
||||
// Log.d("#TEST", "outgoing key requests :${aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().joinToString { it.sessionId ?: "?" }}")
|
||||
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
|
||||
val bobReply = outgoing?.results?.firstOrNull { it.userId == bobSession.myUserId }
|
||||
val result = bobReply?.result
|
||||
|
|
|
@ -36,8 +36,7 @@ import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
|||
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
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
|
||||
import org.matrix.android.sdk.common.MockOkHttpInterceptor
|
||||
import org.matrix.android.sdk.common.RetryTestRule
|
||||
import org.matrix.android.sdk.common.SessionTestParams
|
||||
|
@ -52,9 +51,7 @@ class WithHeldTests : InstrumentedTest {
|
|||
@get:Rule val rule = RetryTestRule(3)
|
||||
|
||||
@Test
|
||||
fun test_WithHeldUnverifiedReason() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun test_WithHeldUnverifiedReason() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
|
||||
// =============================
|
||||
// ARRANGE
|
||||
|
@ -153,16 +150,10 @@ class WithHeldTests : InstrumentedTest {
|
|||
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
|
||||
}
|
||||
}
|
||||
|
||||
testHelper.signOutAndClose(aliceSession)
|
||||
testHelper.signOutAndClose(bobSession)
|
||||
testHelper.signOutAndClose(bobUnverifiedSession)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_WithHeldNoOlm() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun test_WithHeldNoOlm() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
|
||||
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
val aliceSession = testData.firstSession
|
||||
|
@ -239,14 +230,10 @@ class WithHeldTests : InstrumentedTest {
|
|||
Assert.assertEquals("Alice should have marked bob's device for this session", 1, chainIndex2)
|
||||
|
||||
aliceInterceptor.clearRules()
|
||||
testData.cleanUp(testHelper)
|
||||
testHelper.signOutAndClose(bobSecondSession)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_WithHeldKeyRequest() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun test_WithHeldKeyRequest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
|
||||
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
val aliceSession = testData.firstSession
|
||||
|
@ -293,8 +280,5 @@ class WithHeldTests : InstrumentedTest {
|
|||
wc?.code == WithHeldCode.UNAUTHORISED
|
||||
}
|
||||
}
|
||||
|
||||
testHelper.signOutAndClose(aliceSession)
|
||||
testHelper.signOutAndClose(bobSecondSession)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import org.junit.Assert.assertNotNull
|
|||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.MethodSorters
|
||||
|
@ -43,8 +44,9 @@ import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreation
|
|||
import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult
|
||||
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
|
||||
import org.matrix.android.sdk.common.RetryTestRule
|
||||
import org.matrix.android.sdk.common.TestConstants
|
||||
import org.matrix.android.sdk.common.TestMatrixCallback
|
||||
import java.util.Collections
|
||||
|
@ -55,15 +57,15 @@ import java.util.concurrent.CountDownLatch
|
|||
@LargeTest
|
||||
class KeysBackupTest : InstrumentedTest {
|
||||
|
||||
@get:Rule val rule = RetryTestRule(3)
|
||||
|
||||
/**
|
||||
* - From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys
|
||||
* - Check backup keys after having marked one as backed up
|
||||
* - Reset keys backup markers
|
||||
*/
|
||||
@Test
|
||||
fun roomKeysTest_testBackupStore_ok() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun roomKeysTest_testBackupStore_ok() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
||||
|
||||
|
@ -102,8 +104,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||
* Check that prepareKeysBackupVersionWithPassword returns valid data
|
||||
*/
|
||||
@Test
|
||||
fun prepareKeysBackupVersionTest() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
fun prepareKeysBackupVersionTest() = runSessionTest(context()) { testHelper ->
|
||||
|
||||
val bobSession = testHelper.createAccount(TestConstants.USER_BOB, KeysBackupTestConstants.defaultSessionParams)
|
||||
|
||||
|
@ -125,16 +126,13 @@ class KeysBackupTest : InstrumentedTest {
|
|||
assertNotNull(megolmBackupCreationInfo.recoveryKey)
|
||||
|
||||
stateObserver.stopAndCheckStates(null)
|
||||
testHelper.signOutAndClose(bobSession)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test creating a keys backup version and check that createKeysBackupVersion() returns valid data
|
||||
*/
|
||||
@Test
|
||||
fun createKeysBackupVersionTest() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun createKeysBackupVersionTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
|
||||
val bobSession = testHelper.createAccount(TestConstants.USER_BOB, KeysBackupTestConstants.defaultSessionParams)
|
||||
cryptoTestHelper.initializeCrossSigning(bobSession)
|
||||
|
@ -193,7 +191,6 @@ class KeysBackupTest : InstrumentedTest {
|
|||
}
|
||||
|
||||
stateObserver.stopAndCheckStates(null)
|
||||
testHelper.signOutAndClose(bobSession)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -201,9 +198,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||
* - Check the backup completes
|
||||
*/
|
||||
@Test
|
||||
fun backupAfterCreateKeysBackupVersionTest() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun backupAfterCreateKeysBackupVersionTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
||||
|
@ -238,16 +233,13 @@ class KeysBackupTest : InstrumentedTest {
|
|||
KeysBackupState.ReadyToBackUp
|
||||
)
|
||||
)
|
||||
cryptoTestData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that backupAllGroupSessions() returns valid data
|
||||
*/
|
||||
@Test
|
||||
fun backupAllGroupSessionsTest() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun backupAllGroupSessionsTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
||||
|
@ -281,7 +273,6 @@ class KeysBackupTest : InstrumentedTest {
|
|||
assertEquals("All keys must have been marked as backed up", nbOfKeys, backedUpKeys)
|
||||
|
||||
stateObserver.stopAndCheckStates(null)
|
||||
cryptoTestData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -293,9 +284,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||
* - Compare the decrypted megolm key with the original one
|
||||
*/
|
||||
@Test
|
||||
fun testEncryptAndDecryptKeysBackupData() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun testEncryptAndDecryptKeysBackupData() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
||||
|
@ -330,7 +319,6 @@ class KeysBackupTest : InstrumentedTest {
|
|||
keysBackupTestHelper.assertKeysEquals(session.exportKeys(), sessionData)
|
||||
|
||||
stateObserver.stopAndCheckStates(null)
|
||||
cryptoTestData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -340,9 +328,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||
* - Restore must be successful
|
||||
*/
|
||||
@Test
|
||||
fun restoreKeysBackupTest() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun restoreKeysBackupTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||
|
||||
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
|
||||
|
@ -428,9 +414,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||
* - It must be trusted and must have with 2 signatures now
|
||||
*/
|
||||
@Test
|
||||
fun trustKeyBackupVersionTest() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun trustKeyBackupVersionTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||
|
||||
// - Do an e2e backup to the homeserver with a recovery key
|
||||
|
@ -477,7 +461,6 @@ class KeysBackupTest : InstrumentedTest {
|
|||
assertEquals(2, keysBackupVersionTrust.signatures.size)
|
||||
|
||||
stateObserver.stopAndCheckStates(null)
|
||||
testData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -491,9 +474,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||
* - It must be trusted and must have with 2 signatures now
|
||||
*/
|
||||
@Test
|
||||
fun trustKeyBackupVersionWithRecoveryKeyTest() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun trustKeyBackupVersionWithRecoveryKeyTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||
|
||||
// - Do an e2e backup to the homeserver with a recovery key
|
||||
|
@ -540,7 +521,6 @@ class KeysBackupTest : InstrumentedTest {
|
|||
assertEquals(2, keysBackupVersionTrust.signatures.size)
|
||||
|
||||
stateObserver.stopAndCheckStates(null)
|
||||
testData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -552,9 +532,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||
* - The backup must still be untrusted and disabled
|
||||
*/
|
||||
@Test
|
||||
fun trustKeyBackupVersionWithWrongRecoveryKeyTest() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun trustKeyBackupVersionWithWrongRecoveryKeyTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||
|
||||
// - Do an e2e backup to the homeserver with a recovery key
|
||||
|
@ -583,7 +561,6 @@ class KeysBackupTest : InstrumentedTest {
|
|||
assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state)
|
||||
|
||||
stateObserver.stopAndCheckStates(null)
|
||||
testData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -597,9 +574,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||
* - It must be trusted and must have with 2 signatures now
|
||||
*/
|
||||
@Test
|
||||
fun trustKeyBackupVersionWithPasswordTest() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun trustKeyBackupVersionWithPasswordTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||
|
||||
val password = "Password"
|
||||
|
@ -648,7 +623,6 @@ class KeysBackupTest : InstrumentedTest {
|
|||
assertEquals(2, keysBackupVersionTrust.signatures.size)
|
||||
|
||||
stateObserver.stopAndCheckStates(null)
|
||||
testData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -660,9 +634,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||
* - The backup must still be untrusted and disabled
|
||||
*/
|
||||
@Test
|
||||
fun trustKeyBackupVersionWithWrongPasswordTest() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun trustKeyBackupVersionWithWrongPasswordTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||
|
||||
val password = "Password"
|
||||
|
@ -694,7 +666,6 @@ class KeysBackupTest : InstrumentedTest {
|
|||
assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state)
|
||||
|
||||
stateObserver.stopAndCheckStates(null)
|
||||
testData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -704,9 +675,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||
* - It must fail
|
||||
*/
|
||||
@Test
|
||||
fun restoreKeysBackupWithAWrongRecoveryKeyTest() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun restoreKeysBackupWithAWrongRecoveryKeyTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||
|
||||
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
|
||||
|
@ -730,8 +699,6 @@ class KeysBackupTest : InstrumentedTest {
|
|||
|
||||
// onSuccess may not have been called
|
||||
assertNull(importRoomKeysResult)
|
||||
|
||||
testData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -741,9 +708,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||
* - Restore must be successful
|
||||
*/
|
||||
@Test
|
||||
fun testBackupWithPassword() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun testBackupWithPassword() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||
|
||||
val password = "password"
|
||||
|
@ -790,8 +755,6 @@ class KeysBackupTest : InstrumentedTest {
|
|||
assertEquals(100, (steps[104] as StepProgressListener.Step.ImportingKey).progress)
|
||||
|
||||
keysBackupTestHelper.checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys)
|
||||
|
||||
testData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -801,9 +764,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||
* - It must fail
|
||||
*/
|
||||
@Test
|
||||
fun restoreKeysBackupWithAWrongPasswordTest() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun restoreKeysBackupWithAWrongPasswordTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||
|
||||
val password = "password"
|
||||
|
@ -830,8 +791,6 @@ class KeysBackupTest : InstrumentedTest {
|
|||
|
||||
// onSuccess may not have been called
|
||||
assertNull(importRoomKeysResult)
|
||||
|
||||
testData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -841,9 +800,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||
* - Restore must be successful
|
||||
*/
|
||||
@Test
|
||||
fun testUseRecoveryKeyToRestoreAPasswordBasedKeysBackup() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun testUseRecoveryKeyToRestoreAPasswordBasedKeysBackup() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||
|
||||
val password = "password"
|
||||
|
@ -863,8 +820,6 @@ class KeysBackupTest : InstrumentedTest {
|
|||
}
|
||||
|
||||
keysBackupTestHelper.checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys)
|
||||
|
||||
testData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -874,9 +829,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||
* - It must fail
|
||||
*/
|
||||
@Test
|
||||
fun testUsePasswordToRestoreARecoveryKeyBasedKeysBackup() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun testUsePasswordToRestoreARecoveryKeyBasedKeysBackup() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||
|
||||
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
|
||||
|
@ -900,8 +853,6 @@ class KeysBackupTest : InstrumentedTest {
|
|||
|
||||
// onSuccess may not have been called
|
||||
assertNull(importRoomKeysResult)
|
||||
|
||||
testData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -909,9 +860,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||
* - Check the returned KeysVersionResult is trusted
|
||||
*/
|
||||
@Test
|
||||
fun testIsKeysBackupTrusted() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun testIsKeysBackupTrusted() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||
|
||||
// - Create a backup version
|
||||
|
@ -945,7 +894,6 @@ class KeysBackupTest : InstrumentedTest {
|
|||
assertEquals(signature.device!!.deviceId, cryptoTestData.firstSession.sessionParams.deviceId)
|
||||
|
||||
stateObserver.stopAndCheckStates(null)
|
||||
cryptoTestData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -957,9 +905,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||
* -> That must fail and her backup state must be WrongBackUpVersion
|
||||
*/
|
||||
@Test
|
||||
fun testBackupWhenAnotherBackupWasCreated() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun testBackupWhenAnotherBackupWasCreated() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||
|
||||
// - Create a backup version
|
||||
|
@ -1016,7 +962,6 @@ class KeysBackupTest : InstrumentedTest {
|
|||
assertFalse(keysBackup.isEnabled)
|
||||
|
||||
stateObserver.stopAndCheckStates(null)
|
||||
cryptoTestData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1032,9 +977,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||
* -> It must success
|
||||
*/
|
||||
@Test
|
||||
fun testBackupAfterVerifyingADevice() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun testBackupAfterVerifyingADevice() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||
|
||||
// - Create a backup version
|
||||
|
@ -1125,8 +1068,6 @@ class KeysBackupTest : InstrumentedTest {
|
|||
|
||||
stateObserver.stopAndCheckStates(null)
|
||||
stateObserver2.stopAndCheckStates(null)
|
||||
testHelper.signOutAndClose(aliceSession2)
|
||||
cryptoTestData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1134,9 +1075,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||
* - Delete the backup
|
||||
*/
|
||||
@Test
|
||||
fun deleteKeysBackupTest() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun deleteKeysBackupTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||
|
||||
// - Create a backup version
|
||||
|
@ -1159,6 +1098,5 @@ class KeysBackupTest : InstrumentedTest {
|
|||
assertFalse(keysBackup.isEnabled)
|
||||
|
||||
stateObserver.stopAndCheckStates(null)
|
||||
cryptoTestData.cleanUp(testHelper)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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.Companion.runCryptoTest
|
||||
|
||||
@RunWith(JUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
@LargeTest
|
||||
class ReplayAttackTest : InstrumentedTest {
|
||||
|
||||
@Test
|
||||
fun replayAttackAlreadyDecryptedEventTest() = runCryptoTest(context()) { 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() = runCryptoTest(context()) { 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -40,6 +40,7 @@ import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo
|
|||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.api.util.toBase64NoPadding
|
||||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
|
||||
import org.matrix.android.sdk.common.SessionTestParams
|
||||
import org.matrix.android.sdk.common.TestConstants
|
||||
import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService
|
||||
|
@ -55,8 +56,7 @@ class QuadSTests : InstrumentedTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun test_Generate4SKey() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
fun test_Generate4SKey() = runSessionTest(context()) { testHelper ->
|
||||
|
||||
val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
|
||||
|
||||
|
@ -108,12 +108,11 @@ class QuadSTests : InstrumentedTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun test_StoreSecret() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
fun test_StoreSecret() = runSessionTest(context()) { testHelper ->
|
||||
|
||||
val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
|
||||
val keyId = "My.Key"
|
||||
val info = generatedSecret(aliceSession, keyId, true)
|
||||
val info = generatedSecret(testHelper, aliceSession, keyId, true)
|
||||
|
||||
val keySpec = RawBytesKeySpec.fromRecoveryKey(info.recoveryKey)
|
||||
|
||||
|
@ -127,7 +126,7 @@ class QuadSTests : InstrumentedTest {
|
|||
)
|
||||
}
|
||||
|
||||
val secretAccountData = assertAccountData(aliceSession, "secret.of.life")
|
||||
val secretAccountData = assertAccountData(testHelper, aliceSession, "secret.of.life")
|
||||
|
||||
val encryptedContent = secretAccountData.content["encrypted"] as? Map<*, *>
|
||||
assertNotNull("Element should be encrypted", encryptedContent)
|
||||
|
@ -149,12 +148,10 @@ class QuadSTests : InstrumentedTest {
|
|||
}
|
||||
|
||||
assertEquals("Secret mismatch", clearSecret, decryptedSecret)
|
||||
testHelper.signOutAndClose(aliceSession)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_SetDefaultLocalEcho() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
fun test_SetDefaultLocalEcho() = runSessionTest(context()) { testHelper ->
|
||||
|
||||
val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
|
||||
|
||||
|
@ -170,19 +167,16 @@ class QuadSTests : InstrumentedTest {
|
|||
testHelper.runBlockingTest {
|
||||
quadS.setDefaultKey(TEST_KEY_ID)
|
||||
}
|
||||
|
||||
testHelper.signOutAndClose(aliceSession)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_StoreSecretWithMultipleKey() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
fun test_StoreSecretWithMultipleKey() = runSessionTest(context()) { testHelper ->
|
||||
|
||||
val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
|
||||
val keyId1 = "Key.1"
|
||||
val key1Info = generatedSecret(aliceSession, keyId1, true)
|
||||
val key1Info = generatedSecret(testHelper, aliceSession, keyId1, true)
|
||||
val keyId2 = "Key2"
|
||||
val key2Info = generatedSecret(aliceSession, keyId2, true)
|
||||
val key2Info = generatedSecret(testHelper, aliceSession, keyId2, true)
|
||||
|
||||
val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
|
||||
|
||||
|
@ -221,19 +215,16 @@ class QuadSTests : InstrumentedTest {
|
|||
RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)!!
|
||||
)
|
||||
}
|
||||
|
||||
testHelper.signOutAndClose(aliceSession)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("Test is working locally, not in GitHub actions")
|
||||
fun test_GetSecretWithBadPassphrase() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
fun test_GetSecretWithBadPassphrase() = runSessionTest(context()) { testHelper ->
|
||||
|
||||
val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
|
||||
val keyId1 = "Key.1"
|
||||
val passphrase = "The good pass phrase"
|
||||
val key1Info = generatedSecretFromPassphrase(aliceSession, passphrase, keyId1, true)
|
||||
val key1Info = generatedSecretFromPassphrase(testHelper, aliceSession, passphrase, keyId1, true)
|
||||
|
||||
val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
|
||||
|
||||
|
@ -275,13 +266,9 @@ class QuadSTests : InstrumentedTest {
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
testHelper.signOutAndClose(aliceSession)
|
||||
}
|
||||
|
||||
private fun assertAccountData(session: Session, type: String): UserAccountDataEvent {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
|
||||
private fun assertAccountData(testHelper: CommonTestHelper, session: Session, type: String): UserAccountDataEvent {
|
||||
var accountData: UserAccountDataEvent? = null
|
||||
testHelper.waitWithLatch {
|
||||
val liveAccountData = session.accountDataService().getLiveUserAccountDataEvent(type)
|
||||
|
@ -297,29 +284,27 @@ class QuadSTests : InstrumentedTest {
|
|||
return accountData!!
|
||||
}
|
||||
|
||||
private fun generatedSecret(session: Session, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo {
|
||||
private fun generatedSecret(testHelper: CommonTestHelper, session: Session, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo {
|
||||
val quadS = session.sharedSecretStorageService()
|
||||
val testHelper = CommonTestHelper(context())
|
||||
|
||||
val creationInfo = testHelper.runBlockingTest {
|
||||
quadS.generateKey(keyId, null, keyId, emptyKeySigner)
|
||||
}
|
||||
|
||||
assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId")
|
||||
assertAccountData(testHelper, session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId")
|
||||
|
||||
if (asDefault) {
|
||||
testHelper.runBlockingTest {
|
||||
quadS.setDefaultKey(keyId)
|
||||
}
|
||||
assertAccountData(session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID)
|
||||
assertAccountData(testHelper, session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID)
|
||||
}
|
||||
|
||||
return creationInfo
|
||||
}
|
||||
|
||||
private fun generatedSecretFromPassphrase(session: Session, passphrase: String, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo {
|
||||
private fun generatedSecretFromPassphrase(testHelper: CommonTestHelper, session: Session, passphrase: String, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo {
|
||||
val quadS = session.sharedSecretStorageService()
|
||||
val testHelper = CommonTestHelper(context())
|
||||
|
||||
val creationInfo = testHelper.runBlockingTest {
|
||||
quadS.generateKeyWithPassphrase(
|
||||
|
@ -331,12 +316,12 @@ class QuadSTests : InstrumentedTest {
|
|||
)
|
||||
}
|
||||
|
||||
assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId")
|
||||
assertAccountData(testHelper, session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId")
|
||||
if (asDefault) {
|
||||
testHelper.runBlockingTest {
|
||||
quadS.setDefaultKey(keyId)
|
||||
}
|
||||
assertAccountData(session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID)
|
||||
assertAccountData(testHelper, session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID)
|
||||
}
|
||||
|
||||
return creationInfo
|
||||
|
|
|
@ -44,8 +44,7 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransa
|
|||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
||||
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.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationCancel
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationStart
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.toValue
|
||||
|
@ -56,9 +55,7 @@ import java.util.concurrent.CountDownLatch
|
|||
class SASTest : InstrumentedTest {
|
||||
|
||||
@Test
|
||||
fun test_aliceStartThenAliceCancel() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun test_aliceStartThenAliceCancel() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
|
@ -135,15 +132,11 @@ class SASTest : InstrumentedTest {
|
|||
|
||||
assertNull(bobVerificationService.getExistingTransaction(aliceSession.myUserId, txID))
|
||||
assertNull(aliceVerificationService.getExistingTransaction(bobSession.myUserId, txID))
|
||||
|
||||
cryptoTestData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("This test will be ignored until it is fixed")
|
||||
fun test_key_agreement_protocols_must_include_curve25519() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun test_key_agreement_protocols_must_include_curve25519() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
fail("Not passing for the moment")
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
|
||||
|
@ -195,15 +188,11 @@ class SASTest : InstrumentedTest {
|
|||
testHelper.await(cancelLatch)
|
||||
|
||||
assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod, cancelReason)
|
||||
|
||||
cryptoTestData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("This test will be ignored until it is fixed")
|
||||
fun test_key_agreement_macs_Must_include_hmac_sha256() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun test_key_agreement_macs_Must_include_hmac_sha256() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
fail("Not passing for the moment")
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
|
||||
|
@ -236,15 +225,11 @@ class SASTest : InstrumentedTest {
|
|||
|
||||
val cancelReq = canceledToDeviceEvent!!.content.toModel<KeyVerificationCancel>()!!
|
||||
assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code)
|
||||
|
||||
cryptoTestData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("This test will be ignored until it is fixed")
|
||||
fun test_key_agreement_short_code_include_decimal() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun test_key_agreement_short_code_include_decimal() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
fail("Not passing for the moment")
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
|
||||
|
@ -277,8 +262,6 @@ class SASTest : InstrumentedTest {
|
|||
|
||||
val cancelReq = canceledToDeviceEvent!!.content.toModel<KeyVerificationCancel>()!!
|
||||
assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code)
|
||||
|
||||
cryptoTestData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
private fun fakeBobStart(bobSession: Session,
|
||||
|
@ -314,9 +297,7 @@ class SASTest : InstrumentedTest {
|
|||
// any two devices may only have at most one key verification in flight at a time.
|
||||
// If a device has two verifications in progress with the same device, then it should cancel both verifications.
|
||||
@Test
|
||||
fun test_aliceStartTwoRequests() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun test_aliceStartTwoRequests() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
|
@ -357,9 +338,7 @@ class SASTest : InstrumentedTest {
|
|||
*/
|
||||
@Test
|
||||
@Ignore("This test will be ignored until it is fixed")
|
||||
fun test_aliceAndBobAgreement() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun test_aliceAndBobAgreement() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
|
@ -413,14 +392,10 @@ class SASTest : InstrumentedTest {
|
|||
accepted!!.shortAuthenticationStrings.forEach {
|
||||
assertTrue("all agreed Short Code should be known by alice", startReq!!.shortAuthenticationStrings.contains(it))
|
||||
}
|
||||
|
||||
cryptoTestData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_aliceAndBobSASCode() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun test_aliceAndBobSASCode() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
|
@ -473,14 +448,10 @@ class SASTest : InstrumentedTest {
|
|||
"Should have same SAS", aliceTx.getShortCodeRepresentation(SasMode.DECIMAL),
|
||||
bobTx.getShortCodeRepresentation(SasMode.DECIMAL)
|
||||
)
|
||||
|
||||
cryptoTestData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_happyPath() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun test_happyPath() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
|
@ -553,13 +524,10 @@ class SASTest : InstrumentedTest {
|
|||
|
||||
assertTrue("alice device should be verified from bob point of view", aliceDeviceInfoFromBobPOV!!.isVerified)
|
||||
assertTrue("bob device should be verified from alice point of view", bobDeviceInfoFromAlicePOV!!.isVerified)
|
||||
cryptoTestData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_ConcurrentStart() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
fun test_ConcurrentStart() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
|
@ -646,7 +614,5 @@ class SASTest : InstrumentedTest {
|
|||
bobPovTx?.state == VerificationTxState.ShortCodeReady
|
||||
}
|
||||
}
|
||||
|
||||
cryptoTestData.cleanUp(testHelper)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,8 +31,8 @@ import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
|
|||
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
||||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
|
||||
import org.matrix.android.sdk.common.SessionTestParams
|
||||
import org.matrix.android.sdk.common.TestConstants
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
@ -154,9 +154,7 @@ class VerificationTest : InstrumentedTest {
|
|||
private fun doTest(aliceSupportedMethods: List<VerificationMethod>,
|
||||
bobSupportedMethods: List<VerificationMethod>,
|
||||
expectedResultForAlice: ExpectedResult,
|
||||
expectedResultForBob: ExpectedResult) {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
expectedResultForBob: ExpectedResult) = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
|
@ -251,14 +249,11 @@ class VerificationTest : InstrumentedTest {
|
|||
pr.otherCanShowQrCode() shouldBe expectedResultForBob.otherCanShowQrCode
|
||||
pr.otherCanScanQrCode() shouldBe expectedResultForBob.otherCanScanQrCode
|
||||
}
|
||||
|
||||
cryptoTestData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_selfVerificationAcceptedCancelsItForOtherSessions() {
|
||||
fun test_selfVerificationAcceptedCancelsItForOtherSessions() = runSessionTest(context()) { testHelper ->
|
||||
val defaultSessionParams = SessionTestParams(true)
|
||||
val testHelper = CommonTestHelper(context())
|
||||
|
||||
val aliceSessionToVerify = testHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
|
||||
val aliceSessionThatVerifies = testHelper.logIntoAccount(aliceSessionToVerify.myUserId, TestConstants.PASSWORD, defaultSessionParams)
|
||||
|
|
|
@ -34,8 +34,7 @@ import org.matrix.android.sdk.api.session.events.model.isThread
|
|||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
@RunWith(JUnit4::class)
|
||||
|
@ -44,9 +43,7 @@ import java.util.concurrent.CountDownLatch
|
|||
class ThreadMessagingTest : InstrumentedTest {
|
||||
|
||||
@Test
|
||||
fun reply_in_thread_should_create_a_thread() {
|
||||
val commonTestHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||
fun reply_in_thread_should_create_a_thread() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
|
@ -104,9 +101,7 @@ class ThreadMessagingTest : InstrumentedTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun reply_in_thread_should_create_a_thread_from_other_user() {
|
||||
val commonTestHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||
fun reply_in_thread_should_create_a_thread_from_other_user() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
|
@ -179,9 +174,7 @@ class ThreadMessagingTest : InstrumentedTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun reply_in_thread_to_timeline_message_multiple_times() {
|
||||
val commonTestHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||
fun reply_in_thread_to_timeline_message_multiple_times() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
|
@ -244,9 +237,7 @@ class ThreadMessagingTest : InstrumentedTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun thread_summary_advanced_validation_after_multiple_messages_in_multiple_threads() {
|
||||
val commonTestHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||
fun thread_summary_advanced_validation_after_multiple_messages_in_multiple_threads() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
|
|
|
@ -38,8 +38,7 @@ import org.matrix.android.sdk.api.session.room.model.message.PollType
|
|||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
@RunWith(JUnit4::class)
|
||||
|
@ -47,9 +46,7 @@ import java.util.concurrent.CountDownLatch
|
|||
class PollAggregationTest : InstrumentedTest {
|
||||
|
||||
@Test
|
||||
fun testAllPollUseCases() {
|
||||
val commonTestHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||
fun testAllPollUseCases() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
|
@ -138,7 +135,6 @@ class PollAggregationTest : InstrumentedTest {
|
|||
|
||||
aliceSession.stopSync()
|
||||
aliceTimeline.dispose()
|
||||
cryptoTestData.cleanUp(commonTestHelper)
|
||||
}
|
||||
|
||||
private fun testInitialPollConditions(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {
|
||||
|
|
|
@ -34,8 +34,7 @@ import org.matrix.android.sdk.api.session.getRoom
|
|||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
|
||||
import org.matrix.android.sdk.common.checkSendOrder
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
@ -53,9 +52,7 @@ class TimelineForwardPaginationTest : InstrumentedTest {
|
|||
* This test ensure that if we click to permalink, we will be able to go back to the live
|
||||
*/
|
||||
@Test
|
||||
fun forwardPaginationTest() {
|
||||
val commonTestHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||
fun forwardPaginationTest() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
|
||||
val numberOfMessagesToSend = 90
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
|
||||
|
||||
|
@ -177,7 +174,5 @@ class TimelineForwardPaginationTest : InstrumentedTest {
|
|||
}
|
||||
|
||||
aliceTimeline.dispose()
|
||||
|
||||
cryptoTestData.cleanUp(commonTestHelper)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
|||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||
import org.matrix.android.sdk.common.checkSendOrder
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
@ -48,9 +47,7 @@ class TimelinePreviousLastForwardTest : InstrumentedTest {
|
|||
*/
|
||||
|
||||
@Test
|
||||
fun previousLastForwardTest() {
|
||||
val commonTestHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||
fun previousLastForwardTest() = CommonTestHelper.runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
|
@ -242,7 +239,5 @@ class TimelinePreviousLastForwardTest : InstrumentedTest {
|
|||
}
|
||||
|
||||
bobTimeline.dispose()
|
||||
|
||||
cryptoTestData.cleanUp(commonTestHelper)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,8 +32,7 @@ import org.matrix.android.sdk.api.session.getRoom
|
|||
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
|
||||
import org.matrix.android.sdk.common.TestConstants
|
||||
|
||||
@RunWith(JUnit4::class)
|
||||
|
@ -42,9 +41,7 @@ import org.matrix.android.sdk.common.TestConstants
|
|||
class TimelineSimpleBackPaginationTest : InstrumentedTest {
|
||||
|
||||
@Test
|
||||
fun timeline_backPaginate_shouldReachEndOfTimeline() {
|
||||
val commonTestHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||
fun timeline_backPaginate_shouldReachEndOfTimeline() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
|
||||
val numberOfMessagesToSent = 200
|
||||
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
|
||||
|
@ -102,6 +99,5 @@ class TimelineSimpleBackPaginationTest : InstrumentedTest {
|
|||
assertEquals(numberOfMessagesToSent, onlySentEvents.size)
|
||||
|
||||
bobTimeline.dispose()
|
||||
cryptoTestData.cleanUp(commonTestHelper)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,8 +30,7 @@ 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.model.message.MessageContent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
/** !! Not working with the new timeline
|
||||
|
@ -47,15 +46,12 @@ class TimelineWithManyMembersTest : InstrumentedTest {
|
|||
private const val NUMBER_OF_MEMBERS = 6
|
||||
}
|
||||
|
||||
private val commonTestHelper = CommonTestHelper(context())
|
||||
private val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||
|
||||
/**
|
||||
* Ensures when someone sends a message to a crowded room, everyone can decrypt the message.
|
||||
*/
|
||||
|
||||
@Test
|
||||
fun everyone_should_decrypt_message_in_a_crowded_room() {
|
||||
fun everyone_should_decrypt_message_in_a_crowded_room() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithManyMembers(NUMBER_OF_MEMBERS)
|
||||
|
||||
val sessionForFirstMember = cryptoTestData.firstSession
|
||||
|
|
|
@ -26,9 +26,8 @@ import org.matrix.android.sdk.InstrumentedTest
|
|||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.session.search.SearchResult
|
||||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
|
||||
import org.matrix.android.sdk.common.CryptoTestData
|
||||
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||
|
||||
@RunWith(JUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
|
@ -74,9 +73,7 @@ class SearchMessagesTest : InstrumentedTest {
|
|||
}
|
||||
}
|
||||
|
||||
private fun doTest(block: suspend (CryptoTestData) -> SearchResult) {
|
||||
val commonTestHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||
private fun doTest(block: suspend (CryptoTestData) -> SearchResult) = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val aliceRoomId = cryptoTestData.roomId
|
||||
|
@ -99,7 +96,5 @@ class SearchMessagesTest : InstrumentedTest {
|
|||
(it.event.content?.get("body") as? String)?.startsWith(MESSAGE).orFalse()
|
||||
}.orFalse()
|
||||
)
|
||||
|
||||
cryptoTestData.cleanUp(commonTestHelper)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
|||
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
|
||||
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
|
||||
import org.matrix.android.sdk.api.session.space.JoinSpaceResult
|
||||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
|
||||
import org.matrix.android.sdk.common.SessionTestParams
|
||||
|
||||
@RunWith(JUnit4::class)
|
||||
|
@ -49,8 +49,7 @@ import org.matrix.android.sdk.common.SessionTestParams
|
|||
class SpaceCreationTest : InstrumentedTest {
|
||||
|
||||
@Test
|
||||
fun createSimplePublicSpace() {
|
||||
val commonTestHelper = CommonTestHelper(context())
|
||||
fun createSimplePublicSpace() = runSessionTest(context()) { commonTestHelper ->
|
||||
val session = commonTestHelper.createAccount("Hubble", SessionTestParams(true))
|
||||
val roomName = "My Space"
|
||||
val topic = "A public space for test"
|
||||
|
@ -96,13 +95,10 @@ class SpaceCreationTest : InstrumentedTest {
|
|||
?.toModel<RoomHistoryVisibilityContent>()?.historyVisibility
|
||||
|
||||
assertEquals("Public space room should be world readable", RoomHistoryVisibility.WORLD_READABLE, historyVisibility)
|
||||
|
||||
commonTestHelper.signOutAndClose(session)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testJoinSimplePublicSpace() {
|
||||
val commonTestHelper = CommonTestHelper(context())
|
||||
fun testJoinSimplePublicSpace() = runSessionTest(context()) { commonTestHelper ->
|
||||
|
||||
val aliceSession = commonTestHelper.createAccount("alice", SessionTestParams(true))
|
||||
val bobSession = commonTestHelper.createAccount("bob", SessionTestParams(true))
|
||||
|
@ -134,8 +130,7 @@ class SpaceCreationTest : InstrumentedTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun testSimplePublicSpaceWithChildren() {
|
||||
val commonTestHelper = CommonTestHelper(context())
|
||||
fun testSimplePublicSpaceWithChildren() = runSessionTest(context()) { commonTestHelper ->
|
||||
val aliceSession = commonTestHelper.createAccount("alice", SessionTestParams(true))
|
||||
val bobSession = commonTestHelper.createAccount("bob", SessionTestParams(true))
|
||||
|
||||
|
@ -204,8 +199,5 @@ class SpaceCreationTest : InstrumentedTest {
|
|||
// ).size
|
||||
//
|
||||
// assertEquals("Unexpected number of joined children", 1, childCount)
|
||||
|
||||
commonTestHelper.signOutAndClose(aliceSession)
|
||||
commonTestHelper.signOutAndClose(bobSession)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,8 +29,8 @@ 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.query.ActiveSpaceFilter
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.query.SpaceFilter
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
|
@ -48,6 +48,7 @@ import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
|
|||
import org.matrix.android.sdk.api.session.room.powerlevels.Role
|
||||
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
|
||||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
|
||||
import org.matrix.android.sdk.common.SessionTestParams
|
||||
|
||||
@RunWith(JUnit4::class)
|
||||
|
@ -55,8 +56,7 @@ import org.matrix.android.sdk.common.SessionTestParams
|
|||
class SpaceHierarchyTest : InstrumentedTest {
|
||||
|
||||
@Test
|
||||
fun createCanonicalChildRelation() {
|
||||
val commonTestHelper = CommonTestHelper(context())
|
||||
fun createCanonicalChildRelation() = runSessionTest(context()) { commonTestHelper ->
|
||||
|
||||
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
|
||||
val spaceName = "My Space"
|
||||
|
@ -173,8 +173,7 @@ class SpaceHierarchyTest : InstrumentedTest {
|
|||
// }
|
||||
|
||||
@Test
|
||||
fun testFilteringBySpace() {
|
||||
val commonTestHelper = CommonTestHelper(context())
|
||||
fun testFilteringBySpace() = CommonTestHelper.runSessionTest(context()) { commonTestHelper ->
|
||||
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
|
||||
|
||||
val spaceAInfo = createPublicSpace(
|
||||
|
@ -185,12 +184,12 @@ class SpaceHierarchyTest : InstrumentedTest {
|
|||
)
|
||||
|
||||
/* val spaceBInfo = */ createPublicSpace(
|
||||
session, "SpaceB", listOf(
|
||||
Triple("B1", true /*auto-join*/, true/*canonical*/),
|
||||
Triple("B2", true, true),
|
||||
Triple("B3", true, true)
|
||||
)
|
||||
)
|
||||
session, "SpaceB", listOf(
|
||||
Triple("B1", true /*auto-join*/, true/*canonical*/),
|
||||
Triple("B2", true, true),
|
||||
Triple("B3", true, true)
|
||||
)
|
||||
)
|
||||
|
||||
val spaceCInfo = createPublicSpace(
|
||||
session, "SpaceC", listOf(
|
||||
|
@ -249,15 +248,14 @@ class SpaceHierarchyTest : InstrumentedTest {
|
|||
|
||||
Thread.sleep(6_000)
|
||||
val orphansUpdate = session.roomService().getRoomSummaries(roomSummaryQueryParams {
|
||||
activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null)
|
||||
spaceFilter = SpaceFilter.OrphanRooms
|
||||
})
|
||||
assertEquals("Unexpected number of orphan rooms ${orphansUpdate.map { it.name }}", 2, orphansUpdate.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("This test will be ignored until it is fixed")
|
||||
fun testBreakCycle() {
|
||||
val commonTestHelper = CommonTestHelper(context())
|
||||
fun testBreakCycle() = CommonTestHelper.runSessionTest(context()) { commonTestHelper ->
|
||||
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
|
||||
|
||||
val spaceAInfo = createPublicSpace(
|
||||
|
@ -302,8 +300,7 @@ class SpaceHierarchyTest : InstrumentedTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun testLiveFlatChildren() {
|
||||
val commonTestHelper = CommonTestHelper(context())
|
||||
fun testLiveFlatChildren() = CommonTestHelper.runSessionTest(context()) { commonTestHelper ->
|
||||
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
|
||||
|
||||
val spaceAInfo = createPublicSpace(
|
||||
|
@ -395,25 +392,26 @@ class SpaceHierarchyTest : InstrumentedTest {
|
|||
childInfo: List<Triple<String, Boolean, Boolean?>>
|
||||
/** Name, auto-join, canonical*/
|
||||
): TestSpaceCreationResult {
|
||||
val commonTestHelper = CommonTestHelper(context())
|
||||
var spaceId = ""
|
||||
var roomIds: List<String> = emptyList()
|
||||
commonTestHelper.waitWithLatch { latch ->
|
||||
spaceId = session.spaceService().createSpace(spaceName, "Test Topic", null, true)
|
||||
val syncedSpace = session.spaceService().getSpace(spaceId)
|
||||
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
|
||||
runSessionTest(context()) { commonTestHelper ->
|
||||
commonTestHelper.waitWithLatch { latch ->
|
||||
spaceId = session.spaceService().createSpace(spaceName, "Test Topic", null, true)
|
||||
val syncedSpace = session.spaceService().getSpace(spaceId)
|
||||
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
|
||||
|
||||
roomIds = childInfo.map { entry ->
|
||||
session.roomService().createRoom(CreateRoomParams().apply { name = entry.first })
|
||||
}
|
||||
roomIds.forEachIndexed { index, roomId ->
|
||||
syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second)
|
||||
val canonical = childInfo[index].third
|
||||
if (canonical != null) {
|
||||
session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers)
|
||||
roomIds = childInfo.map { entry ->
|
||||
session.roomService().createRoom(CreateRoomParams().apply { name = entry.first })
|
||||
}
|
||||
roomIds.forEachIndexed { index, roomId ->
|
||||
syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second)
|
||||
val canonical = childInfo[index].third
|
||||
if (canonical != null) {
|
||||
session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers)
|
||||
}
|
||||
}
|
||||
latch.countDown()
|
||||
}
|
||||
latch.countDown()
|
||||
}
|
||||
return TestSpaceCreationResult(spaceId, roomIds)
|
||||
}
|
||||
|
@ -423,51 +421,51 @@ class SpaceHierarchyTest : InstrumentedTest {
|
|||
childInfo: List<Triple<String, Boolean, Boolean?>>
|
||||
/** Name, auto-join, canonical*/
|
||||
): TestSpaceCreationResult {
|
||||
val commonTestHelper = CommonTestHelper(context())
|
||||
var spaceId = ""
|
||||
var roomIds: List<String> = emptyList()
|
||||
commonTestHelper.waitWithLatch { latch ->
|
||||
spaceId = session.spaceService().createSpace(spaceName, "My Private Space", null, false)
|
||||
val syncedSpace = session.spaceService().getSpace(spaceId)
|
||||
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
|
||||
roomIds =
|
||||
childInfo.map { entry ->
|
||||
val homeServerCapabilities = session
|
||||
.homeServerCapabilitiesService()
|
||||
.getHomeServerCapabilities()
|
||||
session.roomService().createRoom(CreateRoomParams().apply {
|
||||
name = entry.first
|
||||
this.featurePreset = RestrictedRoomPreset(
|
||||
homeServerCapabilities,
|
||||
listOf(
|
||||
RoomJoinRulesAllowEntry.restrictedToRoom(spaceId)
|
||||
)
|
||||
)
|
||||
})
|
||||
runSessionTest(context()) { commonTestHelper ->
|
||||
commonTestHelper.waitWithLatch { latch ->
|
||||
spaceId = session.spaceService().createSpace(spaceName, "My Private Space", null, false)
|
||||
val syncedSpace = session.spaceService().getSpace(spaceId)
|
||||
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
|
||||
roomIds =
|
||||
childInfo.map { entry ->
|
||||
val homeServerCapabilities = session
|
||||
.homeServerCapabilitiesService()
|
||||
.getHomeServerCapabilities()
|
||||
session.roomService().createRoom(CreateRoomParams().apply {
|
||||
name = entry.first
|
||||
this.featurePreset = RestrictedRoomPreset(
|
||||
homeServerCapabilities,
|
||||
listOf(
|
||||
RoomJoinRulesAllowEntry.restrictedToRoom(spaceId)
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
roomIds.forEachIndexed { index, roomId ->
|
||||
syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second)
|
||||
val canonical = childInfo[index].third
|
||||
if (canonical != null) {
|
||||
session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers)
|
||||
}
|
||||
roomIds.forEachIndexed { index, roomId ->
|
||||
syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second)
|
||||
val canonical = childInfo[index].third
|
||||
if (canonical != null) {
|
||||
session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers)
|
||||
}
|
||||
latch.countDown()
|
||||
}
|
||||
latch.countDown()
|
||||
}
|
||||
return TestSpaceCreationResult(spaceId, roomIds)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRootSpaces() {
|
||||
val commonTestHelper = CommonTestHelper(context())
|
||||
fun testRootSpaces() = runSessionTest(context()) { commonTestHelper ->
|
||||
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
|
||||
|
||||
/* val spaceAInfo = */ createPublicSpace(
|
||||
session, "SpaceA", listOf(
|
||||
Triple("A1", true /*auto-join*/, true/*canonical*/),
|
||||
Triple("A2", true, true)
|
||||
)
|
||||
)
|
||||
session, "SpaceA", listOf(
|
||||
Triple("A1", true /*auto-join*/, true/*canonical*/),
|
||||
Triple("A2", true, true)
|
||||
)
|
||||
)
|
||||
|
||||
val spaceBInfo = createPublicSpace(
|
||||
session, "SpaceB", listOf(
|
||||
|
@ -506,13 +504,10 @@ class SpaceHierarchyTest : InstrumentedTest {
|
|||
}
|
||||
|
||||
assertEquals("Unexpected number of root spaces ${rootSpaces.map { it.name }}", 2, rootSpaces.size)
|
||||
|
||||
commonTestHelper.signOutAndClose(session)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testParentRelation() {
|
||||
val commonTestHelper = CommonTestHelper(context())
|
||||
fun testParentRelation() = runSessionTest(context()) { commonTestHelper ->
|
||||
val aliceSession = commonTestHelper.createAccount("Alice", SessionTestParams(true))
|
||||
val bobSession = commonTestHelper.createAccount("Bib", SessionTestParams(true))
|
||||
|
||||
|
@ -604,8 +599,5 @@ class SpaceHierarchyTest : InstrumentedTest {
|
|||
bobSession.getRoomSummary(bobRoomId)?.flattenParentIds?.contains(spaceAInfo.spaceId) == true
|
||||
}
|
||||
}
|
||||
|
||||
commonTestHelper.signOutAndClose(aliceSession)
|
||||
commonTestHelper.signOutAndClose(bobSession)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,14 @@
|
|||
|
||||
package org.matrix.android.sdk.api
|
||||
|
||||
/**
|
||||
* This interface exists to let the implementation provide localized room display name fallback.
|
||||
* The methods can be called when the room has no name, i.e. its `m.room.name` state event does not exist or
|
||||
* the name in it is an empty String.
|
||||
* It allows the SDK to store the room name fallback into the local storage and so let the client do
|
||||
* queries on the room name.
|
||||
* *Limitation*: if the locale of the device changes, the methods will not be called again.
|
||||
*/
|
||||
interface RoomDisplayNameFallbackProvider {
|
||||
fun getNameForRoomInvite(): String
|
||||
fun getNameForEmptyRoom(isDirect: Boolean, leftMemberNames: List<String>): String
|
||||
|
|
|
@ -20,20 +20,52 @@ package org.matrix.android.sdk.api.query
|
|||
* Basic query language. All these cases are mutually exclusive.
|
||||
*/
|
||||
sealed interface QueryStringValue {
|
||||
/**
|
||||
* No condition, i.e. there will be no test on the tested field.
|
||||
*/
|
||||
object NoCondition : QueryStringValue
|
||||
|
||||
/**
|
||||
* The tested field has to be null.
|
||||
*/
|
||||
object IsNull : QueryStringValue
|
||||
|
||||
/**
|
||||
* The tested field has to be not null.
|
||||
*/
|
||||
object IsNotNull : QueryStringValue
|
||||
|
||||
/**
|
||||
* The tested field has to be empty.
|
||||
*/
|
||||
object IsEmpty : QueryStringValue
|
||||
|
||||
/**
|
||||
* The tested field has to not empty.
|
||||
*/
|
||||
object IsNotEmpty : QueryStringValue
|
||||
|
||||
/**
|
||||
* Interface to check String content.
|
||||
*/
|
||||
sealed interface ContentQueryStringValue : QueryStringValue {
|
||||
val string: String
|
||||
val case: Case
|
||||
}
|
||||
|
||||
object NoCondition : QueryStringValue
|
||||
object IsNull : QueryStringValue
|
||||
object IsNotNull : QueryStringValue
|
||||
object IsEmpty : QueryStringValue
|
||||
object IsNotEmpty : QueryStringValue
|
||||
|
||||
/**
|
||||
* The tested field must match the [string].
|
||||
*/
|
||||
data class Equals(override val string: String, override val case: Case = Case.SENSITIVE) : ContentQueryStringValue
|
||||
|
||||
/**
|
||||
* The tested field must contain the [string].
|
||||
*/
|
||||
data class Contains(override val string: String, override val case: Case = Case.SENSITIVE) : ContentQueryStringValue
|
||||
|
||||
/**
|
||||
* Case enum for [ContentQueryStringValue].
|
||||
*/
|
||||
enum class Case {
|
||||
/**
|
||||
* Match query sensitive to case.
|
||||
|
|
|
@ -16,9 +16,23 @@
|
|||
|
||||
package org.matrix.android.sdk.api.query
|
||||
|
||||
/**
|
||||
* To filter by Room category.
|
||||
* @see [org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams]
|
||||
*/
|
||||
enum class RoomCategoryFilter {
|
||||
/**
|
||||
* Get only the DM, i.e. the rooms referenced in `m.direct` account data.
|
||||
*/
|
||||
ONLY_DM,
|
||||
|
||||
/**
|
||||
* Get only the Room, not the DM, i.e. the rooms not referenced in `m.direct` account data.
|
||||
*/
|
||||
ONLY_ROOMS,
|
||||
|
||||
/**
|
||||
* Get the room with non-0 notifications.
|
||||
*/
|
||||
ONLY_WITH_NOTIFICATIONS,
|
||||
ALL
|
||||
}
|
||||
|
|
|
@ -16,8 +16,22 @@
|
|||
|
||||
package org.matrix.android.sdk.api.query
|
||||
|
||||
/**
|
||||
* Filter room by their tag.
|
||||
* @see [org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams]
|
||||
* @see [org.matrix.android.sdk.api.session.room.model.tag.RoomTag]
|
||||
*/
|
||||
data class RoomTagQueryFilter(
|
||||
/**
|
||||
* Set to true to get the rooms which have the tag "m.favourite".
|
||||
*/
|
||||
val isFavorite: Boolean?,
|
||||
/**
|
||||
* Set to true to get the rooms which have the tag "m.lowpriority".
|
||||
*/
|
||||
val isLowPriority: Boolean?,
|
||||
val isServerNotice: Boolean?
|
||||
/**
|
||||
* Set to true to get the rooms which have the tag "m.server_notice".
|
||||
*/
|
||||
val isServerNotice: Boolean?,
|
||||
)
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright (c) 2021 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.api.query
|
||||
|
||||
/**
|
||||
* Filter to be used to do room queries regarding the space hierarchy.
|
||||
* @see [org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams]
|
||||
*/
|
||||
sealed interface SpaceFilter {
|
||||
/**
|
||||
* Used to get all the rooms that are not in any space.
|
||||
*/
|
||||
object OrphanRooms : SpaceFilter
|
||||
|
||||
/**
|
||||
* Used to get all the rooms that have the provided space in their parent hierarchy.
|
||||
*/
|
||||
data class ActiveSpace(val spaceId: String) : SpaceFilter
|
||||
|
||||
/**
|
||||
* Used to get all the rooms that do not have the provided space in their parent hierarchy.
|
||||
*/
|
||||
data class ExcludeSpace(val spaceId: String) : SpaceFilter
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a [SpaceFilter.ActiveSpace] if the String is not null, or [SpaceFilter.OrphanRooms].
|
||||
*/
|
||||
fun String?.toActiveSpaceOrOrphanRooms(): SpaceFilter = this?.let { SpaceFilter.ActiveSpace(it) } ?: SpaceFilter.OrphanRooms
|
|
@ -97,6 +97,12 @@ interface RoomService {
|
|||
*/
|
||||
fun getRoomSummary(roomIdOrAlias: String): RoomSummary?
|
||||
|
||||
/**
|
||||
* A live [RoomSummary] associated with the room with id [roomId].
|
||||
* You can observe this summary to get dynamic data from this room, even if the room is not joined yet
|
||||
*/
|
||||
fun getRoomSummaryLive(roomId: String): LiveData<Optional<RoomSummary>>
|
||||
|
||||
/**
|
||||
* Get a snapshot list of room summaries.
|
||||
* @return the immutable list of [RoomSummary]
|
||||
|
|
|
@ -16,9 +16,28 @@
|
|||
|
||||
package org.matrix.android.sdk.api.session.room
|
||||
|
||||
/**
|
||||
* Enum to sort room list.
|
||||
*/
|
||||
enum class RoomSortOrder {
|
||||
/**
|
||||
* Sort room list by room ascending name.
|
||||
*/
|
||||
NAME,
|
||||
|
||||
/**
|
||||
* Sort room list by room descending last activity.
|
||||
*/
|
||||
ACTIVITY,
|
||||
|
||||
/**
|
||||
* Sort room list by room priority and last activity: favorite room first, low priority room last,
|
||||
* then descending last activity.
|
||||
*/
|
||||
PRIORITY_AND_ACTIVITY,
|
||||
|
||||
/**
|
||||
* Do not sort room list. Useful if the order does not matter. Order can be indeterminate.
|
||||
*/
|
||||
NONE
|
||||
}
|
||||
|
|
|
@ -16,60 +16,99 @@
|
|||
|
||||
package org.matrix.android.sdk.api.session.room
|
||||
|
||||
import org.matrix.android.sdk.api.query.ActiveSpaceFilter
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.query.RoomCategoryFilter
|
||||
import org.matrix.android.sdk.api.query.RoomTagQueryFilter
|
||||
import org.matrix.android.sdk.api.query.SpaceFilter
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomType
|
||||
import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams
|
||||
|
||||
/**
|
||||
* Create a [RoomSummaryQueryParams] object, calling [init] with a [RoomSummaryQueryParams.Builder].
|
||||
*/
|
||||
fun roomSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): RoomSummaryQueryParams {
|
||||
return RoomSummaryQueryParams.Builder().apply(init).build()
|
||||
}
|
||||
|
||||
fun spaceSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): SpaceSummaryQueryParams {
|
||||
return RoomSummaryQueryParams.Builder()
|
||||
.apply(init)
|
||||
.apply {
|
||||
includeType = listOf(RoomType.SPACE)
|
||||
excludeType = null
|
||||
roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* This class can be used to filter room summaries to use with:
|
||||
* [org.matrix.android.sdk.api.session.room.Room] and [org.matrix.android.sdk.api.session.room.RoomService].
|
||||
* Create a [SpaceSummaryQueryParams] object (which is a [RoomSummaryQueryParams]), calling [init] with a [RoomSummaryQueryParams.Builder].
|
||||
* This is specific for spaces, other filters will be applied after invoking [init]
|
||||
*/
|
||||
fun spaceSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): SpaceSummaryQueryParams {
|
||||
return roomSummaryQueryParams {
|
||||
init()
|
||||
includeType = listOf(RoomType.SPACE)
|
||||
excludeType = null
|
||||
roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class can be used to filter room summaries to use with [RoomService].
|
||||
* It provides a [Builder].
|
||||
* [roomSummaryQueryParams] and [spaceSummaryQueryParams] can also be used to build an instance of this class.
|
||||
*/
|
||||
data class RoomSummaryQueryParams(
|
||||
val roomId: QueryStringValue,
|
||||
/**
|
||||
* Query for the displayName of the room. The display name can be the value of the state event,
|
||||
* or a value returned by [org.matrix.android.sdk.api.RoomDisplayNameFallbackProvider].
|
||||
*/
|
||||
val displayName: QueryStringValue,
|
||||
/**
|
||||
* Query for the canonical alias of the room.
|
||||
*/
|
||||
val canonicalAlias: QueryStringValue,
|
||||
/**
|
||||
* Used to filter room by membership.
|
||||
*/
|
||||
val memberships: List<Membership>,
|
||||
/**
|
||||
* Used to filter room by room category.
|
||||
*/
|
||||
val roomCategoryFilter: RoomCategoryFilter?,
|
||||
/**
|
||||
* Used to filter room by room tag.
|
||||
*/
|
||||
val roomTagQueryFilter: RoomTagQueryFilter?,
|
||||
/**
|
||||
* Used to filter room by room type.
|
||||
* @see [includeType]
|
||||
*/
|
||||
val excludeType: List<String?>?,
|
||||
/**
|
||||
* Used to filter room by room type.
|
||||
* @see [excludeType]
|
||||
*/
|
||||
val includeType: List<String?>?,
|
||||
val activeSpaceFilter: ActiveSpaceFilter?,
|
||||
/**
|
||||
* Used to filter room using the current space.
|
||||
*/
|
||||
val spaceFilter: SpaceFilter?,
|
||||
/**
|
||||
* Used to filter room using the current group.
|
||||
*/
|
||||
val activeGroupId: String? = null
|
||||
) {
|
||||
|
||||
/**
|
||||
* Builder for [RoomSummaryQueryParams].
|
||||
* [roomSummaryQueryParams] and [spaceSummaryQueryParams] can also be used to build an instance of [RoomSummaryQueryParams].
|
||||
*/
|
||||
class Builder {
|
||||
var roomId: QueryStringValue = QueryStringValue.IsNotEmpty
|
||||
var displayName: QueryStringValue = QueryStringValue.IsNotEmpty
|
||||
var displayName: QueryStringValue = QueryStringValue.NoCondition
|
||||
var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition
|
||||
var memberships: List<Membership> = Membership.all()
|
||||
var roomCategoryFilter: RoomCategoryFilter? = RoomCategoryFilter.ALL
|
||||
var roomCategoryFilter: RoomCategoryFilter? = null
|
||||
var roomTagQueryFilter: RoomTagQueryFilter? = null
|
||||
var excludeType: List<String?>? = listOf(RoomType.SPACE)
|
||||
var includeType: List<String?>? = null
|
||||
var activeSpaceFilter: ActiveSpaceFilter = ActiveSpaceFilter.None
|
||||
var spaceFilter: SpaceFilter? = null
|
||||
var activeGroupId: String? = null
|
||||
|
||||
fun build() = RoomSummaryQueryParams(
|
||||
roomId = roomId,
|
||||
displayName = displayName,
|
||||
canonicalAlias = canonicalAlias,
|
||||
memberships = memberships,
|
||||
|
@ -77,7 +116,7 @@ data class RoomSummaryQueryParams(
|
|||
roomTagQueryFilter = roomTagQueryFilter,
|
||||
excludeType = excludeType,
|
||||
includeType = includeType,
|
||||
activeSpaceFilter = activeSpaceFilter,
|
||||
spaceFilter = spaceFilter,
|
||||
activeGroupId = activeGroupId
|
||||
)
|
||||
}
|
||||
|
|
|
@ -28,65 +28,200 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
|||
* It can be retrieved by [org.matrix.android.sdk.api.session.room.Room] and [org.matrix.android.sdk.api.session.room.RoomService]
|
||||
*/
|
||||
data class RoomSummary(
|
||||
/**
|
||||
* The roomId of the room.
|
||||
*/
|
||||
val roomId: String,
|
||||
// Computed display name
|
||||
/**
|
||||
* Computed display name. The value of the state event `m.room.name` if not empty, else can be the value returned
|
||||
* by [org.matrix.android.sdk.api.RoomDisplayNameFallbackProvider].
|
||||
*/
|
||||
val displayName: String = "",
|
||||
/**
|
||||
* The value of the live state event `m.room.name`.
|
||||
*/
|
||||
val name: String = "",
|
||||
/**
|
||||
* The value of the live state event `m.room.topic`.
|
||||
*/
|
||||
val topic: String = "",
|
||||
/**
|
||||
* The value of the live state event `m.room.avatar`.
|
||||
*/
|
||||
val avatarUrl: String = "",
|
||||
/**
|
||||
* The value of the live state event `m.room.canonical_alias`.
|
||||
*/
|
||||
val canonicalAlias: String? = null,
|
||||
/**
|
||||
* The list of all the aliases of this room. Content of the live state event `m.room.aliases`.
|
||||
*/
|
||||
val aliases: List<String> = emptyList(),
|
||||
/**
|
||||
* The value of the live state event `m.room.join_rules`.
|
||||
*/
|
||||
val joinRules: RoomJoinRules? = null,
|
||||
/**
|
||||
* True is this room is referenced in the account data `m.direct`.
|
||||
*/
|
||||
val isDirect: Boolean = false,
|
||||
/**
|
||||
* If [isDirect] is true, this is the id of the first other member of this room.
|
||||
*/
|
||||
val directUserId: String? = null,
|
||||
/**
|
||||
* If [isDirect] is true, this it the presence of the first other member of this room.
|
||||
*/
|
||||
val directUserPresence: UserPresence? = null,
|
||||
/**
|
||||
* Number of members who have joined this room.
|
||||
*/
|
||||
val joinedMembersCount: Int? = 0,
|
||||
/**
|
||||
* Number of members who are invited to this room.
|
||||
*/
|
||||
val invitedMembersCount: Int? = 0,
|
||||
/**
|
||||
* Latest [TimelineEvent] which can be displayed in this room. Can be used in the room list.
|
||||
*/
|
||||
val latestPreviewableEvent: TimelineEvent? = null,
|
||||
/**
|
||||
* List of other member ids of this room.
|
||||
*/
|
||||
val otherMemberIds: List<String> = emptyList(),
|
||||
/**
|
||||
* Number of unread message in this room.
|
||||
*/
|
||||
val notificationCount: Int = 0,
|
||||
/**
|
||||
* Number of unread and highlighted message in this room.
|
||||
*/
|
||||
val highlightCount: Int = 0,
|
||||
/**
|
||||
* True if this room has unread messages.
|
||||
*/
|
||||
val hasUnreadMessages: Boolean = false,
|
||||
/**
|
||||
* List of tags in this room.
|
||||
*/
|
||||
val tags: List<RoomTag> = emptyList(),
|
||||
/**
|
||||
* Current user membership in this room.
|
||||
*/
|
||||
val membership: Membership = Membership.NONE,
|
||||
/**
|
||||
* Versioning state of this room.
|
||||
*/
|
||||
val versioningState: VersioningState = VersioningState.NONE,
|
||||
/**
|
||||
* Value of `m.fully_read` for this room.
|
||||
*/
|
||||
val readMarkerId: String? = null,
|
||||
/**
|
||||
* Message saved as draft for this room.
|
||||
*/
|
||||
val userDrafts: List<UserDraft> = emptyList(),
|
||||
/**
|
||||
* True if this room is encrypted.
|
||||
*/
|
||||
val isEncrypted: Boolean,
|
||||
/**
|
||||
* Timestamp of the `m.room.encryption` state event.
|
||||
*/
|
||||
val encryptionEventTs: Long?,
|
||||
/**
|
||||
* List of users who are currently typing on this room.
|
||||
*/
|
||||
val typingUsers: List<SenderInfo>,
|
||||
/**
|
||||
* UserId of the user who has invited the current user to this room.
|
||||
*/
|
||||
val inviterId: String? = null,
|
||||
/**
|
||||
* Breadcrumb index, util to sort rooms by last seen.
|
||||
*/
|
||||
val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS,
|
||||
/**
|
||||
* The room encryption trust level.
|
||||
* @see [RoomEncryptionTrustLevel]
|
||||
*/
|
||||
val roomEncryptionTrustLevel: RoomEncryptionTrustLevel? = null,
|
||||
/**
|
||||
* True if a message has not been sent in this room.
|
||||
*/
|
||||
val hasFailedSending: Boolean = false,
|
||||
/**
|
||||
* The type of the room. Null for regular room.
|
||||
* @see [RoomType]
|
||||
*/
|
||||
val roomType: String? = null,
|
||||
/**
|
||||
* List of parent spaces.
|
||||
*/
|
||||
val spaceParents: List<SpaceParentInfo>? = null,
|
||||
/**
|
||||
* List of children space.
|
||||
*/
|
||||
val spaceChildren: List<SpaceChildInfo>? = null,
|
||||
/**
|
||||
* List of all the space parents. Will be empty by default, you have to explicitly request it.
|
||||
*/
|
||||
val flattenParents: List<RoomSummary> = emptyList(),
|
||||
/**
|
||||
* List of all the space parent Ids.
|
||||
*/
|
||||
val flattenParentIds: List<String> = emptyList(),
|
||||
val roomEncryptionAlgorithm: RoomEncryptionAlgorithm? = null
|
||||
/**
|
||||
* Information about the encryption algorithm, if this room is encrypted.
|
||||
*/
|
||||
val roomEncryptionAlgorithm: RoomEncryptionAlgorithm? = null,
|
||||
) {
|
||||
|
||||
/**
|
||||
* True if [versioningState] is not [VersioningState.NONE].
|
||||
*/
|
||||
val isVersioned: Boolean
|
||||
get() = versioningState != VersioningState.NONE
|
||||
|
||||
/**
|
||||
* True if [notificationCount] is not `0`.
|
||||
*/
|
||||
val hasNewMessages: Boolean
|
||||
get() = notificationCount != 0
|
||||
|
||||
/**
|
||||
* True if the room has the tag `m.lowpriority`.
|
||||
*/
|
||||
val isLowPriority: Boolean
|
||||
get() = hasTag(RoomTag.ROOM_TAG_LOW_PRIORITY)
|
||||
|
||||
/**
|
||||
* True if the room has the tag `m.favourite`.
|
||||
*/
|
||||
val isFavorite: Boolean
|
||||
get() = hasTag(RoomTag.ROOM_TAG_FAVOURITE)
|
||||
|
||||
/**
|
||||
* True if [joinRules] is [RoomJoinRules.PUBLIC].
|
||||
*/
|
||||
val isPublic: Boolean
|
||||
get() = joinRules == RoomJoinRules.PUBLIC
|
||||
|
||||
/**
|
||||
* Test if the room has the provided [tag].
|
||||
*/
|
||||
fun hasTag(tag: String) = tags.any { it.name == tag }
|
||||
|
||||
/**
|
||||
* True if a 1-1 call can be started, i.e. the room has exactly 2 joined members.
|
||||
*/
|
||||
val canStartCall: Boolean
|
||||
get() = joinedMembersCount == 2
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Constant to indicated that the room is not on the breadcrumbs.
|
||||
* Used by [breadcrumbsIndex].
|
||||
*/
|
||||
const val NOT_IN_BREADCRUMBS = -1
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,8 +16,22 @@
|
|||
|
||||
package org.matrix.android.sdk.api.session.room.model
|
||||
|
||||
/**
|
||||
* Enum for the versioning state of a room.
|
||||
*/
|
||||
enum class VersioningState {
|
||||
/**
|
||||
* The room is not versioned.
|
||||
*/
|
||||
NONE,
|
||||
|
||||
/**
|
||||
* The room has been upgraded, but the new room is not joined yet.
|
||||
*/
|
||||
UPGRADED_ROOM_NOT_JOINED,
|
||||
UPGRADED_ROOM_JOINED
|
||||
|
||||
/**
|
||||
* The room has been upgraded, and the new room has been joined.
|
||||
*/
|
||||
UPGRADED_ROOM_JOINED,
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
|||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
||||
|
@ -42,7 +43,7 @@ import javax.inject.Inject
|
|||
|
||||
private const val SEND_TO_DEVICE_RETRY_COUNT = 3
|
||||
|
||||
private val loggerTag = LoggerTag("CryptoSyncHandler", LoggerTag.CRYPTO)
|
||||
private val loggerTag = LoggerTag("EventDecryptor", LoggerTag.CRYPTO)
|
||||
|
||||
@SessionScope
|
||||
internal class EventDecryptor @Inject constructor(
|
||||
|
@ -110,6 +111,16 @@ internal class EventDecryptor @Inject constructor(
|
|||
if (eventContent == null) {
|
||||
Timber.tag(loggerTag.value).e("decryptEvent : empty event content")
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
|
||||
} else if (event.isRedacted()) {
|
||||
// we shouldn't attempt to decrypt a redacted event because the content is cleared and decryption will fail because of null algorithm
|
||||
return MXEventDecryptionResult(
|
||||
clearEvent = mapOf(
|
||||
"room_id" to event.roomId.orEmpty(),
|
||||
"type" to EventType.MESSAGE,
|
||||
"content" to emptyMap<String, Any>(),
|
||||
"unsigned" to event.unsignedData.toContent()
|
||||
)
|
||||
)
|
||||
} else {
|
||||
val algorithm = eventContent["algorithm"]?.toString()
|
||||
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)
|
||||
|
|
|
@ -96,8 +96,9 @@ internal class MXOlmDevice @Inject constructor(
|
|||
// So, store these message indexes per timeline id.
|
||||
//
|
||||
// The first level keys are timeline ids.
|
||||
// The second level keys are strings of form "<senderKey>|<session_id>|<message_index>"
|
||||
private val inboundGroupSessionMessageIndexes: MutableMap<String, MutableSet<String>> = HashMap()
|
||||
// The second level values is a Map that represents:
|
||||
// "<senderKey>|<session_id>|<roomId>|<message_index>" --> eventId
|
||||
private val inboundGroupSessionMessageIndexes: MutableMap<String, MutableMap<String, String>> = HashMap()
|
||||
|
||||
init {
|
||||
// Retrieve the account from the store
|
||||
|
@ -755,67 +756,72 @@ internal class MXOlmDevice @Inject constructor(
|
|||
* @param body the base64-encoded body of the encrypted message.
|
||||
* @param roomId the room in which the message was received.
|
||||
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
|
||||
* @param eventId the eventId of the message that will be decrypted
|
||||
* @param sessionId the session identifier.
|
||||
* @param senderKey the base64-encoded curve25519 key of the sender.
|
||||
* @return the decrypting result. Nil if the sessionId is unknown.
|
||||
* @return the decrypting result. Null if the sessionId is unknown.
|
||||
*/
|
||||
@Throws(MXCryptoError::class)
|
||||
suspend fun decryptGroupMessage(body: String,
|
||||
roomId: String,
|
||||
timeline: String?,
|
||||
eventId: String,
|
||||
sessionId: String,
|
||||
senderKey: String): OlmDecryptionResult {
|
||||
val sessionHolder = getInboundGroupSession(sessionId, senderKey, roomId)
|
||||
val wrapper = sessionHolder.wrapper
|
||||
val inboundGroupSession = wrapper.olmInboundGroupSession
|
||||
?: throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, "Session is null")
|
||||
// Check that the room id matches the original one for the session. This stops
|
||||
// the HS pretending a message was targeting a different room.
|
||||
if (roomId == wrapper.roomId) {
|
||||
val decryptResult = try {
|
||||
sessionHolder.mutex.withLock {
|
||||
inboundGroupSession.decryptMessage(body)
|
||||
}
|
||||
} catch (e: OlmException) {
|
||||
Timber.tag(loggerTag.value).e(e, "## decryptGroupMessage () : decryptMessage failed")
|
||||
throw MXCryptoError.OlmError(e)
|
||||
}
|
||||
|
||||
if (timeline?.isNotBlank() == true) {
|
||||
val timelineSet = inboundGroupSessionMessageIndexes.getOrPut(timeline) { mutableSetOf() }
|
||||
|
||||
val messageIndexKey = senderKey + "|" + sessionId + "|" + decryptResult.mIndex
|
||||
|
||||
if (timelineSet.contains(messageIndexKey)) {
|
||||
val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex)
|
||||
Timber.tag(loggerTag.value).e("## decryptGroupMessage() timelineId=$timeline: $reason")
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason)
|
||||
}
|
||||
|
||||
timelineSet.add(messageIndexKey)
|
||||
}
|
||||
|
||||
inboundGroupSessionStore.storeInBoundGroupSession(sessionHolder, sessionId, senderKey)
|
||||
val payload = try {
|
||||
val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
|
||||
val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage)
|
||||
adapter.fromJson(payloadString)
|
||||
} catch (e: Exception) {
|
||||
Timber.tag(loggerTag.value).e("## decryptGroupMessage() : fails to parse the payload")
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)
|
||||
}
|
||||
|
||||
return OlmDecryptionResult(
|
||||
payload,
|
||||
wrapper.keysClaimed,
|
||||
senderKey,
|
||||
wrapper.forwardingCurve25519KeyChain
|
||||
)
|
||||
} else {
|
||||
if (roomId != wrapper.roomId) {
|
||||
// Check that the room id matches the original one for the session. This stops
|
||||
// the HS pretending a message was targeting a different room.
|
||||
val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, wrapper.roomId)
|
||||
Timber.tag(loggerTag.value).e("## decryptGroupMessage() : $reason")
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, reason)
|
||||
}
|
||||
val decryptResult = try {
|
||||
sessionHolder.mutex.withLock {
|
||||
inboundGroupSession.decryptMessage(body)
|
||||
}
|
||||
} catch (e: OlmException) {
|
||||
Timber.tag(loggerTag.value).e(e, "## decryptGroupMessage () : decryptMessage failed")
|
||||
throw MXCryptoError.OlmError(e)
|
||||
}
|
||||
|
||||
val messageIndexKey = senderKey + "|" + sessionId + "|" + roomId + "|" + decryptResult.mIndex
|
||||
Timber.tag(loggerTag.value).v("##########################################################")
|
||||
Timber.tag(loggerTag.value).v("## decryptGroupMessage() timeline: $timeline")
|
||||
Timber.tag(loggerTag.value).v("## decryptGroupMessage() senderKey: $senderKey")
|
||||
Timber.tag(loggerTag.value).v("## decryptGroupMessage() sessionId: $sessionId")
|
||||
Timber.tag(loggerTag.value).v("## decryptGroupMessage() roomId: $roomId")
|
||||
Timber.tag(loggerTag.value).v("## decryptGroupMessage() eventId: $eventId")
|
||||
Timber.tag(loggerTag.value).v("## decryptGroupMessage() mIndex: ${decryptResult.mIndex}")
|
||||
|
||||
if (timeline?.isNotBlank() == true) {
|
||||
val replayAttackMap = inboundGroupSessionMessageIndexes.getOrPut(timeline) { mutableMapOf() }
|
||||
if (replayAttackMap.contains(messageIndexKey) && replayAttackMap[messageIndexKey] != eventId) {
|
||||
val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex)
|
||||
Timber.tag(loggerTag.value).e("## decryptGroupMessage() timelineId=$timeline: $reason")
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason)
|
||||
}
|
||||
replayAttackMap[messageIndexKey] = eventId
|
||||
}
|
||||
inboundGroupSessionStore.storeInBoundGroupSession(sessionHolder, sessionId, senderKey)
|
||||
val payload = try {
|
||||
val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
|
||||
val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage)
|
||||
adapter.fromJson(payloadString)
|
||||
} catch (e: Exception) {
|
||||
Timber.tag(loggerTag.value).e("## decryptGroupMessage() : fails to parse the payload")
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)
|
||||
}
|
||||
|
||||
return OlmDecryptionResult(
|
||||
payload,
|
||||
wrapper.keysClaimed,
|
||||
senderKey,
|
||||
wrapper.forwardingCurve25519KeyChain
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -78,6 +78,7 @@ internal class MXMegolmDecryption(
|
|||
encryptedEventContent.ciphertext,
|
||||
event.roomId,
|
||||
timeline,
|
||||
eventId = event.eventId.orEmpty(),
|
||||
encryptedEventContent.sessionId,
|
||||
encryptedEventContent.senderKey
|
||||
)
|
||||
|
|
|
@ -74,11 +74,13 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findActiveLiveIn
|
|||
realm: Realm,
|
||||
roomId: String,
|
||||
userId: String,
|
||||
ignoredEventId: String
|
||||
): List<LiveLocationShareAggregatedSummaryEntity> {
|
||||
return LiveLocationShareAggregatedSummaryEntity
|
||||
.whereRoomId(realm, roomId = roomId)
|
||||
.equalTo(LiveLocationShareAggregatedSummaryEntityFields.USER_ID, userId)
|
||||
.equalTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true)
|
||||
.notEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, ignoredEventId)
|
||||
.findAll()
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,6 @@ import androidx.lifecycle.LiveData
|
|||
import androidx.lifecycle.Transformations
|
||||
import androidx.paging.PagedList
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.identity.model.SignInvitationResult
|
||||
import org.matrix.android.sdk.api.session.room.Room
|
||||
|
@ -91,18 +90,25 @@ internal class DefaultRoomService @Inject constructor(
|
|||
return roomSummaryDataSource.getRoomSummary(roomIdOrAlias)
|
||||
}
|
||||
|
||||
override fun getRoomSummaryLive(roomId: String): LiveData<Optional<RoomSummary>> {
|
||||
return roomSummaryDataSource.getRoomSummaryLive(roomId)
|
||||
}
|
||||
|
||||
override fun getRoomSummaries(queryParams: RoomSummaryQueryParams,
|
||||
sortOrder: RoomSortOrder): List<RoomSummary> {
|
||||
return roomSummaryDataSource.getRoomSummaries(queryParams, sortOrder)
|
||||
}
|
||||
|
||||
override fun refreshJoinedRoomSummaryPreviews(roomId: String?) {
|
||||
val roomSummaries = getRoomSummaries(roomSummaryQueryParams {
|
||||
if (roomId != null) {
|
||||
this.roomId = QueryStringValue.Equals(roomId)
|
||||
}
|
||||
memberships = listOf(Membership.JOIN)
|
||||
})
|
||||
val roomSummaries = if (roomId == null) {
|
||||
getRoomSummaries(roomSummaryQueryParams {
|
||||
memberships = listOf(Membership.JOIN)
|
||||
})
|
||||
} else {
|
||||
listOfNotNull(
|
||||
getRoomSummary(roomId)?.takeIf { it.membership == Membership.JOIN }
|
||||
)
|
||||
}
|
||||
|
||||
if (roomSummaries.isNotEmpty()) {
|
||||
monarchy.runTransactionSync { realm ->
|
||||
|
@ -127,7 +133,7 @@ internal class DefaultRoomService @Inject constructor(
|
|||
override fun getFilteredPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams,
|
||||
pagedListConfig: PagedList.Config,
|
||||
sortOrder: RoomSortOrder): UpdatableLivePageResult {
|
||||
return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder)
|
||||
return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder, getFlattenedParents = true)
|
||||
}
|
||||
|
||||
override fun getRoomCountLive(queryParams: RoomSummaryQueryParams): LiveData<Int> {
|
||||
|
|
|
@ -147,9 +147,9 @@ internal class LiveLocationAggregationProcessor @Inject constructor(
|
|||
.findActiveLiveInRoomForUser(
|
||||
realm = realm,
|
||||
roomId = roomId,
|
||||
userId = userId
|
||||
userId = userId,
|
||||
ignoredEventId = currentEventId
|
||||
)
|
||||
.filterNot { it.eventId == currentEventId }
|
||||
.forEach { it.isActive = false }
|
||||
}
|
||||
|
||||
|
|
|
@ -43,7 +43,6 @@ internal class RoomChildRelationInfo(
|
|||
data class SpaceChildInfo(
|
||||
val roomId: String,
|
||||
val order: String?,
|
||||
// val autoJoin: Boolean,
|
||||
val viaServers: List<String>
|
||||
)
|
||||
|
||||
|
@ -60,18 +59,13 @@ internal class RoomChildRelationInfo(
|
|||
fun getDirectChildrenDescriptions(): List<SpaceChildInfo> {
|
||||
return CurrentStateEventEntity.whereType(realm, roomId, EventType.STATE_SPACE_CHILD)
|
||||
.findAll()
|
||||
// .also {
|
||||
// Timber.v("## Space: Found ${it.count()} m.space.child state events for $roomId")
|
||||
// }
|
||||
.mapNotNull {
|
||||
ContentMapper.map(it.root?.content).toModel<SpaceChildContent>()?.let { scc ->
|
||||
// Timber.v("## Space child desc state event $scc")
|
||||
// Children where via is not present are ignored.
|
||||
scc.via?.let { via ->
|
||||
SpaceChildInfo(
|
||||
roomId = it.stateKey,
|
||||
order = scc.validOrder(),
|
||||
// autoJoin = scc.autoJoin ?: false,
|
||||
viaServers = via
|
||||
)
|
||||
}
|
||||
|
@ -83,17 +77,13 @@ internal class RoomChildRelationInfo(
|
|||
fun getParentDescriptions(): List<SpaceParentInfo> {
|
||||
return CurrentStateEventEntity.whereType(realm, roomId, EventType.STATE_SPACE_PARENT)
|
||||
.findAll()
|
||||
// .also {
|
||||
// Timber.v("## Space: Found ${it.count()} m.space.parent state events for $roomId")
|
||||
// }
|
||||
.mapNotNull {
|
||||
ContentMapper.map(it.root?.content).toModel<SpaceParentContent>()?.let { scc ->
|
||||
// Timber.v("## Space parent desc state event $scc")
|
||||
ContentMapper.map(it.root?.content).toModel<SpaceParentContent>()?.let { spaceParentContent ->
|
||||
// Parent where via is not present are ignored.
|
||||
scc.via?.let { via ->
|
||||
spaceParentContent.via?.let { via ->
|
||||
SpaceParentInfo(
|
||||
roomId = it.stateKey,
|
||||
canonical = scc.canonical ?: false,
|
||||
canonical = spaceParentContent.canonical ?: false,
|
||||
viaServers = via,
|
||||
stateEventSender = it.root?.sender ?: ""
|
||||
)
|
||||
|
|
|
@ -26,8 +26,9 @@ import com.zhuinden.monarchy.Monarchy
|
|||
import io.realm.Realm
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.kotlin.where
|
||||
import org.matrix.android.sdk.api.query.ActiveSpaceFilter
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.query.RoomCategoryFilter
|
||||
import org.matrix.android.sdk.api.query.SpaceFilter
|
||||
import org.matrix.android.sdk.api.query.isNormalized
|
||||
import org.matrix.android.sdk.api.session.room.ResultBoundaries
|
||||
import org.matrix.android.sdk.api.session.room.RoomSortOrder
|
||||
|
@ -187,13 +188,14 @@ internal class RoomSummaryDataSource @Inject constructor(
|
|||
|
||||
fun getUpdatablePagedRoomSummariesLive(queryParams: RoomSummaryQueryParams,
|
||||
pagedListConfig: PagedList.Config,
|
||||
sortOrder: RoomSortOrder): UpdatableLivePageResult {
|
||||
sortOrder: RoomSortOrder,
|
||||
getFlattenedParents: Boolean = false): UpdatableLivePageResult {
|
||||
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
|
||||
roomSummariesQuery(realm, queryParams).process(sortOrder)
|
||||
}
|
||||
val dataSourceFactory = realmDataSourceFactory.map {
|
||||
roomSummaryMapper.map(it)
|
||||
}
|
||||
}.map { if (getFlattenedParents) it.getWithParents() else it }
|
||||
|
||||
val boundaries = MutableLiveData(ResultBoundaries())
|
||||
|
||||
|
@ -232,6 +234,13 @@ internal class RoomSummaryDataSource @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun RoomSummary.getWithParents(): RoomSummary {
|
||||
val parents = flattenParentIds.mapNotNull { parentId ->
|
||||
getRoomSummary(parentId)
|
||||
}
|
||||
return copy(flattenParents = parents)
|
||||
}
|
||||
|
||||
fun getCountLive(queryParams: RoomSummaryQueryParams): LiveData<Int> {
|
||||
val liveRooms = monarchy.findAllManagedWithChanges {
|
||||
roomSummariesQuery(it, queryParams)
|
||||
|
@ -258,29 +267,13 @@ internal class RoomSummaryDataSource @Inject constructor(
|
|||
private fun roomSummariesQuery(realm: Realm, queryParams: RoomSummaryQueryParams): RealmQuery<RoomSummaryEntity> {
|
||||
val query = with(queryStringValueProcessor) {
|
||||
RoomSummaryEntity.where(realm)
|
||||
.process(RoomSummaryEntityFields.ROOM_ID, queryParams.roomId)
|
||||
.let {
|
||||
if (queryParams.displayName.isNormalized()) {
|
||||
it.process(RoomSummaryEntityFields.NORMALIZED_DISPLAY_NAME, queryParams.displayName)
|
||||
} else {
|
||||
it.process(RoomSummaryEntityFields.DISPLAY_NAME, queryParams.displayName)
|
||||
}
|
||||
}
|
||||
.process(RoomSummaryEntityFields.ROOM_ID, QueryStringValue.IsNotEmpty)
|
||||
.process(queryParams.displayName.toDisplayNameField(), queryParams.displayName)
|
||||
.process(RoomSummaryEntityFields.CANONICAL_ALIAS, queryParams.canonicalAlias)
|
||||
.process(RoomSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships)
|
||||
.equalTo(RoomSummaryEntityFields.IS_HIDDEN_FROM_USER, false)
|
||||
}
|
||||
|
||||
queryParams.roomCategoryFilter?.let {
|
||||
when (it) {
|
||||
RoomCategoryFilter.ONLY_DM -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
|
||||
RoomCategoryFilter.ONLY_ROOMS -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, false)
|
||||
RoomCategoryFilter.ONLY_WITH_NOTIFICATIONS -> query.greaterThan(RoomSummaryEntityFields.NOTIFICATION_COUNT, 0)
|
||||
RoomCategoryFilter.ALL -> {
|
||||
// nop
|
||||
}
|
||||
}
|
||||
}
|
||||
queryParams.roomTagQueryFilter?.let {
|
||||
it.isFavorite?.let { fav ->
|
||||
query.equalTo(RoomSummaryEntityFields.IS_FAVOURITE, fav)
|
||||
|
@ -303,28 +296,24 @@ internal class RoomSummaryDataSource @Inject constructor(
|
|||
RoomCategoryFilter.ONLY_DM -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
|
||||
RoomCategoryFilter.ONLY_ROOMS -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, false)
|
||||
RoomCategoryFilter.ONLY_WITH_NOTIFICATIONS -> query.greaterThan(RoomSummaryEntityFields.NOTIFICATION_COUNT, 0)
|
||||
RoomCategoryFilter.ALL -> Unit // nop
|
||||
null -> Unit
|
||||
}
|
||||
|
||||
// Timber.w("VAL: activeSpaceId : ${queryParams.activeSpaceId}")
|
||||
when (queryParams.activeSpaceFilter) {
|
||||
is ActiveSpaceFilter.ActiveSpace -> {
|
||||
when (queryParams.spaceFilter) {
|
||||
SpaceFilter.OrphanRooms -> {
|
||||
// orphan rooms
|
||||
query.isNull(RoomSummaryEntityFields.FLATTEN_PARENT_IDS)
|
||||
}
|
||||
is SpaceFilter.ActiveSpace -> {
|
||||
// It's annoying but for now realm java does not support querying in primitive list :/
|
||||
// https://github.com/realm/realm-java/issues/5361
|
||||
if (queryParams.activeSpaceFilter.currentSpaceId == null) {
|
||||
// orphan rooms
|
||||
query.isNull(RoomSummaryEntityFields.FLATTEN_PARENT_IDS)
|
||||
} else {
|
||||
query.contains(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, queryParams.activeSpaceFilter.currentSpaceId)
|
||||
}
|
||||
query.contains(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, queryParams.spaceFilter.spaceId)
|
||||
}
|
||||
is ActiveSpaceFilter.ExcludeSpace -> {
|
||||
query.not().contains(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, queryParams.activeSpaceFilter.spaceId)
|
||||
}
|
||||
else -> {
|
||||
// nop
|
||||
is SpaceFilter.ExcludeSpace -> {
|
||||
query.not().contains(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, queryParams.spaceFilter.spaceId)
|
||||
}
|
||||
null -> Unit // nop
|
||||
}
|
||||
|
||||
queryParams.activeGroupId?.let { activeGroupId ->
|
||||
|
@ -333,6 +322,14 @@ internal class RoomSummaryDataSource @Inject constructor(
|
|||
return query
|
||||
}
|
||||
|
||||
private fun QueryStringValue.toDisplayNameField(): String {
|
||||
return if (isNormalized()) {
|
||||
RoomSummaryEntityFields.NORMALIZED_DISPLAY_NAME
|
||||
} else {
|
||||
RoomSummaryEntityFields.DISPLAY_NAME
|
||||
}
|
||||
}
|
||||
|
||||
fun getAllRoomSummaryChildOf(spaceAliasOrId: String, memberShips: List<Membership>): List<RoomSummary> {
|
||||
val space = getSpaceSummary(spaceAliasOrId) ?: return emptyList()
|
||||
val result = ArrayList<RoomSummary>()
|
||||
|
|
|
@ -520,9 +520,10 @@ internal class RoomSyncHandler @Inject constructor(
|
|||
|
||||
private fun decryptIfNeeded(event: Event, roomId: String) {
|
||||
try {
|
||||
val timelineId = generateTimelineId(roomId)
|
||||
// Event from sync does not have roomId, so add it to the event first
|
||||
// note: runBlocking should be used here while we are in realm single thread executor, to avoid thread switching
|
||||
val result = runBlocking { cryptoService.decryptEvent(event.copy(roomId = roomId), "") }
|
||||
val result = runBlocking { cryptoService.decryptEvent(event.copy(roomId = roomId), timelineId) }
|
||||
event.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
|
@ -537,6 +538,10 @@ internal class RoomSyncHandler @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun generateTimelineId(roomId: String): String {
|
||||
return "RoomSyncHandler$roomId"
|
||||
}
|
||||
|
||||
data class EphemeralResult(
|
||||
val typingUserIds: List<String> = emptyList()
|
||||
)
|
||||
|
|
|
@ -16,57 +16,41 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.database.mapper
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkObject
|
||||
import io.mockk.unmockkObject
|
||||
import org.amshove.kluent.internal.assertEquals
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import com.squareup.moshi.Moshi
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.junit.Test
|
||||
import org.matrix.android.sdk.api.session.events.model.Content
|
||||
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.message.LocationInfo
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
|
||||
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
|
||||
|
||||
private const val ANY_USER_ID = "a-user-id"
|
||||
private const val ANY_ACTIVE_STATE = true
|
||||
private const val ANY_TIMEOUT = 123L
|
||||
private val A_LOCATION_INFO = LocationInfo("a-geo-uri")
|
||||
|
||||
class LiveLocationShareAggregatedSummaryMapperTest {
|
||||
|
||||
private val mapper = LiveLocationShareAggregatedSummaryMapper()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
mockkObject(ContentMapper)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unmockkObject(ContentMapper)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given an entity then result should be mapped correctly`() {
|
||||
val userId = "userId"
|
||||
val timeout = 123L
|
||||
val isActive = true
|
||||
val lastKnownLocationContent = "lastKnownLocationContent"
|
||||
val entity = LiveLocationShareAggregatedSummaryEntity(
|
||||
userId = userId,
|
||||
isActive = isActive,
|
||||
endOfLiveTimestampMillis = timeout,
|
||||
lastLocationContent = lastKnownLocationContent
|
||||
)
|
||||
val content = mockk<Content>()
|
||||
every { ContentMapper.map(lastKnownLocationContent) } returns content
|
||||
val entity = anEntity(content = MessageBeaconLocationDataContent(locationInfo = A_LOCATION_INFO))
|
||||
|
||||
val summary = mapper.map(entity)
|
||||
|
||||
// note: unfortunately the implementation relies on an inline method to map the lastLocationDataContent
|
||||
// since inline methods do not produce bytecode, it is not mockable and the verification on this field cannot be done
|
||||
val expectedSummary = LiveLocationShareAggregatedSummary(
|
||||
userId = userId,
|
||||
isActive = isActive,
|
||||
endOfLiveTimestampMillis = timeout,
|
||||
lastLocationDataContent = null
|
||||
summary shouldBeEqualTo LiveLocationShareAggregatedSummary(
|
||||
userId = ANY_USER_ID,
|
||||
isActive = ANY_ACTIVE_STATE,
|
||||
endOfLiveTimestampMillis = ANY_TIMEOUT,
|
||||
lastLocationDataContent = MessageBeaconLocationDataContent(locationInfo = A_LOCATION_INFO)
|
||||
)
|
||||
assertEquals(expectedSummary, summary)
|
||||
}
|
||||
|
||||
private fun anEntity(content: MessageBeaconLocationDataContent) = LiveLocationShareAggregatedSummaryEntity(
|
||||
userId = ANY_USER_ID,
|
||||
isActive = ANY_ACTIVE_STATE,
|
||||
endOfLiveTimestampMillis = ANY_TIMEOUT,
|
||||
lastLocationContent = Moshi.Builder().build().adapter(MessageBeaconLocationDataContent::class.java).toJson(content)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -381,7 +381,7 @@ dependencies {
|
|||
implementation 'com.facebook.stetho:stetho:1.6.0'
|
||||
|
||||
// Phone number https://github.com/google/libphonenumber
|
||||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.48'
|
||||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.49'
|
||||
|
||||
// FlowBinding
|
||||
implementation libs.github.flowBinding
|
||||
|
@ -507,9 +507,14 @@ dependencies {
|
|||
implementation 'commons-codec:commons-codec:1.15'
|
||||
|
||||
// MapTiler
|
||||
implementation 'org.maplibre.gl:android-sdk:9.5.2'
|
||||
implementation 'org.maplibre.gl:android-plugin-annotation-v9:1.0.0'
|
||||
|
||||
fdroidImplementation(libs.maplibre.androidSdk) {
|
||||
exclude group: 'com.google.android.gms', module: 'play-services-location'
|
||||
}
|
||||
fdroidImplementation(libs.maplibre.pluginAnnotation) {
|
||||
exclude group: 'com.google.android.gms', module: 'play-services-location'
|
||||
}
|
||||
gplayImplementation libs.maplibre.androidSdk
|
||||
gplayImplementation libs.maplibre.pluginAnnotation
|
||||
|
||||
// TESTS
|
||||
testImplementation libs.tests.junit
|
||||
|
|
|
@ -60,6 +60,11 @@ class DebugFeaturesStateFactory @Inject constructor(
|
|||
key = DebugFeatureKeys.onboardingCombinedRegister,
|
||||
factory = VectorFeatures::isOnboardingCombinedRegisterEnabled
|
||||
),
|
||||
createBooleanFeature(
|
||||
label = "FTUE Combined login",
|
||||
key = DebugFeatureKeys.onboardingCombinedLogin,
|
||||
factory = VectorFeatures::isOnboardingCombinedLoginEnabled
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -57,6 +57,9 @@ class DebugVectorFeatures(
|
|||
override fun isOnboardingCombinedRegisterEnabled(): Boolean = read(DebugFeatureKeys.onboardingCombinedRegister)
|
||||
?: vectorFeatures.isOnboardingCombinedRegisterEnabled()
|
||||
|
||||
override fun isOnboardingCombinedLoginEnabled(): Boolean = read(DebugFeatureKeys.onboardingCombinedLogin)
|
||||
?: vectorFeatures.isOnboardingCombinedLoginEnabled()
|
||||
|
||||
override fun isScreenSharingEnabled(): Boolean = read(DebugFeatureKeys.screenSharing)
|
||||
?: vectorFeatures.isScreenSharingEnabled()
|
||||
|
||||
|
@ -113,6 +116,7 @@ object DebugFeatureKeys {
|
|||
val onboardingUseCase = booleanPreferencesKey("onboarding-splash-carousel")
|
||||
val onboardingPersonalize = booleanPreferencesKey("onboarding-personalize")
|
||||
val onboardingCombinedRegister = booleanPreferencesKey("onboarding-combined-register")
|
||||
val onboardingCombinedLogin = booleanPreferencesKey("onboarding-combined-login")
|
||||
val liveLocationSharing = booleanPreferencesKey("live-location-sharing")
|
||||
val screenSharing = booleanPreferencesKey("screen-sharing")
|
||||
}
|
||||
|
|
|
@ -306,7 +306,9 @@
|
|||
android:supportsPictureInPicture="true" />
|
||||
|
||||
<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.analytics.ui.consent.AnalyticsOptInActivity" />
|
||||
<activity android:name=".features.home.room.detail.search.SearchActivity" />
|
||||
|
|
|
@ -72,6 +72,8 @@ class AppStateHandler @Inject constructor(
|
|||
|
||||
val selectedRoomGroupingFlow = selectedSpaceDataSource.stream()
|
||||
|
||||
private val spaceBackstack = ArrayDeque<String?>()
|
||||
|
||||
fun getCurrentRoomGroupingMethod(): RoomGroupingMethod? {
|
||||
// XXX we should somehow make it live :/ just a work around
|
||||
// For example just after creating a space and switching to it the
|
||||
|
@ -87,12 +89,16 @@ class AppStateHandler @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun setCurrentSpace(spaceId: String?, session: Session? = null, persistNow: Boolean = false) {
|
||||
fun setCurrentSpace(spaceId: String?, session: Session? = null, persistNow: Boolean = false, isForwardNavigation: Boolean = true) {
|
||||
val currentSpace = (selectedSpaceDataSource.currentValue?.orNull() as? RoomGroupingMethod.BySpace)?.space()
|
||||
val uSession = session ?: activeSessionHolder.getSafeActiveSession() ?: return
|
||||
if (selectedSpaceDataSource.currentValue?.orNull() is RoomGroupingMethod.BySpace &&
|
||||
spaceId == selectedSpaceDataSource.currentValue?.orNull()?.space()?.roomId) return
|
||||
if (currentSpace != null && spaceId == currentSpace.roomId) return
|
||||
val spaceSum = spaceId?.let { uSession.getRoomSummary(spaceId) }
|
||||
|
||||
if (isForwardNavigation) {
|
||||
spaceBackstack.addLast(currentSpace?.roomId)
|
||||
}
|
||||
|
||||
if (persistNow) {
|
||||
uiStateRepository.storeGroupingMethod(true, uSession.sessionId)
|
||||
uiStateRepository.storeSelectedSpace(spaceSum?.roomId, uSession.sessionId)
|
||||
|
@ -151,6 +157,8 @@ class AppStateHandler @Inject constructor(
|
|||
}.launchIn(session.coroutineScope)
|
||||
}
|
||||
|
||||
fun getSpaceBackstack() = spaceBackstack
|
||||
|
||||
fun safeActiveSpaceId(): String? {
|
||||
return (selectedSpaceDataSource.currentValue?.orNull() as? RoomGroupingMethod.BySpace)?.spaceSummary?.roomId
|
||||
}
|
||||
|
|
|
@ -64,7 +64,6 @@ import im.vector.app.features.home.room.list.RoomListFragment
|
|||
import im.vector.app.features.home.room.threads.list.views.ThreadListFragment
|
||||
import im.vector.app.features.location.LocationPreviewFragment
|
||||
import im.vector.app.features.location.LocationSharingFragment
|
||||
import im.vector.app.features.location.live.map.LocationLiveMapViewFragment
|
||||
import im.vector.app.features.login.LoginCaptchaFragment
|
||||
import im.vector.app.features.login.LoginFragment
|
||||
import im.vector.app.features.login.LoginGenericTextInputFormFragment
|
||||
|
@ -102,6 +101,9 @@ import im.vector.app.features.onboarding.ftueauth.FtueAuthAccountCreatedFragment
|
|||
import im.vector.app.features.onboarding.ftueauth.FtueAuthCaptchaFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthChooseDisplayNameFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthChooseProfilePictureFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthCombinedLoginFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthCombinedRegisterFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthCombinedServerSelectionFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthEmailEntryFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthGenericTextInputFormFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthLegacyStyleCaptchaFragment
|
||||
|
@ -522,6 +524,21 @@ interface FragmentModule {
|
|||
@FragmentKey(FtueAuthPersonalizationCompleteFragment::class)
|
||||
fun bindFtueAuthPersonalizationCompleteFragment(fragment: FtueAuthPersonalizationCompleteFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(FtueAuthCombinedLoginFragment::class)
|
||||
fun bindFtueAuthCombinedLoginFragment(fragment: FtueAuthCombinedLoginFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(FtueAuthCombinedRegisterFragment::class)
|
||||
fun bindFtueAuthCombinedRegisterFragment(fragment: FtueAuthCombinedRegisterFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(FtueAuthCombinedServerSelectionFragment::class)
|
||||
fun bindFtueAuthCombinedServerSelectionFragment(fragment: FtueAuthCombinedServerSelectionFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(UserListFragment::class)
|
||||
|
@ -1006,9 +1023,4 @@ interface FragmentModule {
|
|||
@IntoMap
|
||||
@FragmentKey(LocationPreviewFragment::class)
|
||||
fun bindLocationPreviewFragment(fragment: LocationPreviewFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(LocationLiveMapViewFragment::class)
|
||||
fun bindLocationLiveMapViewFragment(fragment: LocationLiveMapViewFragment): Fragment
|
||||
}
|
||||
|
|
26
vector/src/main/java/im/vector/app/core/extensions/String.kt
Normal file
26
vector/src/main/java/im/vector/app/core/extensions/String.kt
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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.core.extensions
|
||||
|
||||
private const val RTL_OVERRIDE_CHAR = '\u202E'
|
||||
private const val LTR_OVERRIDE_CHAR = '\u202D'
|
||||
|
||||
fun String.ensureEndsLeftToRight() = if (containsRtLOverride()) "$this$LTR_OVERRIDE_CHAR" else this
|
||||
|
||||
fun String.containsRtLOverride() = contains(RTL_OVERRIDE_CHAR)
|
||||
|
||||
fun String.filterDirectionOverrides() = filterNot { it == RTL_OVERRIDE_CHAR || it == LTR_OVERRIDE_CHAR }
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
package im.vector.app.core.utils
|
||||
|
||||
import com.vanniktech.emoji.EmojiUtils
|
||||
import com.vanniktech.emoji.isOnlyEmojis
|
||||
|
||||
/**
|
||||
* Test if a string contains emojis.
|
||||
|
@ -28,7 +28,7 @@ import com.vanniktech.emoji.EmojiUtils
|
|||
*/
|
||||
fun containsOnlyEmojis(str: String?): Boolean {
|
||||
// Now rely on vanniktech library
|
||||
return EmojiUtils.isOnlyEmojis(str)
|
||||
return str.isOnlyEmojis()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -26,6 +26,7 @@ interface VectorFeatures {
|
|||
fun isOnboardingUseCaseEnabled(): Boolean
|
||||
fun isOnboardingPersonalizeEnabled(): Boolean
|
||||
fun isOnboardingCombinedRegisterEnabled(): Boolean
|
||||
fun isOnboardingCombinedLoginEnabled(): Boolean
|
||||
fun isScreenSharingEnabled(): Boolean
|
||||
|
||||
enum class OnboardingVariant {
|
||||
|
@ -42,5 +43,6 @@ class DefaultVectorFeatures : VectorFeatures {
|
|||
override fun isOnboardingUseCaseEnabled() = true
|
||||
override fun isOnboardingPersonalizeEnabled() = false
|
||||
override fun isOnboardingCombinedRegisterEnabled() = false
|
||||
override fun isOnboardingCombinedLoginEnabled() = false
|
||||
override fun isScreenSharingEnabled(): Boolean = true
|
||||
}
|
||||
|
|
|
@ -199,43 +199,13 @@ class HomeActivity :
|
|||
when (sharedAction) {
|
||||
is HomeActivitySharedAction.OpenDrawer -> views.drawerLayout.openDrawer(GravityCompat.START)
|
||||
is HomeActivitySharedAction.CloseDrawer -> views.drawerLayout.closeDrawer(GravityCompat.START)
|
||||
is HomeActivitySharedAction.OpenGroup -> {
|
||||
views.drawerLayout.closeDrawer(GravityCompat.START)
|
||||
|
||||
// Temporary
|
||||
// When switching from space to group or group to space, we need to reload the fragment
|
||||
// To be removed when dropping legacy groups
|
||||
if (sharedAction.clearFragment) {
|
||||
replaceFragment(views.homeDetailFragmentContainer, HomeDetailFragment::class.java, allowStateLoss = true)
|
||||
} else {
|
||||
// nop
|
||||
}
|
||||
// we might want to delay that to avoid having the drawer animation lagging
|
||||
// would be probably better to let the drawer do that? in the on closed callback?
|
||||
}
|
||||
is HomeActivitySharedAction.OpenSpacePreview -> {
|
||||
startActivity(SpacePreviewActivity.newIntent(this, sharedAction.spaceId))
|
||||
}
|
||||
is HomeActivitySharedAction.AddSpace -> {
|
||||
createSpaceResultLauncher.launch(SpaceCreationActivity.newIntent(this))
|
||||
}
|
||||
is HomeActivitySharedAction.ShowSpaceSettings -> {
|
||||
// open bottom sheet
|
||||
SpaceSettingsMenuBottomSheet
|
||||
.newInstance(sharedAction.spaceId, object : SpaceSettingsMenuBottomSheet.InteractionListener {
|
||||
override fun onShareSpaceSelected(spaceId: String) {
|
||||
ShareSpaceBottomSheet.show(supportFragmentManager, spaceId)
|
||||
}
|
||||
})
|
||||
.show(supportFragmentManager, "SPACE_SETTINGS")
|
||||
}
|
||||
is HomeActivitySharedAction.OpenSpaceInvite -> {
|
||||
SpaceInviteBottomSheet.newInstance(sharedAction.spaceId)
|
||||
.show(supportFragmentManager, "SPACE_INVITE")
|
||||
}
|
||||
HomeActivitySharedAction.SendSpaceFeedBack -> {
|
||||
bugReporter.openBugReportScreen(this, ReportType.SPACE_BETA_FEEDBACK)
|
||||
}
|
||||
is HomeActivitySharedAction.OpenGroup -> openGroup(sharedAction.shouldClearFragment)
|
||||
is HomeActivitySharedAction.OpenSpacePreview -> startActivity(SpacePreviewActivity.newIntent(this, sharedAction.spaceId))
|
||||
is HomeActivitySharedAction.AddSpace -> createSpaceResultLauncher.launch(SpaceCreationActivity.newIntent(this))
|
||||
is HomeActivitySharedAction.ShowSpaceSettings -> showSpaceSettings(sharedAction.spaceId)
|
||||
is HomeActivitySharedAction.OpenSpaceInvite -> openSpaceInvite(sharedAction.spaceId)
|
||||
HomeActivitySharedAction.SendSpaceFeedBack -> bugReporter.openBugReportScreen(this, ReportType.SPACE_BETA_FEEDBACK)
|
||||
HomeActivitySharedAction.CloseGroup -> closeGroup()
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
|
@ -272,6 +242,37 @@ class HomeActivity :
|
|||
homeActivityViewModel.handle(HomeActivityViewActions.ViewStarted)
|
||||
}
|
||||
|
||||
private fun openGroup(shouldClearFragment: Boolean) {
|
||||
views.drawerLayout.closeDrawer(GravityCompat.START)
|
||||
|
||||
// When switching from space to group or group to space, we need to reload the fragment
|
||||
if (shouldClearFragment) {
|
||||
replaceFragment(views.homeDetailFragmentContainer, HomeDetailFragment::class.java, allowStateLoss = true)
|
||||
} else {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSpaceSettings(spaceId: String) {
|
||||
// open bottom sheet
|
||||
SpaceSettingsMenuBottomSheet
|
||||
.newInstance(spaceId, object : SpaceSettingsMenuBottomSheet.InteractionListener {
|
||||
override fun onShareSpaceSelected(spaceId: String) {
|
||||
ShareSpaceBottomSheet.show(supportFragmentManager, spaceId)
|
||||
}
|
||||
})
|
||||
.show(supportFragmentManager, "SPACE_SETTINGS")
|
||||
}
|
||||
|
||||
private fun openSpaceInvite(spaceId: String) {
|
||||
SpaceInviteBottomSheet.newInstance(spaceId)
|
||||
.show(supportFragmentManager, "SPACE_INVITE")
|
||||
}
|
||||
|
||||
private fun closeGroup() {
|
||||
views.drawerLayout.openDrawer(GravityCompat.START)
|
||||
}
|
||||
|
||||
private fun handleShowAnalyticsOptIn() {
|
||||
navigator.openAnalyticsOptIn(this)
|
||||
}
|
||||
|
|
|
@ -24,7 +24,8 @@ import im.vector.app.core.platform.VectorSharedAction
|
|||
sealed class HomeActivitySharedAction : VectorSharedAction {
|
||||
object OpenDrawer : HomeActivitySharedAction()
|
||||
object CloseDrawer : HomeActivitySharedAction()
|
||||
data class OpenGroup(val clearFragment: Boolean) : HomeActivitySharedAction()
|
||||
data class OpenGroup(val shouldClearFragment: Boolean) : HomeActivitySharedAction()
|
||||
object CloseGroup : HomeActivitySharedAction()
|
||||
object AddSpace : HomeActivitySharedAction()
|
||||
data class OpenSpacePreview(val spaceId: String) : HomeActivitySharedAction()
|
||||
data class OpenSpaceInvite(val spaceId: String) : HomeActivitySharedAction()
|
||||
|
|
|
@ -33,6 +33,7 @@ import im.vector.app.R
|
|||
import im.vector.app.RoomGroupingMethod
|
||||
import im.vector.app.core.extensions.commitTransaction
|
||||
import im.vector.app.core.extensions.toMvRxBundle
|
||||
import im.vector.app.core.platform.OnBackPressed
|
||||
import im.vector.app.core.platform.VectorBaseActivity
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
|
@ -69,7 +70,8 @@ class HomeDetailFragment @Inject constructor(
|
|||
private val appStateHandler: AppStateHandler
|
||||
) : VectorBaseFragment<FragmentHomeDetailBinding>(),
|
||||
KeysBackupBanner.Delegate,
|
||||
CurrentCallsView.Callback {
|
||||
CurrentCallsView.Callback,
|
||||
OnBackPressed {
|
||||
|
||||
private val viewModel: HomeDetailViewModel by fragmentViewModel()
|
||||
private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel()
|
||||
|
@ -130,12 +132,8 @@ class HomeDetailFragment @Inject constructor(
|
|||
|
||||
viewModel.onEach(HomeDetailViewState::roomGroupingMethod) { roomGroupingMethod ->
|
||||
when (roomGroupingMethod) {
|
||||
is RoomGroupingMethod.ByLegacyGroup -> {
|
||||
onGroupChange(roomGroupingMethod.groupSummary)
|
||||
}
|
||||
is RoomGroupingMethod.BySpace -> {
|
||||
onSpaceChange(roomGroupingMethod.spaceSummary)
|
||||
}
|
||||
is RoomGroupingMethod.ByLegacyGroup -> onGroupChange(roomGroupingMethod.groupSummary)
|
||||
is RoomGroupingMethod.BySpace -> onSpaceChange(roomGroupingMethod.spaceSummary)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -157,7 +155,6 @@ class HomeDetailFragment @Inject constructor(
|
|||
|
||||
unknownDeviceDetectorSharedViewModel.onEach { state ->
|
||||
state.unknownSessions.invoke()?.let { unknownDevices ->
|
||||
// Timber.v("## Detector Triggerred in fragment - ${unknownDevices.firstOrNull()}")
|
||||
if (unknownDevices.firstOrNull()?.currentSessionTrust == true) {
|
||||
val uid = "review_login"
|
||||
alertManager.cancelAlert(uid)
|
||||
|
@ -190,6 +187,27 @@ class HomeDetailFragment @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun navigateBack() {
|
||||
try {
|
||||
val lastSpace = appStateHandler.getSpaceBackstack().removeLast()
|
||||
setCurrentSpace(lastSpace)
|
||||
} catch (e: NoSuchElementException) {
|
||||
navigateUpOneSpace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setCurrentSpace(spaceId: String?) {
|
||||
appStateHandler.setCurrentSpace(spaceId, isForwardNavigation = false)
|
||||
sharedActionViewModel.post(HomeActivitySharedAction.CloseGroup)
|
||||
}
|
||||
|
||||
private fun navigateUpOneSpace() {
|
||||
val parentId = getCurrentSpace()?.flattenParentIds?.lastOrNull()
|
||||
setCurrentSpace(parentId)
|
||||
}
|
||||
|
||||
private fun getCurrentSpace() = (appStateHandler.getCurrentRoomGroupingMethod() as? RoomGroupingMethod.BySpace)?.spaceSummary
|
||||
|
||||
private fun handleCallStarted() {
|
||||
dismissLoadingDialog()
|
||||
val fragmentTag = HomeTab.DialPad.toFragmentTag()
|
||||
|
@ -203,20 +221,16 @@ class HomeDetailFragment @Inject constructor(
|
|||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// update notification tab if needed
|
||||
updateTabVisibilitySafely(R.id.bottom_action_notification, vectorPreferences.labAddNotificationTab())
|
||||
callManager.checkForProtocolsSupportIfNeeded()
|
||||
refreshSpaceState()
|
||||
}
|
||||
|
||||
// Current space/group is not live so at least refresh toolbar on resume
|
||||
appStateHandler.getCurrentRoomGroupingMethod()?.let { roomGroupingMethod ->
|
||||
when (roomGroupingMethod) {
|
||||
is RoomGroupingMethod.ByLegacyGroup -> {
|
||||
onGroupChange(roomGroupingMethod.groupSummary)
|
||||
}
|
||||
is RoomGroupingMethod.BySpace -> {
|
||||
onSpaceChange(roomGroupingMethod.spaceSummary)
|
||||
}
|
||||
}
|
||||
private fun refreshSpaceState() {
|
||||
when (val roomGroupingMethod = appStateHandler.getCurrentRoomGroupingMethod()) {
|
||||
is RoomGroupingMethod.ByLegacyGroup -> onGroupChange(roomGroupingMethod.groupSummary)
|
||||
is RoomGroupingMethod.BySpace -> onSpaceChange(roomGroupingMethod.spaceSummary)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -260,12 +274,12 @@ class HomeDetailFragment @Inject constructor(
|
|||
viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer)
|
||||
colorInt = colorProvider.getColorFromAttribute(R.attr.colorPrimary)
|
||||
contentAction = Runnable {
|
||||
(weakCurrentActivity?.get() as? VectorBaseActivity<*>)?.let {
|
||||
(weakCurrentActivity?.get() as? VectorBaseActivity<*>)?.let { activity ->
|
||||
// mark as ignored to avoid showing it again
|
||||
unknownDeviceDetectorSharedViewModel.handle(
|
||||
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(oldUnverified.mapNotNull { it.deviceId })
|
||||
)
|
||||
it.navigator.openSettings(it, EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS)
|
||||
activity.navigator.openSettings(activity, EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS)
|
||||
}
|
||||
}
|
||||
dismissedAction = Runnable {
|
||||
|
@ -324,11 +338,11 @@ class HomeDetailFragment @Inject constructor(
|
|||
withState(viewModel) {
|
||||
when (it.roomGroupingMethod) {
|
||||
is RoomGroupingMethod.ByLegacyGroup -> {
|
||||
// nothing do far
|
||||
// do nothing
|
||||
}
|
||||
is RoomGroupingMethod.BySpace -> {
|
||||
it.roomGroupingMethod.spaceSummary?.let {
|
||||
sharedActionViewModel.post(HomeActivitySharedAction.ShowSpaceSettings(it.roomId))
|
||||
it.roomGroupingMethod.spaceSummary?.let { spaceSummary ->
|
||||
sharedActionViewModel.post(HomeActivitySharedAction.ShowSpaceSettings(spaceSummary.roomId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -348,17 +362,6 @@ class HomeDetailFragment @Inject constructor(
|
|||
viewModel.handle(HomeDetailAction.SwitchTab(tab))
|
||||
true
|
||||
}
|
||||
|
||||
// val menuView = bottomNavigationView.getChildAt(0) as BottomNavigationMenuView
|
||||
|
||||
// bottomNavigationView.getOrCreateBadge()
|
||||
// menuView.forEachIndexed { index, view ->
|
||||
// val itemView = view as BottomNavigationItemView
|
||||
// val badgeLayout = LayoutInflater.from(requireContext()).inflate(R.layout.vector_home_badge_unread_layout, menuView, false)
|
||||
// val unreadCounterBadgeView: UnreadCounterBadgeView = badgeLayout.findViewById(R.id.actionUnreadCounterBadgeView)
|
||||
// itemView.addView(badgeLayout)
|
||||
// unreadCounterBadgeViews.add(index, unreadCounterBadgeView)
|
||||
// }
|
||||
}
|
||||
|
||||
private fun updateUIForTab(tab: HomeTab) {
|
||||
|
@ -436,7 +439,6 @@ class HomeDetailFragment @Inject constructor(
|
|||
}
|
||||
|
||||
override fun invalidate() = withState(viewModel) {
|
||||
// Timber.v(it.toString())
|
||||
views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_people).render(it.notificationCountPeople, it.notificationHighlightPeople)
|
||||
views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_rooms).render(it.notificationCountRooms, it.notificationHighlightRooms)
|
||||
views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_notification).render(it.notificationCountCatchup, it.notificationHighlightCatchup)
|
||||
|
@ -496,4 +498,11 @@ class HomeDetailFragment @Inject constructor(
|
|||
}
|
||||
return this
|
||||
}
|
||||
|
||||
override fun onBackPressed(toolbarButton: Boolean) = if (getCurrentSpace() != null) {
|
||||
navigateBack()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,8 +45,9 @@ import kotlinx.coroutines.flow.flatMapLatest
|
|||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.query.ActiveSpaceFilter
|
||||
import org.matrix.android.sdk.api.query.RoomCategoryFilter
|
||||
import org.matrix.android.sdk.api.query.SpaceFilter
|
||||
import org.matrix.android.sdk.api.query.toActiveSpaceOrOrphanRooms
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
|
||||
import org.matrix.android.sdk.api.session.initsync.SyncStatusService
|
||||
|
@ -241,7 +242,7 @@ class HomeDetailViewModel @AssistedInject constructor(
|
|||
roomSummaryQueryParams {
|
||||
memberships = listOf(Membership.INVITE)
|
||||
roomCategoryFilter = RoomCategoryFilter.ONLY_DM
|
||||
activeSpaceFilter = activeSpaceRoomId?.let { ActiveSpaceFilter.ActiveSpace(it) } ?: ActiveSpaceFilter.None
|
||||
spaceFilter = activeSpaceRoomId?.let { SpaceFilter.ActiveSpace(it) }
|
||||
}
|
||||
).size
|
||||
|
||||
|
@ -249,7 +250,7 @@ class HomeDetailViewModel @AssistedInject constructor(
|
|||
roomSummaryQueryParams {
|
||||
memberships = listOf(Membership.INVITE)
|
||||
roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
|
||||
activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(groupingMethod.spaceSummary?.roomId)
|
||||
spaceFilter = groupingMethod.toActiveSpaceOrOrphanRooms()
|
||||
}
|
||||
).size
|
||||
}
|
||||
|
@ -258,7 +259,7 @@ class HomeDetailViewModel @AssistedInject constructor(
|
|||
roomSummaryQueryParams {
|
||||
memberships = listOf(Membership.JOIN)
|
||||
roomCategoryFilter = RoomCategoryFilter.ONLY_DM
|
||||
activeSpaceFilter = activeSpaceRoomId?.let { ActiveSpaceFilter.ActiveSpace(it) } ?: ActiveSpaceFilter.None
|
||||
spaceFilter = activeSpaceRoomId?.let { SpaceFilter.ActiveSpace(it) }
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -266,7 +267,7 @@ class HomeDetailViewModel @AssistedInject constructor(
|
|||
roomSummaryQueryParams {
|
||||
memberships = listOf(Membership.JOIN)
|
||||
roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
|
||||
activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(groupingMethod.spaceSummary?.roomId)
|
||||
spaceFilter = groupingMethod.toActiveSpaceOrOrphanRooms()
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -287,4 +288,8 @@ class HomeDetailViewModel @AssistedInject constructor(
|
|||
}
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
private fun RoomGroupingMethod.BySpace.toActiveSpaceOrOrphanRooms(): SpaceFilter? {
|
||||
return spaceSummary?.roomId?.toActiveSpaceOrOrphanRooms()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ import kotlinx.coroutines.flow.combine
|
|||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import org.matrix.android.sdk.api.query.ActiveSpaceFilter
|
||||
import org.matrix.android.sdk.api.query.SpaceFilter
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.room.RoomSortOrder
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
|
@ -74,11 +74,10 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(@Assisted initia
|
|||
private val roomService = session.roomService()
|
||||
|
||||
init {
|
||||
|
||||
roomService.getPagedRoomSummariesLive(
|
||||
roomSummaryQueryParams {
|
||||
this.memberships = listOf(Membership.JOIN)
|
||||
this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null)
|
||||
this.spaceFilter = SpaceFilter.OrphanRooms
|
||||
}, sortOrder = RoomSortOrder.NONE
|
||||
).asFlow()
|
||||
.throttleFirst(300)
|
||||
|
@ -86,7 +85,7 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(@Assisted initia
|
|||
val counts = roomService.getNotificationCountForRooms(
|
||||
roomSummaryQueryParams {
|
||||
this.memberships = listOf(Membership.JOIN)
|
||||
this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null)
|
||||
this.spaceFilter = SpaceFilter.OrphanRooms
|
||||
}
|
||||
)
|
||||
val invites = if (autoAcceptInvites.hideInvites) {
|
||||
|
@ -95,7 +94,7 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(@Assisted initia
|
|||
roomService.getRoomSummaries(
|
||||
roomSummaryQueryParams {
|
||||
this.memberships = listOf(Membership.INVITE)
|
||||
this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null)
|
||||
this.spaceFilter = SpaceFilter.OrphanRooms
|
||||
}
|
||||
).size
|
||||
}
|
||||
|
@ -151,9 +150,9 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(@Assisted initia
|
|||
val totalCount = roomService.getNotificationCountForRooms(
|
||||
roomSummaryQueryParams {
|
||||
this.memberships = listOf(Membership.JOIN)
|
||||
this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null).takeIf {
|
||||
this.spaceFilter = SpaceFilter.OrphanRooms.takeIf {
|
||||
!vectorPreferences.prefSpacesShowAllRoomInHome()
|
||||
} ?: ActiveSpaceFilter.None
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -74,6 +74,9 @@ import im.vector.app.core.dialogs.ConfirmationDialogBuilder
|
|||
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
|
||||
import im.vector.app.core.epoxy.LayoutManagerStateRestorer
|
||||
import im.vector.app.core.extensions.cleanup
|
||||
import im.vector.app.core.extensions.containsRtLOverride
|
||||
import im.vector.app.core.extensions.ensureEndsLeftToRight
|
||||
import im.vector.app.core.extensions.filterDirectionOverrides
|
||||
import im.vector.app.core.extensions.hideKeyboard
|
||||
import im.vector.app.core.extensions.registerStartForActivityResult
|
||||
import im.vector.app.core.extensions.setTextOrHide
|
||||
|
@ -714,31 +717,31 @@ class TimelineFragment @Inject constructor(
|
|||
}
|
||||
|
||||
private fun createEmojiPopup(): EmojiPopup {
|
||||
return EmojiPopup
|
||||
.Builder
|
||||
.fromRootView(views.rootConstraintLayout)
|
||||
.setKeyboardAnimationStyle(R.style.emoji_fade_animation_style)
|
||||
.setOnEmojiPopupShownListener {
|
||||
return EmojiPopup(
|
||||
rootView = views.rootConstraintLayout,
|
||||
keyboardAnimationStyle = R.style.emoji_fade_animation_style,
|
||||
onEmojiPopupShownListener = {
|
||||
views.composerLayout.views.composerEmojiButton.apply {
|
||||
contentDescription = getString(R.string.a11y_close_emoji_picker)
|
||||
setImageResource(R.drawable.ic_keyboard)
|
||||
}
|
||||
}
|
||||
.setOnEmojiPopupDismissListenerLifecycleAware {
|
||||
},
|
||||
onEmojiPopupDismissListener = lifecycleAwareDismissAction {
|
||||
views.composerLayout.views.composerEmojiButton.apply {
|
||||
contentDescription = getString(R.string.a11y_open_emoji_picker)
|
||||
setImageResource(R.drawable.ic_insert_emoji)
|
||||
}
|
||||
}
|
||||
.build(views.composerLayout.views.composerEditText)
|
||||
},
|
||||
editText = views.composerLayout.views.composerEditText
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure dismiss actions only trigger when the fragment is in the started state.
|
||||
* EmojiPopup by default dismisses onViewDetachedFromWindow, this can cause race conditions with onDestroyView.
|
||||
*/
|
||||
private fun EmojiPopup.Builder.setOnEmojiPopupDismissListenerLifecycleAware(action: () -> Unit): EmojiPopup.Builder {
|
||||
return setOnEmojiPopupDismissListener {
|
||||
private fun lifecycleAwareDismissAction(action: () -> Unit): () -> Unit {
|
||||
return {
|
||||
if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
|
||||
action()
|
||||
}
|
||||
|
@ -1922,23 +1925,20 @@ class TimelineFragment @Inject constructor(
|
|||
}
|
||||
})
|
||||
if (!isManaged) {
|
||||
if (title.isValidUrl() && url.isValidUrl() && URL(title).host != URL(url).host) {
|
||||
MaterialAlertDialogBuilder(requireActivity(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_NegativeDestructive)
|
||||
.setTitle(R.string.external_link_confirmation_title)
|
||||
.setMessage(
|
||||
getString(R.string.external_link_confirmation_message, title, url)
|
||||
.toSpannable()
|
||||
.colorizeMatchingText(url, colorProvider.getColorFromAttribute(R.attr.vctr_content_tertiary))
|
||||
.colorizeMatchingText(title, colorProvider.getColorFromAttribute(R.attr.vctr_content_tertiary))
|
||||
)
|
||||
.setPositiveButton(R.string._continue) { _, _ ->
|
||||
openUrlInExternalBrowser(requireContext(), url)
|
||||
}
|
||||
.setNegativeButton(R.string.action_cancel, null)
|
||||
.show()
|
||||
} else {
|
||||
// Open in external browser, in a new Tab
|
||||
openUrlInExternalBrowser(requireContext(), url)
|
||||
when {
|
||||
url.containsRtLOverride() -> {
|
||||
displayUrlConfirmationDialog(
|
||||
seenUrl = title.ensureEndsLeftToRight(),
|
||||
actualUrl = url.filterDirectionOverrides(),
|
||||
continueTo = url
|
||||
)
|
||||
}
|
||||
title.isValidUrl() && url.isValidUrl() && URL(title).host != URL(url).host -> {
|
||||
displayUrlConfirmationDialog(title, url)
|
||||
}
|
||||
else -> {
|
||||
openUrlInExternalBrowser(requireContext(), url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1946,6 +1946,22 @@ class TimelineFragment @Inject constructor(
|
|||
return true
|
||||
}
|
||||
|
||||
private fun displayUrlConfirmationDialog(seenUrl: String, actualUrl: String, continueTo: String = actualUrl) {
|
||||
MaterialAlertDialogBuilder(requireActivity(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_NegativeDestructive)
|
||||
.setTitle(R.string.external_link_confirmation_title)
|
||||
.setMessage(
|
||||
getString(R.string.external_link_confirmation_message, seenUrl, actualUrl)
|
||||
.toSpannable()
|
||||
.colorizeMatchingText(actualUrl, colorProvider.getColorFromAttribute(R.attr.vctr_content_tertiary))
|
||||
.colorizeMatchingText(seenUrl, colorProvider.getColorFromAttribute(R.attr.vctr_content_tertiary))
|
||||
)
|
||||
.setPositiveButton(R.string._continue) { _, _ ->
|
||||
openUrlInExternalBrowser(requireContext(), continueTo)
|
||||
}
|
||||
.setNegativeButton(R.string.action_cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onUrlLongClicked(url: String): Boolean {
|
||||
if (url != getString(R.string.edited_suffix) && url.isValidUrl()) {
|
||||
// Copy the url to the clipboard
|
||||
|
|
|
@ -68,7 +68,7 @@ abstract class CollapsableTypedEpoxyController<T> :
|
|||
}
|
||||
|
||||
override fun buildModels() {
|
||||
check(isBuildingModels()) {
|
||||
check(isBuildingModels) {
|
||||
("You cannot call `buildModels` directly. Call `setData` instead to trigger a model " +
|
||||
"refresh with new data.")
|
||||
}
|
||||
|
|
|
@ -211,7 +211,7 @@ class RoomListFragment @Inject constructor(
|
|||
RoomListDisplayMode.NOTIFICATIONS -> views.createChatFabMenu.isVisible = true
|
||||
RoomListDisplayMode.PEOPLE -> views.createChatRoomButton.isVisible = true
|
||||
RoomListDisplayMode.ROOMS -> views.createGroupRoomButton.isVisible = true
|
||||
else -> Unit // No button in this mode
|
||||
RoomListDisplayMode.FILTERED -> Unit // No button in this mode
|
||||
}
|
||||
|
||||
views.createChatRoomButton.debouncedClicks {
|
||||
|
@ -237,7 +237,7 @@ class RoomListFragment @Inject constructor(
|
|||
RoomListDisplayMode.NOTIFICATIONS -> views.createChatFabMenu.hide()
|
||||
RoomListDisplayMode.PEOPLE -> views.createChatRoomButton.hide()
|
||||
RoomListDisplayMode.ROOMS -> views.createGroupRoomButton.hide()
|
||||
else -> Unit
|
||||
RoomListDisplayMode.FILTERED -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -294,7 +294,7 @@ class RoomListFragment @Inject constructor(
|
|||
val contentAdapter =
|
||||
when {
|
||||
section.livePages != null -> {
|
||||
pagedControllerFactory.createRoomSummaryPagedController()
|
||||
pagedControllerFactory.createRoomSummaryPagedController(roomListParams.displayMode)
|
||||
.also { controller ->
|
||||
section.livePages.observe(viewLifecycleOwner) { pl ->
|
||||
controller.submitList(pl)
|
||||
|
@ -316,7 +316,7 @@ class RoomListFragment @Inject constructor(
|
|||
)
|
||||
}
|
||||
}
|
||||
section.isExpanded.observe(viewLifecycleOwner) { _ ->
|
||||
section.isExpanded.observe(viewLifecycleOwner) {
|
||||
refreshCollapseStates()
|
||||
}
|
||||
controller.listener = this
|
||||
|
@ -337,14 +337,14 @@ class RoomListFragment @Inject constructor(
|
|||
checkEmptyState()
|
||||
}
|
||||
observeItemCount(section, sectionAdapter)
|
||||
section.isExpanded.observe(viewLifecycleOwner) { _ ->
|
||||
section.isExpanded.observe(viewLifecycleOwner) {
|
||||
refreshCollapseStates()
|
||||
}
|
||||
controller.listener = this
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
pagedControllerFactory.createRoomSummaryListController()
|
||||
pagedControllerFactory.createRoomSummaryListController(roomListParams.displayMode)
|
||||
.also { controller ->
|
||||
section.liveList?.observe(viewLifecycleOwner) { list ->
|
||||
controller.setData(list)
|
||||
|
@ -366,7 +366,7 @@ class RoomListFragment @Inject constructor(
|
|||
)
|
||||
}
|
||||
}
|
||||
section.isExpanded.observe(viewLifecycleOwner) { _ ->
|
||||
section.isExpanded.observe(viewLifecycleOwner) {
|
||||
refreshCollapseStates()
|
||||
}
|
||||
controller.listener = this
|
||||
|
@ -402,7 +402,7 @@ class RoomListFragment @Inject constructor(
|
|||
RoomListDisplayMode.NOTIFICATIONS -> views.createChatFabMenu.show()
|
||||
RoomListDisplayMode.PEOPLE -> views.createChatRoomButton.show()
|
||||
RoomListDisplayMode.ROOMS -> views.createGroupRoomButton.show()
|
||||
else -> Unit
|
||||
RoomListDisplayMode.FILTERED -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -498,7 +498,7 @@ class RoomListFragment @Inject constructor(
|
|||
isBigImage = true,
|
||||
message = getString(R.string.room_list_rooms_empty_body)
|
||||
)
|
||||
else ->
|
||||
RoomListDisplayMode.FILTERED ->
|
||||
// Always display the content in this mode, because if the footer
|
||||
StateView.State.Content
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ import org.matrix.android.sdk.api.session.Session
|
|||
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
|
||||
import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
|
||||
|
||||
class RoomListSectionBuilderGroup(
|
||||
private val coroutineScope: CoroutineScope,
|
||||
|
@ -96,7 +97,6 @@ class RoomListSectionBuilderGroup(
|
|||
true
|
||||
) {
|
||||
it.memberships = listOf(Membership.INVITE)
|
||||
it.roomCategoryFilter = RoomCategoryFilter.ALL
|
||||
it.activeGroupId = actualGroupId
|
||||
}
|
||||
}
|
||||
|
@ -281,9 +281,6 @@ class RoomListSectionBuilderGroup(
|
|||
}
|
||||
|
||||
private fun withQueryParams(builder: (RoomSummaryQueryParams.Builder) -> Unit, block: (RoomSummaryQueryParams) -> Unit) {
|
||||
RoomSummaryQueryParams.Builder()
|
||||
.apply { builder.invoke(this) }
|
||||
.build()
|
||||
.let { block(it) }
|
||||
block(roomSummaryQueryParams { builder.invoke(this) })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,14 +43,16 @@ import kotlinx.coroutines.flow.map
|
|||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.query.ActiveSpaceFilter
|
||||
import org.matrix.android.sdk.api.query.RoomCategoryFilter
|
||||
import org.matrix.android.sdk.api.query.RoomTagQueryFilter
|
||||
import org.matrix.android.sdk.api.query.SpaceFilter
|
||||
import org.matrix.android.sdk.api.query.toActiveSpaceOrOrphanRooms
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.getRoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
|
||||
import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
|
||||
import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
|
||||
import timber.log.Timber
|
||||
|
||||
|
@ -297,7 +299,6 @@ class RoomListSectionBuilderSpace(
|
|||
countRoomAsNotif = true
|
||||
) {
|
||||
it.memberships = listOf(Membership.INVITE)
|
||||
it.roomCategoryFilter = RoomCategoryFilter.ALL
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -323,9 +324,9 @@ class RoomListSectionBuilderSpace(
|
|||
{
|
||||
it.memberships = Membership.activeMemberships()
|
||||
},
|
||||
{ qpm ->
|
||||
{ queryParams ->
|
||||
val name = stringProvider.getString(R.string.bottom_action_rooms)
|
||||
val updatableFilterLivePageResult = session.roomService().getFilteredPagedRoomSummariesLive(qpm)
|
||||
val updatableFilterLivePageResult = session.roomService().getFilteredPagedRoomSummariesLive(queryParams)
|
||||
onUpdatable(updatableFilterLivePageResult)
|
||||
|
||||
val itemCountFlow = updatableFilterLivePageResult.livePagedList.asFlow()
|
||||
|
@ -370,7 +371,7 @@ class RoomListSectionBuilderSpace(
|
|||
activeSpaceUpdaters.add(object : RoomListViewModel.ActiveSpaceQueryUpdater {
|
||||
override fun updateForSpaceId(roomId: String?) {
|
||||
filteredPagedRoomSummariesLive.queryParams = roomQueryParams.copy(
|
||||
activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(roomId)
|
||||
spaceFilter = roomId?.toActiveSpaceOrOrphanRooms()
|
||||
)
|
||||
liveQueryParams.update { filteredPagedRoomSummariesLive.queryParams }
|
||||
}
|
||||
|
@ -381,11 +382,11 @@ class RoomListSectionBuilderSpace(
|
|||
override fun updateForSpaceId(roomId: String?) {
|
||||
if (roomId != null) {
|
||||
filteredPagedRoomSummariesLive.queryParams = roomQueryParams.copy(
|
||||
activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(roomId)
|
||||
spaceFilter = SpaceFilter.ActiveSpace(roomId)
|
||||
)
|
||||
} else {
|
||||
filteredPagedRoomSummariesLive.queryParams = roomQueryParams.copy(
|
||||
activeSpaceFilter = ActiveSpaceFilter.None
|
||||
spaceFilter = null
|
||||
)
|
||||
}
|
||||
liveQueryParams.update { filteredPagedRoomSummariesLive.queryParams }
|
||||
|
@ -429,29 +430,20 @@ class RoomListSectionBuilderSpace(
|
|||
}
|
||||
|
||||
private fun withQueryParams(builder: (RoomSummaryQueryParams.Builder) -> Unit, block: (RoomSummaryQueryParams) -> Unit) {
|
||||
RoomSummaryQueryParams.Builder()
|
||||
.apply { builder.invoke(this) }
|
||||
.build()
|
||||
.let { block(it) }
|
||||
block(roomSummaryQueryParams { builder.invoke(this) })
|
||||
}
|
||||
|
||||
internal fun RoomSummaryQueryParams.process(spaceFilter: RoomListViewModel.SpaceFilterStrategy, currentSpace: String?): RoomSummaryQueryParams {
|
||||
return when (spaceFilter) {
|
||||
RoomListViewModel.SpaceFilterStrategy.ORPHANS_IF_SPACE_NULL -> {
|
||||
copy(
|
||||
activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(currentSpace)
|
||||
spaceFilter = currentSpace?.toActiveSpaceOrOrphanRooms()
|
||||
)
|
||||
}
|
||||
RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL -> {
|
||||
if (currentSpace == null) {
|
||||
copy(
|
||||
activeSpaceFilter = ActiveSpaceFilter.None
|
||||
)
|
||||
} else {
|
||||
copy(
|
||||
activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(currentSpace)
|
||||
)
|
||||
}
|
||||
copy(
|
||||
spaceFilter = currentSpace?.let { SpaceFilter.ActiveSpace(it) }
|
||||
)
|
||||
}
|
||||
RoomListViewModel.SpaceFilterStrategy.NONE -> this
|
||||
}
|
||||
|
|
|
@ -18,9 +18,9 @@ package im.vector.app.features.home.room.list
|
|||
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
|
@ -36,6 +36,7 @@ import im.vector.app.core.ui.views.PresenceStateImageView
|
|||
import im.vector.app.core.ui.views.ShieldImageView
|
||||
import im.vector.app.features.displayname.getBestName
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.RoomListDisplayMode
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
|
||||
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
|
||||
|
@ -45,48 +46,102 @@ import org.matrix.android.sdk.api.util.MatrixItem
|
|||
@EpoxyModelClass(layout = R.layout.item_room)
|
||||
abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute lateinit var typingMessage: String
|
||||
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
|
||||
@EpoxyAttribute lateinit var matrixItem: MatrixItem
|
||||
@EpoxyAttribute
|
||||
lateinit var typingMessage: String
|
||||
|
||||
@EpoxyAttribute lateinit var lastFormattedEvent: EpoxyCharSequence
|
||||
@EpoxyAttribute lateinit var lastEventTime: String
|
||||
@EpoxyAttribute var encryptionTrustLevel: RoomEncryptionTrustLevel? = null
|
||||
@EpoxyAttribute var userPresence: UserPresence? = null
|
||||
@EpoxyAttribute var showPresence: Boolean = false
|
||||
@EpoxyAttribute var izPublic: Boolean = false
|
||||
@EpoxyAttribute var unreadNotificationCount: Int = 0
|
||||
@EpoxyAttribute var hasUnreadMessage: Boolean = false
|
||||
@EpoxyAttribute var hasDraft: Boolean = false
|
||||
@EpoxyAttribute var showHighlighted: Boolean = false
|
||||
@EpoxyAttribute var hasFailedSending: Boolean = false
|
||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var itemLongClickListener: View.OnLongClickListener? = null
|
||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var itemClickListener: ClickListener? = null
|
||||
@EpoxyAttribute var showSelected: Boolean = false
|
||||
@EpoxyAttribute
|
||||
lateinit var avatarRenderer: AvatarRenderer
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var matrixItem: MatrixItem
|
||||
|
||||
@EpoxyAttribute
|
||||
var displayMode: RoomListDisplayMode = RoomListDisplayMode.PEOPLE
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var subtitle: String
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var lastFormattedEvent: EpoxyCharSequence
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var lastEventTime: String
|
||||
|
||||
@EpoxyAttribute
|
||||
var encryptionTrustLevel: RoomEncryptionTrustLevel? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var userPresence: UserPresence? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var showPresence: Boolean = false
|
||||
|
||||
@EpoxyAttribute @JvmField
|
||||
var isPublic: Boolean = false
|
||||
|
||||
@EpoxyAttribute
|
||||
var unreadNotificationCount: Int = 0
|
||||
|
||||
@EpoxyAttribute
|
||||
var hasUnreadMessage: Boolean = false
|
||||
|
||||
@EpoxyAttribute
|
||||
var hasDraft: Boolean = false
|
||||
|
||||
@EpoxyAttribute
|
||||
var showHighlighted: Boolean = false
|
||||
|
||||
@EpoxyAttribute
|
||||
var hasFailedSending: Boolean = false
|
||||
|
||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
||||
var itemLongClickListener: View.OnLongClickListener? = null
|
||||
|
||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
||||
var itemClickListener: ClickListener? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var showSelected: Boolean = false
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
|
||||
renderDisplayMode(holder)
|
||||
holder.rootView.onClick(itemClickListener)
|
||||
holder.rootView.setOnLongClickListener {
|
||||
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
itemLongClickListener?.onLongClick(it) ?: false
|
||||
}
|
||||
holder.titleView.text = matrixItem.getBestName()
|
||||
holder.lastEventTimeView.text = lastEventTime
|
||||
holder.lastEventView.text = lastFormattedEvent.charSequence
|
||||
holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted))
|
||||
holder.unreadIndentIndicator.isVisible = hasUnreadMessage
|
||||
holder.draftView.isVisible = hasDraft
|
||||
avatarRenderer.render(matrixItem, holder.avatarImageView)
|
||||
holder.roomAvatarDecorationImageView.render(encryptionTrustLevel)
|
||||
holder.roomAvatarPublicDecorationImageView.isVisible = izPublic
|
||||
holder.roomAvatarPublicDecorationImageView.isVisible = isPublic
|
||||
holder.roomAvatarFailSendingImageView.isVisible = hasFailedSending
|
||||
renderSelection(holder, showSelected)
|
||||
holder.typingView.setTextOrHide(typingMessage)
|
||||
holder.lastEventView.isInvisible = holder.typingView.isVisible
|
||||
holder.roomAvatarPresenceImageView.render(showPresence, userPresence)
|
||||
}
|
||||
|
||||
private fun renderDisplayMode(holder: Holder) = when (displayMode) {
|
||||
RoomListDisplayMode.ROOMS,
|
||||
RoomListDisplayMode.PEOPLE,
|
||||
RoomListDisplayMode.NOTIFICATIONS -> renderForDefaultDisplayMode(holder)
|
||||
RoomListDisplayMode.FILTERED -> renderForFilteredDisplayMode(holder)
|
||||
}
|
||||
|
||||
private fun renderForDefaultDisplayMode(holder: Holder) {
|
||||
holder.subtitleView.text = lastFormattedEvent.charSequence
|
||||
holder.lastEventTimeView.text = lastEventTime
|
||||
holder.typingView.setTextOrHide(typingMessage)
|
||||
holder.subtitleView.isInvisible = holder.typingView.isVisible
|
||||
}
|
||||
|
||||
private fun renderForFilteredDisplayMode(holder: Holder) {
|
||||
holder.subtitleView.text = subtitle
|
||||
}
|
||||
|
||||
override fun unbind(holder: Holder) {
|
||||
holder.rootView.setOnClickListener(null)
|
||||
holder.rootView.setOnLongClickListener(null)
|
||||
|
@ -110,7 +165,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
|
|||
val titleView by bind<TextView>(R.id.roomNameView)
|
||||
val unreadCounterBadgeView by bind<UnreadCounterBadgeView>(R.id.roomUnreadCounterBadgeView)
|
||||
val unreadIndentIndicator by bind<View>(R.id.roomUnreadIndicator)
|
||||
val lastEventView by bind<TextView>(R.id.roomLastEventView)
|
||||
val subtitleView by bind<TextView>(R.id.subtitleView)
|
||||
val typingView by bind<TextView>(R.id.roomTypingView)
|
||||
val draftView by bind<ImageView>(R.id.roomDraftBadge)
|
||||
val lastEventTimeView by bind<TextView>(R.id.roomLastEventTimeView)
|
||||
|
@ -120,6 +175,6 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
|
|||
val roomAvatarPublicDecorationImageView by bind<ImageView>(R.id.roomAvatarPublicDecorationImageView)
|
||||
val roomAvatarFailSendingImageView by bind<ImageView>(R.id.roomAvatarFailSendingImageView)
|
||||
val roomAvatarPresenceImageView by bind<PresenceStateImageView>(R.id.roomAvatarPresenceImageView)
|
||||
val rootView by bind<ViewGroup>(R.id.itemRoomLayout)
|
||||
val rootView by bind<ConstraintLayout>(R.id.itemRoomLayout)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* Copyright 2019 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.home.room.list
|
||||
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import com.amulyakhare.textdrawable.TextDrawable
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.ClickListener
|
||||
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.app.core.epoxy.onClick
|
||||
import im.vector.app.core.ui.views.PresenceStateImageView
|
||||
import im.vector.app.core.ui.views.ShieldImageView
|
||||
import im.vector.app.features.displayname.getBestName
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.RoomListDisplayMode
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
|
||||
import org.matrix.android.sdk.api.session.presence.model.UserPresence
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_room_centered)
|
||||
abstract class RoomSummaryItemCentered : VectorEpoxyModel<RoomSummaryItemCentered.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var avatarRenderer: AvatarRenderer
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var matrixItem: MatrixItem
|
||||
|
||||
@EpoxyAttribute
|
||||
var displayMode: RoomListDisplayMode = RoomListDisplayMode.PEOPLE
|
||||
|
||||
@EpoxyAttribute
|
||||
var encryptionTrustLevel: RoomEncryptionTrustLevel? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var userPresence: UserPresence? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var showPresence: Boolean = false
|
||||
|
||||
@EpoxyAttribute @JvmField
|
||||
var isPublic: Boolean = false
|
||||
|
||||
@EpoxyAttribute
|
||||
var unreadNotificationCount: Int = 0
|
||||
|
||||
@EpoxyAttribute
|
||||
var hasUnreadMessage: Boolean = false
|
||||
|
||||
@EpoxyAttribute
|
||||
var hasDraft: Boolean = false
|
||||
|
||||
@EpoxyAttribute
|
||||
var hasFailedSending: Boolean = false
|
||||
|
||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
||||
var itemLongClickListener: View.OnLongClickListener? = null
|
||||
|
||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
||||
var itemClickListener: ClickListener? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var showSelected: Boolean = false
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
|
||||
holder.rootView.onClick(itemClickListener)
|
||||
holder.rootView.setOnLongClickListener {
|
||||
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
itemLongClickListener?.onLongClick(it) ?: false
|
||||
}
|
||||
holder.titleView.text = matrixItem.getBestName()
|
||||
avatarRenderer.render(matrixItem, holder.avatarImageView)
|
||||
holder.roomAvatarDecorationImageView.render(encryptionTrustLevel)
|
||||
holder.roomAvatarPublicDecorationImageView.isVisible = isPublic
|
||||
holder.roomAvatarFailSendingImageView.isVisible = hasFailedSending
|
||||
renderSelection(holder, showSelected)
|
||||
holder.roomAvatarPresenceImageView.render(showPresence, userPresence)
|
||||
}
|
||||
|
||||
override fun unbind(holder: Holder) {
|
||||
holder.rootView.setOnClickListener(null)
|
||||
holder.rootView.setOnLongClickListener(null)
|
||||
avatarRenderer.clear(holder.avatarImageView)
|
||||
super.unbind(holder)
|
||||
}
|
||||
|
||||
private fun renderSelection(holder: Holder, isSelected: Boolean) {
|
||||
if (isSelected) {
|
||||
holder.avatarCheckedImageView.visibility = View.VISIBLE
|
||||
val backgroundColor = ThemeUtils.getColor(holder.view.context, R.attr.colorPrimary)
|
||||
val backgroundDrawable = TextDrawable.builder().buildRound("", backgroundColor)
|
||||
holder.avatarImageView.setImageDrawable(backgroundDrawable)
|
||||
} else {
|
||||
holder.avatarCheckedImageView.visibility = View.GONE
|
||||
avatarRenderer.render(matrixItem, holder.avatarImageView)
|
||||
}
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
val titleView by bind<TextView>(R.id.roomNameView)
|
||||
val avatarCheckedImageView by bind<ImageView>(R.id.roomAvatarCheckedImageView)
|
||||
val avatarImageView by bind<ImageView>(R.id.roomAvatarImageView)
|
||||
val roomAvatarDecorationImageView by bind<ShieldImageView>(R.id.roomAvatarDecorationImageView)
|
||||
val roomAvatarPublicDecorationImageView by bind<ImageView>(R.id.roomAvatarPublicDecorationImageView)
|
||||
val roomAvatarFailSendingImageView by bind<ImageView>(R.id.roomAvatarFailSendingImageView)
|
||||
val roomAvatarPresenceImageView by bind<PresenceStateImageView>(R.id.roomAvatarPresenceImageView)
|
||||
val rootView by bind<ConstraintLayout>(R.id.itemRoomLayout)
|
||||
}
|
||||
}
|
|
@ -26,6 +26,7 @@ import im.vector.app.core.epoxy.VectorEpoxyModel
|
|||
import im.vector.app.core.error.ErrorFormatter
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.RoomListDisplayMode
|
||||
import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter
|
||||
import im.vector.app.features.home.room.typing.TypingHelper
|
||||
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
|
||||
|
@ -46,13 +47,16 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
|
|||
fun create(roomSummary: RoomSummary,
|
||||
roomChangeMembershipStates: Map<String, ChangeMembershipState>,
|
||||
selectedRoomIds: Set<String>,
|
||||
displayMode: RoomListDisplayMode,
|
||||
listener: RoomListListener?): VectorEpoxyModel<*> {
|
||||
return when (roomSummary.membership) {
|
||||
Membership.INVITE -> {
|
||||
val changeMembershipState = roomChangeMembershipStates[roomSummary.roomId] ?: ChangeMembershipState.Unknown
|
||||
createInvitationItem(roomSummary, changeMembershipState, listener)
|
||||
}
|
||||
else -> createRoomItem(roomSummary, selectedRoomIds, listener?.let { it::onRoomClicked }, listener?.let { it::onRoomLongClicked })
|
||||
else -> createRoomItem(
|
||||
roomSummary, selectedRoomIds, displayMode, listener?.let { it::onRoomClicked }, listener?.let { it::onRoomLongClicked }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -105,9 +109,11 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
|
|||
fun createRoomItem(
|
||||
roomSummary: RoomSummary,
|
||||
selectedRoomIds: Set<String>,
|
||||
displayMode: RoomListDisplayMode,
|
||||
onClick: ((RoomSummary) -> Unit)?,
|
||||
onLongClick: ((RoomSummary) -> Boolean)?
|
||||
): VectorEpoxyModel<*> {
|
||||
val subtitle = getSearchResultSubtitle(roomSummary)
|
||||
val unreadCount = roomSummary.notificationCount
|
||||
val showHighlighted = roomSummary.highlightCount > 0
|
||||
val showSelected = selectedRoomIds.contains(roomSummary.roomId)
|
||||
|
@ -118,28 +124,84 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
|
|||
latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect, roomSummary.isDirect.not())
|
||||
latestEventTime = dateFormatter.format(latestEvent.root.originServerTs, DateFormatKind.ROOM_LIST)
|
||||
}
|
||||
|
||||
val typingMessage = typingHelper.getTypingMessage(roomSummary.typingUsers)
|
||||
return RoomSummaryItem_()
|
||||
.id(roomSummary.roomId)
|
||||
.avatarRenderer(avatarRenderer)
|
||||
// We do not display shield in the room list anymore
|
||||
// .encryptionTrustLevel(roomSummary.roomEncryptionTrustLevel)
|
||||
.izPublic(roomSummary.isPublic)
|
||||
.showPresence(roomSummary.isDirect)
|
||||
.userPresence(roomSummary.directUserPresence)
|
||||
.matrixItem(roomSummary.toMatrixItem())
|
||||
.lastEventTime(latestEventTime)
|
||||
.typingMessage(typingMessage)
|
||||
.lastFormattedEvent(latestFormattedEvent.toEpoxyCharSequence())
|
||||
.showHighlighted(showHighlighted)
|
||||
.showSelected(showSelected)
|
||||
.hasFailedSending(roomSummary.hasFailedSending)
|
||||
.unreadNotificationCount(unreadCount)
|
||||
.hasUnreadMessage(roomSummary.hasUnreadMessages)
|
||||
.hasDraft(roomSummary.userDrafts.isNotEmpty())
|
||||
.itemLongClickListener { _ ->
|
||||
onLongClick?.invoke(roomSummary) ?: false
|
||||
}
|
||||
.itemClickListener { onClick?.invoke(roomSummary) }
|
||||
|
||||
return if (subtitle.isBlank() && displayMode == RoomListDisplayMode.FILTERED) {
|
||||
createCenteredRoomSummaryItem(roomSummary, displayMode, showSelected, unreadCount, onClick, onLongClick)
|
||||
} else {
|
||||
createRoomSummaryItem(
|
||||
roomSummary, displayMode, subtitle, latestEventTime, typingMessage,
|
||||
latestFormattedEvent, showHighlighted, showSelected, unreadCount, onClick, onLongClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createRoomSummaryItem(
|
||||
roomSummary: RoomSummary,
|
||||
displayMode: RoomListDisplayMode,
|
||||
subtitle: String,
|
||||
latestEventTime: String,
|
||||
typingMessage: String,
|
||||
latestFormattedEvent: CharSequence,
|
||||
showHighlighted: Boolean,
|
||||
showSelected: Boolean,
|
||||
unreadCount: Int,
|
||||
onClick: ((RoomSummary) -> Unit)?,
|
||||
onLongClick: ((RoomSummary) -> Boolean)?
|
||||
) = RoomSummaryItem_()
|
||||
.id(roomSummary.roomId)
|
||||
.avatarRenderer(avatarRenderer)
|
||||
// We do not display shield in the room list anymore
|
||||
// .encryptionTrustLevel(roomSummary.roomEncryptionTrustLevel)
|
||||
.displayMode(displayMode)
|
||||
.subtitle(subtitle)
|
||||
.isPublic(roomSummary.isPublic)
|
||||
.showPresence(roomSummary.isDirect)
|
||||
.userPresence(roomSummary.directUserPresence)
|
||||
.matrixItem(roomSummary.toMatrixItem())
|
||||
.lastEventTime(latestEventTime)
|
||||
.typingMessage(typingMessage)
|
||||
.lastFormattedEvent(latestFormattedEvent.toEpoxyCharSequence())
|
||||
.showHighlighted(showHighlighted)
|
||||
.showSelected(showSelected)
|
||||
.hasFailedSending(roomSummary.hasFailedSending)
|
||||
.unreadNotificationCount(unreadCount)
|
||||
.hasUnreadMessage(roomSummary.hasUnreadMessages)
|
||||
.hasDraft(roomSummary.userDrafts.isNotEmpty())
|
||||
.itemLongClickListener { _ -> onLongClick?.invoke(roomSummary) ?: false }
|
||||
.itemClickListener { onClick?.invoke(roomSummary) }
|
||||
|
||||
private fun createCenteredRoomSummaryItem(
|
||||
roomSummary: RoomSummary,
|
||||
displayMode: RoomListDisplayMode,
|
||||
showSelected: Boolean,
|
||||
unreadCount: Int,
|
||||
onClick: ((RoomSummary) -> Unit)?,
|
||||
onLongClick: ((RoomSummary) -> Boolean)?
|
||||
) = RoomSummaryItemCentered_()
|
||||
.id(roomSummary.roomId)
|
||||
.avatarRenderer(avatarRenderer)
|
||||
// We do not display shield in the room list anymore
|
||||
// .encryptionTrustLevel(roomSummary.roomEncryptionTrustLevel)
|
||||
.displayMode(displayMode)
|
||||
.isPublic(roomSummary.isPublic)
|
||||
.showPresence(roomSummary.isDirect)
|
||||
.userPresence(roomSummary.directUserPresence)
|
||||
.matrixItem(roomSummary.toMatrixItem())
|
||||
.showSelected(showSelected)
|
||||
.hasFailedSending(roomSummary.hasFailedSending)
|
||||
.unreadNotificationCount(unreadCount)
|
||||
.hasUnreadMessage(roomSummary.hasUnreadMessages)
|
||||
.hasDraft(roomSummary.userDrafts.isNotEmpty())
|
||||
.itemLongClickListener { _ -> onLongClick?.invoke(roomSummary) ?: false }
|
||||
.itemClickListener { onClick?.invoke(roomSummary) }
|
||||
|
||||
private fun getSearchResultSubtitle(roomSummary: RoomSummary): String {
|
||||
val userId = roomSummary.directUserId
|
||||
val spaceName = roomSummary.spaceParents?.firstOrNull()?.roomSummary?.name
|
||||
val canonicalAlias = roomSummary.canonicalAlias
|
||||
|
||||
return (userId ?: spaceName ?: canonicalAlias).orEmpty()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,17 +16,19 @@
|
|||
|
||||
package im.vector.app.features.home.room.list
|
||||
|
||||
import im.vector.app.features.home.RoomListDisplayMode
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
|
||||
class RoomSummaryListController(
|
||||
private val roomSummaryItemFactory: RoomSummaryItemFactory
|
||||
private val roomSummaryItemFactory: RoomSummaryItemFactory,
|
||||
private val displayMode: RoomListDisplayMode
|
||||
) : CollapsableTypedEpoxyController<List<RoomSummary>>() {
|
||||
|
||||
var listener: RoomListListener? = null
|
||||
|
||||
override fun buildModels(data: List<RoomSummary>?) {
|
||||
data?.forEach {
|
||||
add(roomSummaryItemFactory.create(it, emptyMap(), emptySet(), listener))
|
||||
add(roomSummaryItemFactory.create(it, emptyMap(), emptySet(), displayMode, listener))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,11 +19,13 @@ package im.vector.app.features.home.room.list
|
|||
import com.airbnb.epoxy.EpoxyModel
|
||||
import com.airbnb.epoxy.paging.PagedListEpoxyController
|
||||
import im.vector.app.core.utils.createUIHandler
|
||||
import im.vector.app.features.home.RoomListDisplayMode
|
||||
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
|
||||
class RoomSummaryPagedController(
|
||||
private val roomSummaryItemFactory: RoomSummaryItemFactory
|
||||
private val roomSummaryItemFactory: RoomSummaryItemFactory,
|
||||
private val displayMode: RoomListDisplayMode
|
||||
) : PagedListEpoxyController<RoomSummary>(
|
||||
// Important it must match the PageList builder notify Looper
|
||||
modelBuildingHandler = createUIHandler()
|
||||
|
@ -57,6 +59,6 @@ class RoomSummaryPagedController(
|
|||
override fun buildItemModel(currentPosition: Int, item: RoomSummary?): EpoxyModel<*> {
|
||||
// for place holder if enabled
|
||||
item ?: return RoomSummaryItemPlaceHolder_().apply { id(currentPosition) }
|
||||
return roomSummaryItemFactory.create(item, roomChangeMembershipStates.orEmpty(), emptySet(), listener)
|
||||
return roomSummaryItemFactory.create(item, roomChangeMembershipStates.orEmpty(), emptySet(), displayMode, listener)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,18 +16,19 @@
|
|||
|
||||
package im.vector.app.features.home.room.list
|
||||
|
||||
import im.vector.app.features.home.RoomListDisplayMode
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomSummaryPagedControllerFactory @Inject constructor(
|
||||
private val roomSummaryItemFactory: RoomSummaryItemFactory
|
||||
) {
|
||||
|
||||
fun createRoomSummaryPagedController(): RoomSummaryPagedController {
|
||||
return RoomSummaryPagedController(roomSummaryItemFactory)
|
||||
fun createRoomSummaryPagedController(displayMode: RoomListDisplayMode): RoomSummaryPagedController {
|
||||
return RoomSummaryPagedController(roomSummaryItemFactory, displayMode)
|
||||
}
|
||||
|
||||
fun createRoomSummaryListController(): RoomSummaryListController {
|
||||
return RoomSummaryListController(roomSummaryItemFactory)
|
||||
fun createRoomSummaryListController(displayMode: RoomListDisplayMode): RoomSummaryListController {
|
||||
return RoomSummaryListController(roomSummaryItemFactory, displayMode)
|
||||
}
|
||||
|
||||
fun createSuggestedRoomListController(): SuggestedRoomListController {
|
||||
|
|
|
@ -35,6 +35,7 @@ import com.mapbox.mapboxsdk.maps.SupportMapFragment
|
|||
import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager
|
||||
import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions
|
||||
import com.mapbox.mapboxsdk.style.layers.Property
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.addChildFragment
|
||||
import im.vector.app.core.extensions.configureWith
|
||||
|
@ -51,10 +52,12 @@ import javax.inject.Inject
|
|||
/**
|
||||
* Screen showing a map with all the current users sharing their live location in a room.
|
||||
*/
|
||||
class LocationLiveMapViewFragment @Inject constructor(
|
||||
private var urlMapProvider: UrlMapProvider,
|
||||
private var bottomSheetController: LiveLocationBottomSheetController,
|
||||
) : VectorBaseFragment<FragmentLocationLiveMapViewBinding>() {
|
||||
|
||||
@AndroidEntryPoint
|
||||
class LocationLiveMapViewFragment @Inject constructor() : VectorBaseFragment<FragmentLocationLiveMapViewBinding>() {
|
||||
|
||||
@Inject lateinit var urlMapProvider: UrlMapProvider
|
||||
@Inject lateinit var bottomSheetController: LiveLocationBottomSheetController
|
||||
|
||||
private val viewModel: LocationLiveMapViewModel by fragmentViewModel()
|
||||
|
||||
|
|
|
@ -159,3 +159,9 @@ class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs:
|
|||
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), resources.displayMetrics).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
fun SocialLoginButtonsView.render(ssoProviders: List<SsoIdentityProvider>?, mode: SocialLoginButtonsView.Mode, listener: (String?) -> Unit) {
|
||||
this.mode = mode
|
||||
this.ssoIdentityProviders = ssoProviders?.sorted()
|
||||
this.listener = SocialLoginButtonsView.InteractionListener { listener(it) }
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ package im.vector.app.features.onboarding
|
|||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.andThen
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.onboarding.OnboardingAction.LoginOrRegister
|
||||
import im.vector.app.features.onboarding.OnboardingAction.AuthenticateAction.LoginDirect
|
||||
import org.matrix.android.sdk.api.MatrixPatterns.getServerName
|
||||
import org.matrix.android.sdk.api.auth.AuthenticationService
|
||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||
|
@ -33,8 +33,8 @@ class DirectLoginUseCase @Inject constructor(
|
|||
private val uriFactory: UriFactory
|
||||
) {
|
||||
|
||||
suspend fun execute(action: LoginOrRegister, homeServerConnectionConfig: HomeServerConnectionConfig?): Result<Session> {
|
||||
return fetchWellKnown(action.username, homeServerConnectionConfig)
|
||||
suspend fun execute(action: LoginDirect, homeServerConnectionConfig: HomeServerConnectionConfig?): Result<Session> {
|
||||
return fetchWellKnown(action.matrixId, homeServerConnectionConfig)
|
||||
.andThen { wellKnown -> createSessionFor(wellKnown, action, homeServerConnectionConfig) }
|
||||
}
|
||||
|
||||
|
@ -42,13 +42,13 @@ class DirectLoginUseCase @Inject constructor(
|
|||
authenticationService.getWellKnownData(matrixId, config)
|
||||
}
|
||||
|
||||
private suspend fun createSessionFor(data: WellknownResult, action: LoginOrRegister, config: HomeServerConnectionConfig?) = when (data) {
|
||||
is WellknownResult.Prompt -> loginDirect(action, data, config)
|
||||
private suspend fun createSessionFor(data: WellknownResult, action: LoginDirect, config: HomeServerConnectionConfig?) = when (data) {
|
||||
is WellknownResult.Prompt -> loginDirect(action, data, config)
|
||||
is WellknownResult.FailPrompt -> handleFailPrompt(data, action, config)
|
||||
else -> onWellKnownError()
|
||||
else -> onWellKnownError()
|
||||
}
|
||||
|
||||
private suspend fun handleFailPrompt(data: WellknownResult.FailPrompt, action: LoginOrRegister, config: HomeServerConnectionConfig?): Result<Session> {
|
||||
private suspend fun handleFailPrompt(data: WellknownResult.FailPrompt, action: LoginDirect, config: HomeServerConnectionConfig?): Result<Session> {
|
||||
// Relax on IS discovery if homeserver is valid
|
||||
val isMissingInformationToLogin = data.homeServerUrl == null || data.wellKnown == null
|
||||
return when {
|
||||
|
@ -57,12 +57,12 @@ class DirectLoginUseCase @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun loginDirect(action: LoginOrRegister, wellKnownPrompt: WellknownResult.Prompt, config: HomeServerConnectionConfig?): Result<Session> {
|
||||
private suspend fun loginDirect(action: LoginDirect, wellKnownPrompt: WellknownResult.Prompt, config: HomeServerConnectionConfig?): Result<Session> {
|
||||
val alteredHomeServerConnectionConfig = config?.updateWith(wellKnownPrompt) ?: fallbackConfig(action, wellKnownPrompt)
|
||||
return runCatching {
|
||||
authenticationService.directAuthentication(
|
||||
alteredHomeServerConnectionConfig,
|
||||
action.username,
|
||||
action.matrixId,
|
||||
action.password,
|
||||
action.initialDeviceName
|
||||
)
|
||||
|
@ -74,8 +74,8 @@ class DirectLoginUseCase @Inject constructor(
|
|||
identityServerUri = wellKnownPrompt.identityServerUrl?.let { uriFactory.parse(it) }
|
||||
)
|
||||
|
||||
private fun fallbackConfig(action: LoginOrRegister, wellKnownPrompt: WellknownResult.Prompt) = HomeServerConnectionConfig(
|
||||
homeServerUri = uriFactory.parse("https://${action.username.getServerName()}"),
|
||||
private fun fallbackConfig(action: LoginDirect, wellKnownPrompt: WellknownResult.Prompt) = HomeServerConnectionConfig(
|
||||
homeServerUri = uriFactory.parse("https://${action.matrixId.getServerName()}"),
|
||||
homeServerUriBase = uriFactory.parse(wellKnownPrompt.homeServerUrl),
|
||||
identityServerUri = wellKnownPrompt.identityServerUrl?.let { uriFactory.parse(it) }
|
||||
)
|
||||
|
|
|
@ -46,9 +46,12 @@ sealed interface OnboardingAction : VectorViewModelAction {
|
|||
data class ResetPassword(val email: String, val newPassword: String) : OnboardingAction
|
||||
object ResetPasswordMailConfirmed : OnboardingAction
|
||||
|
||||
// Login or Register, depending on the signMode
|
||||
data class LoginOrRegister(val username: String, val password: String, val initialDeviceName: String) : OnboardingAction
|
||||
data class Register(val username: String, val password: String, val initialDeviceName: String) : OnboardingAction
|
||||
sealed interface AuthenticateAction : OnboardingAction {
|
||||
data class Register(val username: String, val password: String, val initialDeviceName: String) : AuthenticateAction
|
||||
data class Login(val username: String, val password: String, val initialDeviceName: String) : AuthenticateAction
|
||||
data class LoginDirect(val matrixId: String, val password: String, val initialDeviceName: String) : AuthenticateAction
|
||||
}
|
||||
|
||||
object StopEmailValidationCheck : OnboardingAction
|
||||
|
||||
data class PostRegisterAction(val registerAction: RegisterAction) : OnboardingAction
|
||||
|
|
|
@ -37,6 +37,7 @@ sealed class OnboardingViewEvents : VectorViewEvents {
|
|||
object OpenUseCaseSelection : OnboardingViewEvents()
|
||||
object OpenServerSelection : OnboardingViewEvents()
|
||||
object OpenCombinedRegister : OnboardingViewEvents()
|
||||
object OpenCombinedLogin : OnboardingViewEvents()
|
||||
object EditServerSelection : OnboardingViewEvents()
|
||||
data class OnServerSelectionDone(val serverType: ServerType) : OnboardingViewEvents()
|
||||
object OnLoginFlowRetrieved : OnboardingViewEvents()
|
||||
|
|
|
@ -42,7 +42,9 @@ import im.vector.app.features.login.LoginMode
|
|||
import im.vector.app.features.login.ReAuthHelper
|
||||
import im.vector.app.features.login.ServerType
|
||||
import im.vector.app.features.login.SignMode
|
||||
import im.vector.app.features.onboarding.OnboardingAction.AuthenticateAction
|
||||
import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase.StartAuthenticationResult
|
||||
import im.vector.app.features.onboarding.ftueauth.MatrixOrgRegistrationStagesComparator
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -138,8 +140,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
is OnboardingAction.UpdateSignMode -> handleUpdateSignMode(action)
|
||||
is OnboardingAction.InitWith -> handleInitWith(action)
|
||||
is OnboardingAction.HomeServerChange -> withAction(action) { handleHomeserverChange(action) }
|
||||
is OnboardingAction.LoginOrRegister -> handleLoginOrRegister(action).also { lastAction = action }
|
||||
is OnboardingAction.Register -> handleRegisterWith(action).also { lastAction = action }
|
||||
is AuthenticateAction -> withAction(action) { handleAuthenticateAction(action) }
|
||||
is OnboardingAction.LoginWithToken -> handleLoginWithToken(action)
|
||||
is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action)
|
||||
is OnboardingAction.ResetPassword -> handleResetPassword(action)
|
||||
|
@ -164,6 +165,14 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
block(action)
|
||||
}
|
||||
|
||||
private fun handleAuthenticateAction(action: AuthenticateAction) {
|
||||
when (action) {
|
||||
is AuthenticateAction.Register -> handleRegisterWith(action)
|
||||
is AuthenticateAction.Login -> handleLogin(action)
|
||||
is AuthenticateAction.LoginDirect -> handleDirectLogin(action, homeServerConnectionConfig = null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSplashAction(resetConfig: Boolean, onboardingFlow: OnboardingFlow) {
|
||||
if (resetConfig) {
|
||||
loginConfig = null
|
||||
|
@ -187,16 +196,21 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun continueToPageAfterSplash(onboardingFlow: OnboardingFlow) {
|
||||
val nextOnboardingStep = when (onboardingFlow) {
|
||||
OnboardingFlow.SignUp -> if (vectorFeatures.isOnboardingUseCaseEnabled()) {
|
||||
OnboardingViewEvents.OpenUseCaseSelection
|
||||
} else {
|
||||
OnboardingViewEvents.OpenServerSelection
|
||||
when (onboardingFlow) {
|
||||
OnboardingFlow.SignUp -> {
|
||||
_viewEvents.post(
|
||||
if (vectorFeatures.isOnboardingUseCaseEnabled()) {
|
||||
OnboardingViewEvents.OpenUseCaseSelection
|
||||
} else {
|
||||
OnboardingViewEvents.OpenServerSelection
|
||||
}
|
||||
)
|
||||
}
|
||||
OnboardingFlow.SignIn,
|
||||
OnboardingFlow.SignInSignUp -> OnboardingViewEvents.OpenServerSelection
|
||||
OnboardingFlow.SignIn -> if (vectorFeatures.isOnboardingCombinedLoginEnabled()) {
|
||||
handle(OnboardingAction.HomeServerChange.SelectHomeServer(defaultHomeserverUrl))
|
||||
} else _viewEvents.post(OnboardingViewEvents.OpenServerSelection)
|
||||
OnboardingFlow.SignInSignUp -> _viewEvents.post(OnboardingViewEvents.OpenServerSelection)
|
||||
}
|
||||
_viewEvents.post(nextOnboardingStep)
|
||||
}
|
||||
|
||||
private fun handleUserAcceptCertificate(action: OnboardingAction.UserAcceptCertificate) {
|
||||
|
@ -208,7 +222,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
?.let { it.copy(allowedFingerprints = it.allowedFingerprints + action.fingerprint) }
|
||||
?.let { startAuthenticationFlow(finalLastAction, it) }
|
||||
}
|
||||
is OnboardingAction.LoginOrRegister ->
|
||||
is AuthenticateAction.LoginDirect ->
|
||||
handleDirectLogin(
|
||||
finalLastAction,
|
||||
HomeServerConnectionConfig.Builder()
|
||||
|
@ -293,10 +307,20 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun emitFlowResultViewEvent(flowResult: FlowResult) {
|
||||
_viewEvents.post(OnboardingViewEvents.RegistrationFlowResult(flowResult, isRegistrationStarted))
|
||||
withState { state ->
|
||||
val orderedResult = when {
|
||||
state.hasSelectedMatrixOrg() && vectorFeatures.isOnboardingCombinedRegisterEnabled() -> flowResult.copy(
|
||||
missingStages = flowResult.missingStages.sortedWith(MatrixOrgRegistrationStagesComparator())
|
||||
)
|
||||
else -> flowResult
|
||||
}
|
||||
_viewEvents.post(OnboardingViewEvents.RegistrationFlowResult(orderedResult, isRegistrationStarted))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRegisterWith(action: OnboardingAction.Register) {
|
||||
private fun OnboardingViewState.hasSelectedMatrixOrg() = selectedHomeserver.userFacingUrl == matrixOrgUrl
|
||||
|
||||
private fun handleRegisterWith(action: AuthenticateAction.Register) {
|
||||
reAuthHelper.data = action.password
|
||||
handleRegisterAction(
|
||||
RegisterAction.CreateAccount(
|
||||
|
@ -471,16 +495,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleLoginOrRegister(action: OnboardingAction.LoginOrRegister) = withState { state ->
|
||||
when (state.signMode) {
|
||||
SignMode.Unknown -> error("Developer error, invalid sign mode")
|
||||
SignMode.SignIn -> handleLogin(action)
|
||||
SignMode.SignUp -> handleRegisterWith(OnboardingAction.Register(action.username, action.password, action.initialDeviceName))
|
||||
SignMode.SignInWithMatrixId -> handleDirectLogin(action, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDirectLogin(action: OnboardingAction.LoginOrRegister, homeServerConnectionConfig: HomeServerConnectionConfig?) {
|
||||
private fun handleDirectLogin(action: AuthenticateAction.LoginDirect, homeServerConnectionConfig: HomeServerConnectionConfig?) {
|
||||
setState { copy(isLoading = true) }
|
||||
currentJob = viewModelScope.launch {
|
||||
directLoginUseCase.execute(action, homeServerConnectionConfig).fold(
|
||||
|
@ -493,7 +508,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleLogin(action: OnboardingAction.LoginOrRegister) {
|
||||
private fun handleLogin(action: AuthenticateAction.Login) {
|
||||
val safeLoginWizard = loginWizard
|
||||
|
||||
if (safeLoginWizard == null) {
|
||||
|
@ -637,7 +652,11 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
when (trigger) {
|
||||
is OnboardingAction.HomeServerChange.EditHomeServer -> {
|
||||
when (awaitState().onboardingFlow) {
|
||||
OnboardingFlow.SignUp -> internalRegisterAction(RegisterAction.StartRegistration) { _ ->
|
||||
OnboardingFlow.SignUp -> internalRegisterAction(RegisterAction.StartRegistration) {
|
||||
updateServerSelection(config, serverTypeOverride, authResult)
|
||||
_viewEvents.post(OnboardingViewEvents.OnHomeserverEdited)
|
||||
}
|
||||
OnboardingFlow.SignIn -> {
|
||||
updateServerSelection(config, serverTypeOverride, authResult)
|
||||
_viewEvents.post(OnboardingViewEvents.OnHomeserverEdited)
|
||||
}
|
||||
|
@ -650,7 +669,10 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
when (awaitState().onboardingFlow) {
|
||||
OnboardingFlow.SignIn -> {
|
||||
updateSignMode(SignMode.SignIn)
|
||||
_viewEvents.post(OnboardingViewEvents.OnSignModeSelected(SignMode.SignIn))
|
||||
when (vectorFeatures.isOnboardingCombinedLoginEnabled()) {
|
||||
true -> _viewEvents.post(OnboardingViewEvents.OpenCombinedLogin)
|
||||
false -> _viewEvents.post(OnboardingViewEvents.OnSignModeSelected(SignMode.SignIn))
|
||||
}
|
||||
}
|
||||
OnboardingFlow.SignUp -> {
|
||||
updateSignMode(SignMode.SignUp)
|
||||
|
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* Copyright 2019 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.onboarding.ftueauth
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.autofill.HintConstants
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.content
|
||||
import im.vector.app.core.extensions.editText
|
||||
import im.vector.app.core.extensions.hideKeyboard
|
||||
import im.vector.app.core.extensions.hidePassword
|
||||
import im.vector.app.core.extensions.realignPercentagesToParent
|
||||
import im.vector.app.core.extensions.setOnImeDoneListener
|
||||
import im.vector.app.core.extensions.toReducedUrl
|
||||
import im.vector.app.databinding.FragmentFtueCombinedLoginBinding
|
||||
import im.vector.app.features.login.LoginMode
|
||||
import im.vector.app.features.login.SSORedirectRouterActivity
|
||||
import im.vector.app.features.login.SocialLoginButtonsView
|
||||
import im.vector.app.features.login.render
|
||||
import im.vector.app.features.onboarding.OnboardingAction
|
||||
import im.vector.app.features.onboarding.OnboardingViewEvents
|
||||
import im.vector.app.features.onboarding.OnboardingViewState
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
class FtueAuthCombinedLoginFragment @Inject constructor(
|
||||
private val loginFieldsValidation: LoginFieldsValidation,
|
||||
private val loginErrorParser: LoginErrorParser
|
||||
) : AbstractSSOFtueAuthFragment<FragmentFtueCombinedLoginBinding>() {
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueCombinedLoginBinding {
|
||||
return FragmentFtueCombinedLoginBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setupSubmitButton()
|
||||
views.loginRoot.realignPercentagesToParent()
|
||||
views.editServerButton.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) }
|
||||
views.loginPasswordInput.setOnImeDoneListener { submit() }
|
||||
}
|
||||
|
||||
private fun setupSubmitButton() {
|
||||
views.loginSubmit.setOnClickListener { submit() }
|
||||
observeContentChangesAndResetErrors(views.loginInput, views.loginPasswordInput, views.loginSubmit)
|
||||
.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||
}
|
||||
|
||||
private fun submit() {
|
||||
cleanupUi()
|
||||
loginFieldsValidation.validate(views.loginInput.content(), views.loginPasswordInput.content())
|
||||
.onUsernameOrIdError { views.loginInput.error = it }
|
||||
.onPasswordError { views.loginPasswordInput.error = it }
|
||||
.onValid { usernameOrId, password ->
|
||||
val initialDeviceName = getString(R.string.login_default_session_public_name)
|
||||
viewModel.handle(OnboardingAction.AuthenticateAction.Login(usernameOrId, password, initialDeviceName))
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanupUi() {
|
||||
views.loginSubmit.hideKeyboard()
|
||||
views.loginInput.error = null
|
||||
views.loginPasswordInput.error = null
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
viewModel.handle(OnboardingAction.ResetAuthenticationAttempt)
|
||||
}
|
||||
|
||||
override fun onError(throwable: Throwable) {
|
||||
// Trick to display the error without text.
|
||||
views.loginInput.error = " "
|
||||
loginErrorParser.parse(throwable, views.loginPasswordInput.content())
|
||||
.onUnknown { super.onError(it) }
|
||||
.onUsernameOrIdError { views.loginInput.error = it }
|
||||
.onPasswordError { views.loginPasswordInput.error = it }
|
||||
}
|
||||
|
||||
override fun updateWithState(state: OnboardingViewState) {
|
||||
setupUi(state)
|
||||
setupAutoFill()
|
||||
|
||||
views.selectedServerName.text = state.selectedHomeserver.userFacingUrl.toReducedUrl()
|
||||
views.selectedServerDescription.text = state.selectedHomeserver.description
|
||||
|
||||
if (state.isLoading) {
|
||||
// Ensure password is hidden
|
||||
views.loginPasswordInput.editText().hidePassword()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupUi(state: OnboardingViewState) {
|
||||
when (state.selectedHomeserver.preferredLoginMode) {
|
||||
is LoginMode.SsoAndPassword -> {
|
||||
showUsernamePassword()
|
||||
renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders)
|
||||
}
|
||||
is LoginMode.Sso -> {
|
||||
hideUsernamePassword()
|
||||
renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders)
|
||||
}
|
||||
else -> {
|
||||
showUsernamePassword()
|
||||
hideSsoProviders()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderSsoProviders(deviceId: String?, ssoProviders: List<SsoIdentityProvider>?) {
|
||||
views.ssoGroup.isVisible = ssoProviders?.isNotEmpty() == true
|
||||
views.ssoButtonsHeader.isVisible = views.ssoGroup.isVisible && views.loginEntryGroup.isVisible
|
||||
views.ssoButtons.render(ssoProviders, SocialLoginButtonsView.Mode.MODE_CONTINUE) { id ->
|
||||
viewModel.getSsoUrl(
|
||||
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
|
||||
deviceId = deviceId,
|
||||
providerId = id
|
||||
)?.let { openInCustomTab(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideSsoProviders() {
|
||||
views.ssoGroup.isVisible = false
|
||||
views.ssoButtons.ssoIdentityProviders = null
|
||||
}
|
||||
|
||||
private fun hideUsernamePassword() {
|
||||
views.loginEntryGroup.isVisible = false
|
||||
}
|
||||
|
||||
private fun showUsernamePassword() {
|
||||
views.loginEntryGroup.isVisible = true
|
||||
}
|
||||
|
||||
private fun setupAutoFill() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
views.loginInput.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_USERNAME)
|
||||
views.loginPasswordInput.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,7 +21,6 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import androidx.autofill.HintConstants
|
||||
import androidx.core.text.isDigitsOnly
|
||||
import androidx.core.view.isVisible
|
||||
|
@ -31,22 +30,22 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.content
|
||||
import im.vector.app.core.extensions.editText
|
||||
import im.vector.app.core.extensions.hasContentFlow
|
||||
import im.vector.app.core.extensions.hasSurroundingSpaces
|
||||
import im.vector.app.core.extensions.hideKeyboard
|
||||
import im.vector.app.core.extensions.hidePassword
|
||||
import im.vector.app.core.extensions.realignPercentagesToParent
|
||||
import im.vector.app.core.extensions.setOnImeDoneListener
|
||||
import im.vector.app.core.extensions.toReducedUrl
|
||||
import im.vector.app.databinding.FragmentFtueCombinedRegisterBinding
|
||||
import im.vector.app.features.login.LoginMode
|
||||
import im.vector.app.features.login.SSORedirectRouterActivity
|
||||
import im.vector.app.features.login.SocialLoginButtonsView
|
||||
import im.vector.app.features.login.render
|
||||
import im.vector.app.features.onboarding.OnboardingAction
|
||||
import im.vector.app.features.onboarding.OnboardingAction.AuthenticateAction
|
||||
import im.vector.app.features.onboarding.OnboardingViewEvents
|
||||
import im.vector.app.features.onboarding.OnboardingViewState
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
|
||||
import org.matrix.android.sdk.api.failure.isInvalidPassword
|
||||
import org.matrix.android.sdk.api.failure.isInvalidUsername
|
||||
|
@ -66,36 +65,16 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu
|
|||
super.onViewCreated(view, savedInstanceState)
|
||||
setupSubmitButton()
|
||||
views.createAccountRoot.realignPercentagesToParent()
|
||||
views.editServerButton.debouncedClicks {
|
||||
viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection))
|
||||
}
|
||||
|
||||
views.createAccountPasswordInput.editText().setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
submit()
|
||||
return@setOnEditorActionListener true
|
||||
}
|
||||
return@setOnEditorActionListener false
|
||||
}
|
||||
views.editServerButton.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) }
|
||||
views.createAccountPasswordInput.setOnImeDoneListener { submit() }
|
||||
}
|
||||
|
||||
private fun setupSubmitButton() {
|
||||
views.createAccountSubmit.setOnClickListener { submit() }
|
||||
observeInputFields()
|
||||
.onEach {
|
||||
views.createAccountPasswordInput.error = null
|
||||
views.createAccountInput.error = null
|
||||
views.createAccountSubmit.isEnabled = it
|
||||
}
|
||||
observeContentChangesAndResetErrors(views.createAccountInput, views.createAccountPasswordInput, views.createAccountSubmit)
|
||||
.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||
}
|
||||
|
||||
private fun observeInputFields() = combine(
|
||||
views.createAccountInput.hasContentFlow { it.trim() },
|
||||
views.createAccountPasswordInput.hasContentFlow(),
|
||||
transform = { isLoginNotEmpty, isPasswordNotEmpty -> isLoginNotEmpty && isPasswordNotEmpty }
|
||||
)
|
||||
|
||||
private fun submit() {
|
||||
withState(viewModel) { state ->
|
||||
cleanupUi()
|
||||
|
@ -119,7 +98,7 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu
|
|||
}
|
||||
|
||||
if (error == 0) {
|
||||
viewModel.handle(OnboardingAction.Register(login, password, getString(R.string.login_default_session_public_name)))
|
||||
viewModel.handle(AuthenticateAction.Register(login, password, getString(R.string.login_default_session_public_name)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -185,9 +164,7 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu
|
|||
|
||||
private fun renderSsoProviders(deviceId: String?, ssoProviders: List<SsoIdentityProvider>?) {
|
||||
views.ssoGroup.isVisible = ssoProviders?.isNotEmpty() == true
|
||||
views.ssoButtons.mode = SocialLoginButtonsView.Mode.MODE_CONTINUE
|
||||
views.ssoButtons.ssoIdentityProviders = ssoProviders?.sorted()
|
||||
views.ssoButtons.listener = SocialLoginButtonsView.InteractionListener { id ->
|
||||
views.ssoButtons.render(ssoProviders, SocialLoginButtonsView.Mode.MODE_CONTINUE) { id ->
|
||||
viewModel.getSsoUrl(
|
||||
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
|
||||
deviceId = deviceId,
|
||||
|
|
|
@ -26,6 +26,7 @@ import androidx.autofill.HintConstants
|
|||
import androidx.core.text.isDigitsOnly
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.hideKeyboard
|
||||
|
@ -119,40 +120,43 @@ class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment<
|
|||
}
|
||||
|
||||
private fun submit() {
|
||||
cleanupUi()
|
||||
withState(viewModel) { state ->
|
||||
cleanupUi()
|
||||
|
||||
val login = views.loginField.text.toString()
|
||||
val password = views.passwordField.text.toString()
|
||||
val login = views.loginField.text.toString()
|
||||
val password = views.passwordField.text.toString()
|
||||
|
||||
// This can be called by the IME action, so deal with empty cases
|
||||
var error = 0
|
||||
if (login.isEmpty()) {
|
||||
views.loginFieldTil.error = getString(
|
||||
if (isSignupMode) {
|
||||
R.string.error_empty_field_choose_user_name
|
||||
} else {
|
||||
R.string.error_empty_field_enter_user_name
|
||||
}
|
||||
)
|
||||
error++
|
||||
}
|
||||
if (isSignupMode && isNumericOnlyUserIdForbidden && login.isDigitsOnly()) {
|
||||
views.loginFieldTil.error = getString(R.string.error_forbidden_digits_only_username)
|
||||
error++
|
||||
}
|
||||
if (password.isEmpty()) {
|
||||
views.passwordFieldTil.error = getString(
|
||||
if (isSignupMode) {
|
||||
R.string.error_empty_field_choose_password
|
||||
} else {
|
||||
R.string.error_empty_field_your_password
|
||||
}
|
||||
)
|
||||
error++
|
||||
}
|
||||
// This can be called by the IME action, so deal with empty cases
|
||||
var error = 0
|
||||
if (login.isEmpty()) {
|
||||
views.loginFieldTil.error = getString(
|
||||
if (isSignupMode) {
|
||||
R.string.error_empty_field_choose_user_name
|
||||
} else {
|
||||
R.string.error_empty_field_enter_user_name
|
||||
}
|
||||
)
|
||||
error++
|
||||
}
|
||||
if (isSignupMode && isNumericOnlyUserIdForbidden && login.isDigitsOnly()) {
|
||||
views.loginFieldTil.error = getString(R.string.error_forbidden_digits_only_username)
|
||||
error++
|
||||
}
|
||||
if (password.isEmpty()) {
|
||||
views.passwordFieldTil.error = getString(
|
||||
if (isSignupMode) {
|
||||
R.string.error_empty_field_choose_password
|
||||
} else {
|
||||
R.string.error_empty_field_your_password
|
||||
}
|
||||
)
|
||||
error++
|
||||
}
|
||||
|
||||
if (error == 0) {
|
||||
viewModel.handle(OnboardingAction.LoginOrRegister(login, password, getString(R.string.login_default_session_public_name)))
|
||||
if (error == 0) {
|
||||
val initialDeviceName = getString(R.string.login_default_session_public_name)
|
||||
viewModel.handle(state.signMode.toAuthenticateAction(login, password, initialDeviceName))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -54,7 +54,6 @@ import im.vector.app.features.onboarding.OnboardingViewState
|
|||
import im.vector.app.features.onboarding.ftueauth.terms.FtueAuthLegacyStyleTermsFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.terms.FtueAuthTermsFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.terms.FtueAuthTermsLegacyStyleFragmentArgument
|
||||
import org.matrix.android.sdk.api.auth.registration.FlowResult
|
||||
import org.matrix.android.sdk.api.auth.registration.Stage
|
||||
import org.matrix.android.sdk.api.auth.toLocalizedLoginTerms
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
|
@ -228,24 +227,24 @@ class FtueAuthVariant(
|
|||
option = commonOption
|
||||
)
|
||||
}
|
||||
OnboardingViewEvents.OnHomeserverEdited -> activity.popBackstack()
|
||||
OnboardingViewEvents.OnHomeserverEdited -> activity.popBackstack()
|
||||
OnboardingViewEvents.OpenCombinedLogin -> onStartCombinedLogin()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onStartCombinedLogin() {
|
||||
addRegistrationStageFragmentToBackstack(FtueAuthCombinedLoginFragment::class.java)
|
||||
}
|
||||
|
||||
private fun onRegistrationFlow(viewEvents: OnboardingViewEvents.RegistrationFlowResult) {
|
||||
when {
|
||||
registrationShouldFallback(viewEvents) -> displayFallbackWebDialog()
|
||||
viewEvents.isRegistrationStarted -> handleRegistrationNavigation(viewEvents.flowResult.orderedStages())
|
||||
viewEvents.isRegistrationStarted -> handleRegistrationNavigation(viewEvents.flowResult.missingStages)
|
||||
vectorFeatures.isOnboardingCombinedRegisterEnabled() -> openStartCombinedRegister()
|
||||
else -> openAuthLoginFragmentWithTag(FRAGMENT_REGISTRATION_STAGE_TAG)
|
||||
}
|
||||
}
|
||||
|
||||
private fun FlowResult.orderedStages() = when {
|
||||
vectorFeatures.isOnboardingCombinedRegisterEnabled() -> missingStages.sortedWith(FtueMissingRegistrationStagesComparator())
|
||||
else -> missingStages
|
||||
}
|
||||
|
||||
private fun openStartCombinedRegister() {
|
||||
addRegistrationStageFragmentToBackstack(FtueAuthCombinedRegisterFragment::class.java)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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.onboarding.ftueauth
|
||||
|
||||
import android.widget.Button
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import im.vector.app.core.extensions.hasContentFlow
|
||||
import im.vector.app.features.login.SignMode
|
||||
import im.vector.app.features.onboarding.OnboardingAction
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
fun SignMode.toAuthenticateAction(login: String, password: String, initialDeviceName: String): OnboardingAction.AuthenticateAction {
|
||||
return when (this) {
|
||||
SignMode.Unknown -> error("developer error")
|
||||
SignMode.SignUp -> OnboardingAction.AuthenticateAction.Register(username = login, password, initialDeviceName)
|
||||
SignMode.SignIn -> OnboardingAction.AuthenticateAction.Login(username = login, password, initialDeviceName)
|
||||
SignMode.SignInWithMatrixId -> OnboardingAction.AuthenticateAction.LoginDirect(matrixId = login, password, initialDeviceName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A flow to monitor content changes from both username/id and password fields,
|
||||
* clearing errors and enabling/disabling the submission button on non empty content changes.
|
||||
*/
|
||||
fun observeContentChangesAndResetErrors(username: TextInputLayout, password: TextInputLayout, submit: Button): Flow<*> {
|
||||
return combine(
|
||||
username.hasContentFlow { it.trim() },
|
||||
password.hasContentFlow(),
|
||||
transform = { usernameHasContent, passwordHasContent -> usernameHasContent && passwordHasContent }
|
||||
).onEach {
|
||||
username.error = null
|
||||
password.error = null
|
||||
submit.isEnabled = it
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue