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:
Onuray Sahin 2022-05-30 16:27:11 +03:00
commit 04679ea21d
121 changed files with 2583 additions and 1115 deletions

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

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

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

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

1
changelog.d/5860.feature Normal file
View file

@ -0,0 +1 @@
Adds space or user id as a subtitle under rooms in search

1
changelog.d/6073.feature Normal file
View file

@ -0,0 +1 @@
Adds up navigation in spaces

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

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

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

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

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

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

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

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

3
changelog.d/6143.sdk Normal file
View 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
View file

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

1
changelog.d/6163.feature Normal file
View file

@ -0,0 +1 @@
Security - Asking for user confirmation when tapping URLs which contain unicode directional overrides

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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?) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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?,
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.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)

View file

@ -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
)
}
/**

View file

@ -78,6 +78,7 @@ internal class MXMegolmDecryption(
encryptedEventContent.ciphertext,
event.roomId,
timeline,
eventId = event.eventId.orEmpty(),
encryptedEventContent.sessionId,
encryptedEventContent.senderKey
)

View file

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

View file

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

View file

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

View file

@ -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 ?: ""
)

View file

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

View file

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

View file

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

View file

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

View file

@ -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
),
)
)
}

View file

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

View file

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

View file

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

View file

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

View 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 }

View file

@ -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()
}
/**

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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.")
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -54,7 +54,6 @@ import im.vector.app.features.onboarding.OnboardingViewState
import im.vector.app.features.onboarding.ftueauth.terms.FtueAuthLegacyStyleTermsFragment
import im.vector.app.features.onboarding.ftueauth.terms.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)
}

View file

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