diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml
index a34f4219d9..10c12796c0 100644
--- a/.idea/dictionaries/bmarty.xml
+++ b/.idea/dictionaries/bmarty.xml
@@ -20,6 +20,7 @@
signin
signout
signup
+ threepid
\ No newline at end of file
diff --git a/CHANGES.md b/CHANGES.md
index 3a33a8a940..9f9d49d0a0 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,20 +1,17 @@
-Changes in RiotX 0.12.0 (2019-XX-XX)
+Changes in RiotX 0.13.0 (2020-XX-XX)
===================================================
Features ✨:
-
Improvements 🙌:
- - The initial sync is now handled by a foreground service
- - Render aliases and canonical alias change in the timeline
- - Fix autocompletion issues and add support for rooms and groups
+ -
Other changes:
-
Bugfix 🐛:
- - Fix avatar image disappearing (#777)
- - Fix read marker banner when permalink
+ -
Translations 🗣:
-
@@ -22,6 +19,32 @@ Translations 🗣:
Build 🧱:
-
+Changes in RiotX 0.12.0 (2020-01-09)
+===================================================
+
+Improvements 🙌:
+ - The initial sync is now handled by a foreground service
+ - Render aliases and canonical alias change in the timeline
+ - Introduce developer mode in the settings (#745, #796)
+ - Improve devices list screen
+ - Add settings for rageshake sensibility
+ - Fix autocompletion issues and add support for rooms, groups, and emoji (#780)
+ - Show skip to bottom FAB while scrolling down (#752)
+ - Enable encryption on a room, SDK part (#212)
+
+Other changes:
+ - Change the way RiotX identifies a session to allow the SDK to support several sessions with the same user (#800)
+ - Exclude play-services-oss-licenses library from F-Droid build (#814)
+ - Email domain can be limited on some homeservers, i18n of the displayed error (#754)
+
+Bugfix 🐛:
+ - Fix crash when opening room creation screen from the room filtering screen
+ - Fix avatar image disappearing (#777)
+ - Fix read marker banner when permalink
+ - Fix joining upgraded rooms (#697)
+ - Fix matrix.org room directory not being browsable (#807)
+ - Hide non working settings (#751)
+
Changes in RiotX 0.11.0 (2019-12-19)
===================================================
@@ -281,7 +304,7 @@ Mode details here: https://medium.com/@RiotChat/introducing-the-riotx-beta-for-a
=======================================================
-Changes in RiotX 0.0.0 (2019-XX-XX)
+Changes in RiotX 0.0.0 (2020-XX-XX)
===================================================
Features ✨:
diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt
index e5ebc536ff..bbf0e76823 100644
--- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt
+++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt
@@ -17,13 +17,16 @@
package im.vector.matrix.rx
import im.vector.matrix.android.api.session.room.Room
+import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.session.room.model.ReadReceipt
+import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.notification.RoomNotificationState
import im.vector.matrix.android.api.session.room.send.UserDraft
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.util.Optional
+import im.vector.matrix.android.api.util.toOptional
import io.reactivex.Observable
import io.reactivex.Single
@@ -31,18 +34,22 @@ class RxRoom(private val room: Room) {
fun liveRoomSummary(): Observable> {
return room.getRoomSummaryLive().asObservable()
+ .startWith(room.roomSummary().toOptional())
}
- fun liveRoomMemberIds(): Observable> {
- return room.getRoomMemberIdsLive().asObservable()
+ fun liveRoomMembers(queryParams: RoomMemberQueryParams): Observable> {
+ return room.getRoomMembersLive(queryParams).asObservable()
+ .startWith(room.getRoomMembers(queryParams))
}
fun liveAnnotationSummary(eventId: String): Observable> {
- return room.getEventSummaryLive(eventId).asObservable()
+ return room.getEventAnnotationsSummaryLive(eventId).asObservable()
+ .startWith(room.getEventAnnotationsSummary(eventId).toOptional())
}
fun liveTimelineEvent(eventId: String): Observable> {
return room.getTimeLineEventLive(eventId).asObservable()
+ .startWith(room.getTimeLineEvent(eventId).toOptional())
}
fun liveReadMarker(): Observable> {
diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt
index c9381b861d..084f497de5 100644
--- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt
+++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt
@@ -18,8 +18,10 @@ package im.vector.matrix.rx
import androidx.paging.PagedList
import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.api.session.group.GroupSummaryQueryParams
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.pushers.Pusher
+import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.sync.SyncState
@@ -30,40 +32,43 @@ import io.reactivex.Single
class RxSession(private val session: Session) {
- fun liveRoomSummaries(): Observable> {
- return session.liveRoomSummaries().asObservable()
+ fun liveRoomSummaries(queryParams: RoomSummaryQueryParams): Observable> {
+ return session.getRoomSummariesLive(queryParams).asObservable()
+ .startWith(session.getRoomSummaries(queryParams))
}
- fun liveGroupSummaries(): Observable> {
- return session.liveGroupSummaries().asObservable()
+ fun liveGroupSummaries(queryParams: GroupSummaryQueryParams): Observable> {
+ return session.getGroupSummariesLive(queryParams).asObservable()
+ .startWith(session.getGroupSummaries(queryParams))
}
fun liveBreadcrumbs(): Observable> {
- return session.liveBreadcrumbs().asObservable()
+ return session.getBreadcrumbsLive().asObservable()
+ .startWith(session.getBreadcrumbs())
}
fun liveSyncState(): Observable {
- return session.syncState().asObservable()
+ return session.getSyncStateLive().asObservable()
}
fun livePushers(): Observable> {
- return session.livePushers().asObservable()
+ return session.getPushersLive().asObservable()
}
fun liveUser(userId: String): Observable> {
- return session.liveUser(userId).asObservable().distinctUntilChanged()
+ return session.getUserLive(userId).asObservable().distinctUntilChanged()
}
fun liveUsers(): Observable> {
- return session.liveUsers().asObservable()
+ return session.getUsersLive().asObservable()
}
fun liveIgnoredUsers(): Observable> {
- return session.liveIgnoredUsers().asObservable()
+ return session.getIgnoredUsersLive().asObservable()
}
fun livePagedUsers(filter: String? = null): Observable> {
- return session.livePagedUsers(filter).asObservable()
+ return session.getPagedUsersLive(filter).asObservable()
}
fun createRoom(roomParams: CreateRoomParams): Single = singleBuilder {
diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index ab5f122dbc..7a1348a54c 100644
--- a/matrix-sdk-android/build.gradle
+++ b/matrix-sdk-android/build.gradle
@@ -10,7 +10,7 @@ buildscript {
jcenter()
}
dependencies {
- classpath "io.realm:realm-gradle-plugin:5.12.0"
+ classpath "io.realm:realm-gradle-plugin:6.0.2"
}
}
@@ -102,7 +102,6 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
implementation "androidx.appcompat:appcompat:1.1.0"
- implementation "androidx.recyclerview:recyclerview:1.1.0-beta05"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
@@ -119,14 +118,14 @@ dependencies {
implementation "ru.noties.markwon:core:$markwon_version"
// Image
- implementation 'androidx.exifinterface:exifinterface:1.0.0'
+ implementation 'androidx.exifinterface:exifinterface:1.1.0'
// Database
implementation 'com.github.Zhuinden:realm-monarchy:0.5.1'
kapt 'dk.ilios:realmfieldnameshelper:1.1.1'
// Work
- implementation "androidx.work:work-runtime-ktx:2.3.0-alpha01"
+ implementation "androidx.work:work-runtime-ktx:2.3.0-beta02"
// FP
implementation "io.arrow-kt:arrow-core:$arrow_version"
diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/account/AccountCreationTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/account/AccountCreationTest.kt
new file mode 100644
index 0000000000..c44ac9c47b
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/account/AccountCreationTest.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2020 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.matrix.android.account
+
+import im.vector.matrix.android.InstrumentedTest
+import im.vector.matrix.android.common.CommonTestHelper
+import im.vector.matrix.android.common.CryptoTestHelper
+import im.vector.matrix.android.common.SessionTestParams
+import im.vector.matrix.android.common.TestConstants
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.junit.runners.MethodSorters
+
+@RunWith(JUnit4::class)
+@FixMethodOrder(MethodSorters.JVM)
+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.signout(session)
+
+ session.close()
+ }
+
+ @Test
+ fun createAccountAndLoginAgainTest() {
+ 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))
+
+ session.close()
+ session2.close()
+ }
+
+ @Test
+ fun simpleE2eTest() {
+ val res = cryptoTestHelper.doE2ETestWithAliceInARoom()
+
+ res.close()
+ }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt
new file mode 100644
index 0000000000..b16c865765
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt
@@ -0,0 +1,278 @@
+/*
+ * Copyright 2016 OpenMarket Ltd
+ * Copyright 2018 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.matrix.android.common
+
+import android.content.Context
+import android.net.Uri
+import im.vector.matrix.android.api.Matrix
+import im.vector.matrix.android.api.MatrixCallback
+import im.vector.matrix.android.api.MatrixConfiguration
+import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
+import im.vector.matrix.android.api.auth.data.LoginFlowResult
+import im.vector.matrix.android.api.auth.registration.RegistrationResult
+import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.api.session.events.model.EventType
+import im.vector.matrix.android.api.session.room.Room
+import im.vector.matrix.android.api.session.room.timeline.Timeline
+import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
+import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
+import org.junit.Assert.*
+import java.util.*
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+/**
+ * This class exposes methods to be used in common cases
+ * Registration, login, Sync, Sending messages...
+ */
+class CommonTestHelper(context: Context) {
+
+ val matrix: Matrix
+
+ init {
+ Matrix.initialize(context, MatrixConfiguration("TestFlavor"))
+
+ matrix = Matrix.getInstance(context)
+ }
+
+ fun createAccount(userNamePrefix: String, testParams: SessionTestParams): Session {
+ return createAccount(userNamePrefix, TestConstants.PASSWORD, testParams)
+ }
+
+ fun logIntoAccount(userId: String, testParams: SessionTestParams): Session {
+ return logIntoAccount(userId, TestConstants.PASSWORD, testParams)
+ }
+
+ /**
+ * Create a Home server configuration, with Http connection allowed for test
+ */
+ fun createHomeServerConfig(): HomeServerConnectionConfig {
+ return HomeServerConnectionConfig.Builder()
+ .withHomeServerUri(Uri.parse(TestConstants.TESTS_HOME_SERVER_URL))
+ .build()
+ }
+
+ /**
+ * This methods init the event stream and check for initial sync
+ *
+ * @param session the session to sync
+ */
+ fun syncSession(session: Session) {
+ // val lock = CountDownLatch(1)
+
+ // val observer = androidx.lifecycle.Observer { syncState ->
+ // if (syncState is SyncState.Idle) {
+ // lock.countDown()
+ // }
+ // }
+
+ // TODO observe?
+ // while (session.syncState().value !is SyncState.Idle) {
+ // sleep(100)
+ // }
+
+ session.open()
+ session.startSync(true)
+ // await(lock)
+ // session.syncState().removeObserver(observer)
+ }
+
+ /**
+ * Sends text messages in a room
+ *
+ * @param room the room where to send the messages
+ * @param message the message to send
+ * @param nbOfMessages the number of time the message will be sent
+ */
+ fun sendTextMessage(room: Room, message: String, nbOfMessages: Int): List {
+ val sentEvents = ArrayList(nbOfMessages)
+ val latch = CountDownLatch(nbOfMessages)
+ val onEventSentListener = object : Timeline.Listener {
+ override fun onTimelineFailure(throwable: Throwable) {
+ }
+
+ override fun onTimelineUpdated(snapshot: List) {
+ // TODO Count only new messages?
+ if (snapshot.count { it.root.type == EventType.MESSAGE } == nbOfMessages) {
+ sentEvents.addAll(snapshot.filter { it.root.type == EventType.MESSAGE })
+ latch.countDown()
+ }
+ }
+ }
+ val timeline = room.createTimeline(null, TimelineSettings(10))
+ timeline.addListener(onEventSentListener)
+ for (i in 0 until nbOfMessages) {
+ room.sendTextMessage(message + " #" + (i + 1))
+ }
+ await(latch)
+ timeline.removeListener(onEventSentListener)
+
+ // Check that all events has been created
+ assertEquals(nbOfMessages.toLong(), sentEvents.size.toLong())
+
+ return sentEvents
+ }
+
+ // PRIVATE METHODS *****************************************************************************
+
+ /**
+ * Creates a unique account
+ *
+ * @param userNamePrefix the user name prefix
+ * @param password the password
+ * @param testParams test params about the session
+ * @return the session associated with the newly created account
+ */
+ private fun createAccount(userNamePrefix: String,
+ password: String,
+ testParams: SessionTestParams): Session {
+ val session = createAccountAndSync(
+ userNamePrefix + "_" + System.currentTimeMillis() + UUID.randomUUID(),
+ password,
+ testParams
+ )
+ assertNotNull(session)
+ return session
+ }
+
+ /**
+ * Logs into an existing account
+ *
+ * @param userId the userId to log in
+ * @param password the password to log in
+ * @param testParams test params about the session
+ * @return the session associated with the existing account
+ */
+ private fun logIntoAccount(userId: String,
+ password: String,
+ testParams: SessionTestParams): Session {
+ val session = logAccountAndSync(userId, password, testParams)
+ assertNotNull(session)
+ return session
+ }
+
+ /**
+ * Create an account and a dedicated session
+ *
+ * @param userName the account username
+ * @param password the password
+ * @param sessionTestParams parameters for the test
+ */
+ private fun createAccountAndSync(userName: String,
+ password: String,
+ sessionTestParams: SessionTestParams): Session {
+ val hs = createHomeServerConfig()
+
+ doSync {
+ matrix.authenticationService
+ .getLoginFlow(hs, it)
+ }
+
+ doSync {
+ matrix.authenticationService
+ .getRegistrationWizard()
+ .createAccount(userName, password, null, it)
+ }
+
+ // Preform dummy step
+ val registrationResult = doSync {
+ matrix.authenticationService
+ .getRegistrationWizard()
+ .dummy(it)
+ }
+
+ assertTrue(registrationResult is RegistrationResult.Success)
+ val session = (registrationResult as RegistrationResult.Success).session
+ if (sessionTestParams.withInitialSync) {
+ syncSession(session)
+ }
+
+ return session
+ }
+
+ /**
+ * Start an account login
+ *
+ * @param userName the account username
+ * @param password the password
+ * @param sessionTestParams session test params
+ */
+ private fun logAccountAndSync(userName: String,
+ password: String,
+ sessionTestParams: SessionTestParams): Session {
+ val hs = createHomeServerConfig()
+
+ doSync {
+ matrix.authenticationService
+ .getLoginFlow(hs, it)
+ }
+
+ val session = doSync {
+ matrix.authenticationService
+ .getLoginWizard()
+ .login(userName, password, "myDevice", it)
+ }
+
+ if (sessionTestParams.withInitialSync) {
+ syncSession(session)
+ }
+
+ return session
+ }
+
+ /**
+ * Await for a latch and ensure the result is true
+ *
+ * @param latch
+ * @throws InterruptedException
+ */
+ fun await(latch: CountDownLatch) {
+ assertTrue(latch.await(TestConstants.timeOutMillis, TimeUnit.MILLISECONDS))
+ }
+
+ // Transform a method with a MatrixCallback to a synchronous method
+ inline fun doSync(block: (MatrixCallback) -> Unit): T {
+ val lock = CountDownLatch(1)
+ var result: T? = null
+
+ val callback = object : TestMatrixCallback(lock) {
+ override fun onSuccess(data: T) {
+ result = data
+ super.onSuccess(data)
+ }
+ }
+
+ block.invoke(callback)
+
+ await(lock)
+
+ assertNotNull(result)
+ return result!!
+ }
+
+ /**
+ * Clear all provided sessions
+ */
+ fun Iterable.close() = forEach { it.close() }
+
+ fun signout(session: Session) {
+ val lock = CountDownLatch(1)
+ session.signOut(true, object : TestMatrixCallback(lock) {})
+ await(lock)
+ }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestData.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestData.kt
new file mode 100644
index 0000000000..8ad9f1ec6f
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestData.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2018 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.matrix.android.common
+
+import im.vector.matrix.android.api.session.Session
+
+data class CryptoTestData(val firstSession: Session,
+ val roomId: String,
+ val secondSession: Session? = null,
+ val thirdSession: Session? = null) {
+
+ fun close() {
+ firstSession.close()
+ secondSession?.close()
+ secondSession?.close()
+ }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt
new file mode 100644
index 0000000000..df45249265
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt
@@ -0,0 +1,335 @@
+/*
+ * Copyright 2018 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.matrix.android.common
+
+import android.os.SystemClock
+import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.api.session.events.model.Event
+import im.vector.matrix.android.api.session.events.model.EventType
+import im.vector.matrix.android.api.session.events.model.toContent
+import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
+import im.vector.matrix.android.api.session.room.timeline.Timeline
+import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
+import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
+import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
+import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
+import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupAuthData
+import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
+import org.junit.Assert.*
+import java.util.*
+import java.util.concurrent.CountDownLatch
+
+class CryptoTestHelper(val mTestHelper: CommonTestHelper) {
+
+ val messagesFromAlice: List = Arrays.asList("0 - Hello I'm Alice!", "4 - Go!")
+ val messagesFromBob: List = Arrays.asList("1 - Hello I'm Bob!", "2 - Isn't life grand?", "3 - Let's go to the opera.")
+
+ val defaultSessionParams = SessionTestParams(true)
+
+ /**
+ * @return alice session
+ */
+ fun doE2ETestWithAliceInARoom(): CryptoTestData {
+ val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
+
+ var roomId: String? = null
+ val lock1 = CountDownLatch(1)
+
+ aliceSession.createRoom(CreateRoomParams().apply { name = "MyRoom" }, object : TestMatrixCallback(lock1) {
+ override fun onSuccess(data: String) {
+ roomId = data
+ super.onSuccess(data)
+ }
+ })
+
+ mTestHelper.await(lock1)
+ assertNotNull(roomId)
+
+ val room = aliceSession.getRoom(roomId!!)!!
+
+ val lock2 = CountDownLatch(1)
+ room.enableEncryptionWithAlgorithm(MXCRYPTO_ALGORITHM_MEGOLM, object : TestMatrixCallback(lock2) {})
+ mTestHelper.await(lock2)
+
+ return CryptoTestData(aliceSession, roomId!!)
+ }
+
+ /**
+ * @return alice and bob sessions
+ */
+ fun doE2ETestWithAliceAndBobInARoom(): CryptoTestData {
+ val statuses = HashMap()
+
+ val cryptoTestData = doE2ETestWithAliceInARoom()
+ val aliceSession = cryptoTestData.firstSession
+ val aliceRoomId = cryptoTestData.roomId
+
+ val room = aliceSession.getRoom(aliceRoomId)!!
+
+ val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, defaultSessionParams)
+
+ val lock1 = CountDownLatch(2)
+
+// val bobEventListener = object : MXEventListener() {
+// override fun onNewRoom(roomId: String) {
+// if (TextUtils.equals(roomId, aliceRoomId)) {
+// if (!statuses.containsKey("onNewRoom")) {
+// statuses["onNewRoom"] = "onNewRoom"
+// lock1.countDown()
+// }
+// }
+// }
+// }
+//
+// bobSession.dataHandler.addListener(bobEventListener)
+
+ room.invite(bobSession.myUserId, callback = object : TestMatrixCallback(lock1) {
+ override fun onSuccess(data: Unit) {
+ statuses["invite"] = "invite"
+ super.onSuccess(data)
+ }
+ })
+
+ mTestHelper.await(lock1)
+
+ assertTrue(statuses.containsKey("invite") && statuses.containsKey("onNewRoom"))
+
+// bobSession.dataHandler.removeListener(bobEventListener)
+
+ val lock2 = CountDownLatch(2)
+
+ bobSession.joinRoom(aliceRoomId, callback = TestMatrixCallback(lock2))
+
+// room.addEventListener(object : MXEventListener() {
+// override fun onLiveEvent(event: Event, roomState: RoomState) {
+// if (TextUtils.equals(event.getType(), Event.EVENT_TYPE_STATE_ROOM_MEMBER)) {
+// val contentToConsider = event.contentAsJsonObject
+// val member = JsonUtils.toRoomMember(contentToConsider)
+//
+// if (TextUtils.equals(member.membership, RoomMember.MEMBERSHIP_JOIN)) {
+// statuses["AliceJoin"] = "AliceJoin"
+// lock2.countDown()
+// }
+// }
+// }
+// })
+
+ mTestHelper.await(lock2)
+
+ // Ensure bob can send messages to the room
+// val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!!
+// assertNotNull(roomFromBobPOV.powerLevels)
+// assertTrue(roomFromBobPOV.powerLevels.maySendMessage(bobSession.myUserId))
+
+ assertTrue(statuses.toString() + "", statuses.containsKey("AliceJoin"))
+
+// bobSession.dataHandler.removeListener(bobEventListener)
+
+ return CryptoTestData(aliceSession, aliceRoomId, bobSession)
+ }
+
+ /**
+ * @return Alice, Bob and Sam session
+ */
+ fun doE2ETestWithAliceAndBobAndSamInARoom(): CryptoTestData {
+ val statuses = HashMap()
+
+ val cryptoTestData = doE2ETestWithAliceAndBobInARoom()
+ val aliceSession = cryptoTestData.firstSession
+ val aliceRoomId = cryptoTestData.roomId
+
+ val room = aliceSession.getRoom(aliceRoomId)!!
+
+ val samSession = mTestHelper.createAccount(TestConstants.USER_SAM, defaultSessionParams)
+
+ val lock1 = CountDownLatch(2)
+
+// val samEventListener = object : MXEventListener() {
+// override fun onNewRoom(roomId: String) {
+// if (TextUtils.equals(roomId, aliceRoomId)) {
+// if (!statuses.containsKey("onNewRoom")) {
+// statuses["onNewRoom"] = "onNewRoom"
+// lock1.countDown()
+// }
+// }
+// }
+// }
+//
+// samSession.dataHandler.addListener(samEventListener)
+
+ room.invite(samSession.myUserId, null, object : TestMatrixCallback(lock1) {
+ override fun onSuccess(data: Unit) {
+ statuses["invite"] = "invite"
+ super.onSuccess(data)
+ }
+ })
+
+ mTestHelper.await(lock1)
+
+ assertTrue(statuses.containsKey("invite") && statuses.containsKey("onNewRoom"))
+
+// samSession.dataHandler.removeListener(samEventListener)
+
+ val lock2 = CountDownLatch(1)
+
+ samSession.joinRoom(aliceRoomId, null, object : TestMatrixCallback(lock2) {
+ override fun onSuccess(data: Unit) {
+ statuses["joinRoom"] = "joinRoom"
+ super.onSuccess(data)
+ }
+ })
+
+ mTestHelper.await(lock2)
+ assertTrue(statuses.containsKey("joinRoom"))
+
+ // wait the initial sync
+ SystemClock.sleep(1000)
+
+// samSession.dataHandler.removeListener(samEventListener)
+
+ return CryptoTestData(aliceSession, aliceRoomId, cryptoTestData.secondSession, samSession)
+ }
+
+ /**
+ * @return Alice and Bob sessions
+ */
+ fun doE2ETestWithAliceAndBobInARoomWithEncryptedMessages(): CryptoTestData {
+ val cryptoTestData = doE2ETestWithAliceAndBobInARoom()
+ val aliceSession = cryptoTestData.firstSession
+ val aliceRoomId = cryptoTestData.roomId
+ val bobSession = cryptoTestData.secondSession!!
+
+ bobSession.setWarnOnUnknownDevices(false)
+
+ aliceSession.setWarnOnUnknownDevices(false)
+
+ val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!!
+ val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
+
+ var lock = CountDownLatch(1)
+
+ val bobEventsListener = object : Timeline.Listener {
+ override fun onTimelineFailure(throwable: Throwable) {
+ }
+
+ override fun onTimelineUpdated(snapshot: List) {
+ val size = snapshot.filter { it.root.senderId != bobSession.myUserId && it.root.getClearType() == EventType.MESSAGE }
+ .size
+
+ if (size == 3) {
+ lock.countDown()
+ }
+ }
+ }
+
+ val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(10))
+ bobTimeline.addListener(bobEventsListener)
+
+ val results = HashMap()
+
+ // bobSession.dataHandler.addListener(object : MXEventListener() {
+ // override fun onToDeviceEvent(event: Event) {
+ // results["onToDeviceEvent"] = event
+ // lock.countDown()
+ // }
+ // })
+
+ // Alice sends a message
+ roomFromAlicePOV.sendTextMessage(messagesFromAlice[0])
+ assertTrue(results.containsKey("onToDeviceEvent"))
+// assertEquals(1, messagesReceivedByBobCount)
+
+ // Bob send a message
+ lock = CountDownLatch(1)
+ roomFromBobPOV.sendTextMessage(messagesFromBob[0])
+ // android does not echo the messages sent from itself
+// messagesReceivedByBobCount++
+ mTestHelper.await(lock)
+// assertEquals(2, messagesReceivedByBobCount)
+
+ // Bob send a message
+ lock = CountDownLatch(1)
+ roomFromBobPOV.sendTextMessage(messagesFromBob[1])
+ // android does not echo the messages sent from itself
+// messagesReceivedByBobCount++
+ mTestHelper.await(lock)
+// assertEquals(3, messagesReceivedByBobCount)
+
+ // Bob send a message
+ lock = CountDownLatch(1)
+ roomFromBobPOV.sendTextMessage(messagesFromBob[2])
+ // android does not echo the messages sent from itself
+// messagesReceivedByBobCount++
+ mTestHelper.await(lock)
+// assertEquals(4, messagesReceivedByBobCount)
+
+ // Alice sends a message
+ lock = CountDownLatch(2)
+ roomFromAlicePOV.sendTextMessage(messagesFromAlice[1])
+ mTestHelper.await(lock)
+// assertEquals(5, messagesReceivedByBobCount)
+
+ bobTimeline.removeListener(bobEventsListener)
+
+ return cryptoTestData
+ }
+
+ fun checkEncryptedEvent(event: Event, roomId: String, clearMessage: String, senderSession: Session) {
+ assertEquals(EventType.ENCRYPTED, event.type)
+ assertNotNull(event.content)
+
+ val eventWireContent = event.content.toContent()
+ assertNotNull(eventWireContent)
+
+ assertNull(eventWireContent.get("body"))
+ assertEquals(MXCRYPTO_ALGORITHM_MEGOLM, eventWireContent.get("algorithm"))
+
+ assertNotNull(eventWireContent.get("ciphertext"))
+ assertNotNull(eventWireContent.get("session_id"))
+ assertNotNull(eventWireContent.get("sender_key"))
+
+ assertEquals(senderSession.sessionParams.credentials.deviceId, eventWireContent.get("device_id"))
+
+ assertNotNull(event.eventId)
+ assertEquals(roomId, event.roomId)
+ assertEquals(EventType.MESSAGE, event.getClearType())
+ // TODO assertTrue(event.getAge() < 10000)
+
+ val eventContent = event.toContent()
+ assertNotNull(eventContent)
+ assertEquals(clearMessage, eventContent.get("body"))
+ assertEquals(senderSession.myUserId, event.senderId)
+ }
+
+ fun createFakeMegolmBackupAuthData(): MegolmBackupAuthData {
+ return MegolmBackupAuthData(
+ publicKey = "abcdefg",
+ signatures = HashMap>().apply {
+ this["something"] = HashMap().apply {
+ this["ed25519:something"] = "hijklmnop"
+ }
+ }
+ )
+ }
+
+ fun createFakeMegolmBackupCreationInfo(): MegolmBackupCreationInfo {
+ return MegolmBackupCreationInfo().apply {
+ algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
+ authData = createFakeMegolmBackupAuthData()
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/MockOkHttpInterceptor.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/MockOkHttpInterceptor.kt
new file mode 100644
index 0000000000..a1f95424a6
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/MockOkHttpInterceptor.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.matrix.android.common
+
+import okhttp3.Interceptor
+import okhttp3.Protocol
+import okhttp3.Request
+import okhttp3.Response
+import okhttp3.ResponseBody.Companion.toResponseBody
+import javax.net.ssl.HttpsURLConnection
+
+/**
+ * Allows to intercept network requests for test purpose by
+ * - re-writing the response
+ * - changing the response code (200/404/etc..).
+ * - Test delays..
+ *
+ * Basic usage:
+ *
+ * val mockInterceptor = MockOkHttpInterceptor()
+ * mockInterceptor.addRule(MockOkHttpInterceptor.SimpleRule(".well-known/matrix/client", 200, "{}"))
+ *
+ * RestHttpClientFactoryProvider.defaultProvider = RestClientHttpClientFactory(mockInterceptor)
+ * AutoDiscovery().findClientConfig("matrix.org", )
+ *
+ */
+class MockOkHttpInterceptor : Interceptor {
+
+ private var rules: ArrayList = ArrayList()
+
+ fun addRule(rule: Rule) {
+ rules.add(rule)
+ }
+
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val originalRequest = chain.request()
+
+ rules.forEach { rule ->
+ if (originalRequest.url.toString().contains(rule.match)) {
+ rule.process(originalRequest)?.let {
+ return it
+ }
+ }
+ }
+
+ return chain.proceed(originalRequest)
+ }
+
+ abstract class Rule(val match: String) {
+ abstract fun process(originalRequest: Request): Response?
+ }
+
+ /**
+ * Simple rule that reply with the given body for any request that matches the match param
+ */
+ class SimpleRule(match: String,
+ private val code: Int = HttpsURLConnection.HTTP_OK,
+ private val body: String = "{}") : Rule(match) {
+
+ override fun process(originalRequest: Request): Response? {
+ return Response.Builder()
+ .protocol(Protocol.HTTP_1_1)
+ .request(originalRequest)
+ .message("mocked answer")
+ .body(body.toResponseBody(null))
+ .code(code)
+ .build()
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/SessionTestParams.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/SessionTestParams.kt
new file mode 100644
index 0000000000..7d1d23e951
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/SessionTestParams.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2018 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.matrix.android.common
+
+data class SessionTestParams @JvmOverloads constructor(val withInitialSync: Boolean = false)
diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestAssertUtil.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestAssertUtil.kt
new file mode 100644
index 0000000000..2a62165210
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestAssertUtil.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2018 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.matrix.android.common
+
+import org.junit.Assert.*
+
+/**
+ * Compare two lists and their content
+ */
+fun assertListEquals(list1: List?, list2: List?) {
+ if (list1 == null) {
+ assertNull(list2)
+ } else {
+ assertNotNull(list2)
+
+ assertEquals("List sizes must match", list1.size, list2!!.size)
+
+ for (i in list1.indices) {
+ assertEquals("Elements at index $i are not equal", list1[i], list2[i])
+ }
+ }
+}
+
+/**
+ * Compare two maps and their content
+ */
+fun assertDictEquals(dict1: Map?, dict2: Map?) {
+ if (dict1 == null) {
+ assertNull(dict2)
+ } else {
+ assertNotNull(dict2)
+
+ assertEquals("Map sizes must match", dict1.size, dict2!!.size)
+
+ for (i in dict1.keys) {
+ assertEquals("Values for key $i are not equal", dict1[i], dict2[i])
+ }
+ }
+}
+
+/**
+ * Compare two byte arrays content.
+ * Note that if the arrays have not the same size, it also fails.
+ */
+fun assertByteArrayNotEqual(a1: ByteArray, a2: ByteArray) {
+ if (a1.size != a2.size) {
+ fail("Arrays have not the same size.")
+ }
+
+ for (index in a1.indices) {
+ if (a1[index] != a2[index]) {
+ // Difference found!
+ return
+ }
+ }
+
+ fail("Arrays are equals.")
+}
diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestConstants.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestConstants.kt
new file mode 100644
index 0000000000..60cc87d330
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestConstants.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2018 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.matrix.android.common
+
+import android.os.Debug
+
+object TestConstants {
+
+ const val TESTS_HOME_SERVER_URL = "http://10.0.2.2:8080"
+
+ // Time out to use when waiting for server response. 60s
+ private const val AWAIT_TIME_OUT_MILLIS = 60000
+
+ // Time out to use when waiting for server response, when the debugger is connected. 10 minutes
+ private const val AWAIT_TIME_OUT_WITH_DEBUGGER_MILLIS = 10 * 60000
+
+ const val USER_ALICE = "Alice"
+ const val USER_BOB = "Bob"
+ const val USER_SAM = "Sam"
+
+ const val PASSWORD = "password"
+
+ val timeOutMillis: Long
+ get() = if (Debug.isDebuggerConnected()) {
+ // Wait more
+ AWAIT_TIME_OUT_WITH_DEBUGGER_MILLIS.toLong()
+ } else {
+ AWAIT_TIME_OUT_MILLIS.toLong()
+ }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestMatrixCallback.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestMatrixCallback.kt
new file mode 100644
index 0000000000..c04777440b
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestMatrixCallback.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2018 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.matrix.android.common
+
+import androidx.annotation.CallSuper
+import im.vector.matrix.android.api.MatrixCallback
+import org.junit.Assert.fail
+import timber.log.Timber
+import java.util.concurrent.CountDownLatch
+
+/**
+ * Simple implementation of MatrixCallback, which count down the CountDownLatch on each API callback
+ * @param onlySuccessful true to fail if an error occurs. This is the default behavior
+ * @param
+ */
+open class TestMatrixCallback(private val countDownLatch: CountDownLatch,
+ private val onlySuccessful: Boolean = true) : MatrixCallback {
+
+ @CallSuper
+ override fun onSuccess(data: T) {
+ countDownLatch.countDown()
+ }
+
+ @CallSuper
+ override fun onFailure(failure: Throwable) {
+ Timber.e(failure, "TestApiCallback")
+
+ if (onlySuccessful) {
+ fail("onFailure " + failure.localizedMessage)
+ }
+
+ countDownLatch.countDown()
+ }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/AttachmentEncryptionTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/AttachmentEncryptionTest.kt
new file mode 100644
index 0000000000..84b3f24191
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/AttachmentEncryptionTest.kt
@@ -0,0 +1,148 @@
+/*
+ * 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.matrix.android.internal.crypto
+
+import android.os.MemoryFile
+import android.util.Base64
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
+import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
+import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileKey
+import org.junit.Assert.*
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import java.io.ByteArrayInputStream
+import java.io.InputStream
+
+/**
+ * Unit tests AttachmentEncryptionTest.
+ */
+@Suppress("SpellCheckingInspection")
+@RunWith(AndroidJUnit4::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class AttachmentEncryptionTest {
+
+ private fun checkDecryption(input: String, encryptedFileInfo: EncryptedFileInfo): String {
+ val `in` = Base64.decode(input, Base64.DEFAULT)
+
+ val inputStream: InputStream
+
+ inputStream = if (`in`.isEmpty()) {
+ ByteArrayInputStream(`in`)
+ } else {
+ val memoryFile = MemoryFile("file" + System.currentTimeMillis(), `in`.size)
+ memoryFile.outputStream.write(`in`)
+ memoryFile.inputStream
+ }
+
+ val decryptedStream = MXEncryptedAttachments.decryptAttachment(inputStream, encryptedFileInfo)
+
+ assertNotNull(decryptedStream)
+
+ inputStream.close()
+
+ val buffer = ByteArray(100)
+
+ val len = decryptedStream!!.read(buffer)
+
+ decryptedStream.close()
+
+ return Base64.encodeToString(buffer, 0, len, Base64.DEFAULT).replace("\n".toRegex(), "").replace("=".toRegex(), "")
+ }
+
+ @Test
+ fun checkDecrypt1() {
+ val encryptedFileInfo = EncryptedFileInfo(
+ v = "v2",
+ hashes = mapOf("sha256" to "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU"),
+ key = EncryptedFileKey(
+ alg = "A256CTR",
+ k = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+ key_ops = listOf("encrypt", "decrypt"),
+ kty = "oct",
+ ext = true
+ ),
+ iv = "AAAAAAAAAAAAAAAAAAAAAA",
+ url = "dummyUrl"
+ )
+
+ assertEquals("", checkDecryption("", encryptedFileInfo))
+ }
+
+ @Test
+ fun checkDecrypt2() {
+ val encryptedFileInfo = EncryptedFileInfo(
+ v = "v2",
+ hashes = mapOf("sha256" to "YzF08lARDdOCzJpzuSwsjTNlQc4pHxpdHcXiD/wpK6k"),
+ key = EncryptedFileKey(
+ alg = "A256CTR",
+ k = "__________________________________________8",
+ key_ops = listOf("encrypt", "decrypt"),
+ kty = "oct",
+ ext = true
+ ),
+ iv = "//////////8AAAAAAAAAAA",
+ url = "dummyUrl"
+ )
+
+ assertEquals("SGVsbG8sIFdvcmxk", checkDecryption("5xJZTt5cQicm+9f4", encryptedFileInfo))
+ }
+
+ @Test
+ fun checkDecrypt3() {
+ val encryptedFileInfo = EncryptedFileInfo(
+ v = "v2",
+ hashes = mapOf("sha256" to "IOq7/dHHB+mfHfxlRY5XMeCWEwTPmlf4cJcgrkf6fVU"),
+ key = EncryptedFileKey(
+ alg = "A256CTR",
+ k = "__________________________________________8",
+ key_ops = listOf("encrypt", "decrypt"),
+ kty = "oct",
+ ext = true
+ ),
+ iv = "//////////8AAAAAAAAAAA",
+ url = "dummyUrl"
+ )
+
+ assertEquals("YWxwaGFudW1lcmljYWxseWFscGhhbnVtZXJpY2FsbHlhbHBoYW51bWVyaWNhbGx5YWxwaGFudW1lcmljYWxseQ",
+ checkDecryption("zhtFStAeFx0s+9L/sSQO+WQMtldqYEHqTxMduJrCIpnkyer09kxJJuA4K+adQE4w+7jZe/vR9kIcqj9rOhDR8Q",
+ encryptedFileInfo))
+ }
+
+ @Test
+ fun checkDecrypt4() {
+ val encryptedFileInfo = EncryptedFileInfo(
+ v = "v2",
+ hashes = mapOf("sha256" to "LYG/orOViuFwovJpv2YMLSsmVKwLt7pY3f8SYM7KU5E"),
+ key = EncryptedFileKey(
+ alg = "A256CTR",
+ k = "__________________________________________8",
+ key_ops = listOf("encrypt", "decrypt"),
+ kty = "oct",
+ ext = true
+ ),
+ iv = "/////////////////////w",
+ url = "dummyUrl"
+ )
+
+ assertNotEquals("YWxwaGFudW1lcmljYWxseWFscGhhbnVtZXJpY2FsbHlhbHBoYW51bWVyaWNhbGx5YWxwaGFudW1lcmljYWxseQ",
+ checkDecryption("tJVNBVJ/vl36UQt4Y5e5m84bRUrQHhcdLPvS/7EkDvlkDLZXamBB6k8THbiawiKZ5Mnq9PZMSSbgOCvmnUBOMA",
+ encryptedFileInfo))
+ }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/ExportEncryptionTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/ExportEncryptionTest.kt
new file mode 100644
index 0000000000..89ed3c2e65
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/ExportEncryptionTest.kt
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2018 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.matrix.android.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
+
+/**
+ * Unit tests ExportEncryptionTest.
+ */
+@RunWith(AndroidJUnit4::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class ExportEncryptionTest {
+
+ @Test
+ fun checkExportError1() {
+ val password = "password"
+ val input = "-----"
+ var failed = false
+
+ try {
+ MXMegolmExportEncryption.decryptMegolmKeyFile(input.toByteArray(charset("UTF-8")), password)
+ } catch (e: Exception) {
+ failed = true
+ }
+
+ assertTrue(failed)
+ }
+
+ @Test
+ fun checkExportError2() {
+ val password = "password"
+ val input = "-----BEGIN MEGOLM SESSION DATA-----\n" + "-----"
+ var failed = false
+
+ try {
+ MXMegolmExportEncryption.decryptMegolmKeyFile(input.toByteArray(charset("UTF-8")), password)
+ } catch (e: Exception) {
+ failed = true
+ }
+
+ assertTrue(failed)
+ }
+
+ @Test
+ fun checkExportError3() {
+ val password = "password"
+ val input = "-----BEGIN MEGOLM SESSION DATA-----\n" +
+ " AXNhbHRzYWx0c2FsdHNhbHSIiIiIiIiIiIiIiIiIiIiIAAAACmIRUW2OjZ3L2l6j9h0lHlV3M2dx\n" +
+ " cissyYBxjsfsAn\n" +
+ " -----END MEGOLM SESSION DATA-----"
+ var failed = false
+
+ try {
+ MXMegolmExportEncryption.decryptMegolmKeyFile(input.toByteArray(charset("UTF-8")), password)
+ } catch (e: Exception) {
+ failed = true
+ }
+
+ assertTrue(failed)
+ }
+
+ @Test
+ fun checkExportDecrypt1() {
+ val password = "password"
+ val input = "-----BEGIN MEGOLM SESSION DATA-----\nAXNhbHRzYWx0c2FsdHNhbHSIiIiIiIiIiIiIiIiIiIiIAAAACmIRUW2OjZ3L2l6j9h0lHlV3M2dx\n" + "cissyYBxjsfsAndErh065A8=\n-----END MEGOLM SESSION DATA-----"
+ val expectedString = "plain"
+
+ var decodedString: String? = null
+ try {
+ decodedString = MXMegolmExportEncryption.decryptMegolmKeyFile(input.toByteArray(charset("UTF-8")), password)
+ } catch (e: Exception) {
+ fail("## checkExportDecrypt1() failed : " + e.message)
+ }
+
+ assertEquals("## checkExportDecrypt1() : expectedString $expectedString -- decodedString $decodedString",
+ expectedString,
+ decodedString)
+ }
+
+ @Test
+ fun checkExportDecrypt2() {
+ val password = "betterpassword"
+ val input = "-----BEGIN MEGOLM SESSION DATA-----\nAW1vcmVzYWx0bW9yZXNhbHT//////////wAAAAAAAAAAAAAD6KyBpe1Niv5M5NPm4ZATsJo5nghk\n" + "KYu63a0YQ5DRhUWEKk7CcMkrKnAUiZny\n-----END MEGOLM SESSION DATA-----"
+ val expectedString = "Hello, World"
+
+ var decodedString: String? = null
+ try {
+ decodedString = MXMegolmExportEncryption.decryptMegolmKeyFile(input.toByteArray(charset("UTF-8")), password)
+ } catch (e: Exception) {
+ fail("## checkExportDecrypt2() failed : " + e.message)
+ }
+
+ assertEquals("## checkExportDecrypt2() : expectedString $expectedString -- decodedString $decodedString",
+ expectedString,
+ decodedString)
+ }
+
+ @Test
+ fun checkExportDecrypt3() {
+ val password = "SWORDFISH"
+ val input = "-----BEGIN MEGOLM SESSION DATA-----\nAXllc3NhbHR5Z29vZG5lc3P//////////wAAAAAAAAAAAAAD6OIW+Je7gwvjd4kYrb+49gKCfExw\n" + "MgJBMD4mrhLkmgAngwR1pHjbWXaoGybtiAYr0moQ93GrBQsCzPbvl82rZhaXO3iH5uHo/RCEpOqp\nPgg29363BGR+/Ripq/VCLKGNbw==\n-----END MEGOLM SESSION DATA-----"
+ val expectedString = "alphanumericallyalphanumericallyalphanumericallyalphanumerically"
+
+ var decodedString: String? = null
+ try {
+ decodedString = MXMegolmExportEncryption.decryptMegolmKeyFile(input.toByteArray(charset("UTF-8")), password)
+ } catch (e: Exception) {
+ fail("## checkExportDecrypt3() failed : " + e.message)
+ }
+
+ assertEquals("## checkExportDecrypt3() : expectedString $expectedString -- decodedString $decodedString",
+ expectedString,
+ decodedString)
+ }
+
+ @Test
+ fun checkExportEncrypt1() {
+ val password = "password"
+ val expectedString = "plain"
+ var decodedString: String? = null
+
+ try {
+ decodedString = MXMegolmExportEncryption
+ .decryptMegolmKeyFile(MXMegolmExportEncryption.encryptMegolmKeyFile(expectedString, password, 1000), password)
+ } catch (e: Exception) {
+ fail("## checkExportEncrypt1() failed : " + e.message)
+ }
+
+ assertEquals("## checkExportEncrypt1() : expectedString $expectedString -- decodedString $decodedString",
+ expectedString,
+ decodedString)
+ }
+
+ @Test
+ fun checkExportEncrypt2() {
+ val password = "betterpassword"
+ val expectedString = "Hello, World"
+ var decodedString: String? = null
+
+ try {
+ decodedString = MXMegolmExportEncryption
+ .decryptMegolmKeyFile(MXMegolmExportEncryption.encryptMegolmKeyFile(expectedString, password, 1000), password)
+ } catch (e: Exception) {
+ fail("## checkExportEncrypt2() failed : " + e.message)
+ }
+
+ assertEquals("## checkExportEncrypt2() : expectedString $expectedString -- decodedString $decodedString",
+ expectedString,
+ decodedString)
+ }
+
+ @Test
+ fun checkExportEncrypt3() {
+ val password = "SWORDFISH"
+ val expectedString = "alphanumericallyalphanumericallyalphanumericallyalphanumerically"
+ var decodedString: String? = null
+
+ try {
+ decodedString = MXMegolmExportEncryption
+ .decryptMegolmKeyFile(MXMegolmExportEncryption.encryptMegolmKeyFile(expectedString, password, 1000), password)
+ } catch (e: Exception) {
+ fail("## checkExportEncrypt3() failed : " + e.message)
+ }
+
+ assertEquals("## checkExportEncrypt3() : expectedString $expectedString -- decodedString $decodedString",
+ expectedString,
+ decodedString)
+ }
+
+ @Test
+ fun checkExportEncrypt4() {
+ val password = "passwordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpassword" + "passwordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpassword"
+ val expectedString = "alphanumericallyalphanumericallyalphanumericallyalphanumerically"
+ var decodedString: String? = null
+
+ try {
+ decodedString = MXMegolmExportEncryption
+ .decryptMegolmKeyFile(MXMegolmExportEncryption.encryptMegolmKeyFile(expectedString, password, 1000), password)
+ } catch (e: Exception) {
+ fail("## checkExportEncrypt4() failed : " + e.message)
+ }
+
+ assertEquals("## checkExportEncrypt4() : expectedString $expectedString -- decodedString $decodedString",
+ expectedString,
+ decodedString)
+ }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupPasswordTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupPasswordTest.kt
new file mode 100644
index 0000000000..53e68383ee
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupPasswordTest.kt
@@ -0,0 +1,178 @@
+/*
+ * 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.matrix.android.internal.crypto.keysbackup
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import im.vector.matrix.android.InstrumentedTest
+import im.vector.matrix.android.api.listeners.ProgressListener
+import im.vector.matrix.android.common.assertByteArrayNotEqual
+import org.junit.Assert.*
+import org.junit.Before
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.matrix.olm.OlmManager
+import org.matrix.olm.OlmPkDecryption
+
+@RunWith(AndroidJUnit4::class)
+@FixMethodOrder(MethodSorters.JVM)
+class KeysBackupPasswordTest : InstrumentedTest {
+
+ @Before
+ fun ensureLibLoaded() {
+ OlmManager()
+ }
+
+ /**
+ * Check KeysBackupPassword utilities
+ */
+ @Test
+ fun passwordConverter_ok() {
+ val generatePrivateKeyResult = generatePrivateKeyWithPassword(PASSWORD, null)
+
+ assertEquals(32, generatePrivateKeyResult.salt.length)
+ assertEquals(500_000, generatePrivateKeyResult.iterations)
+ assertEquals(OlmPkDecryption.privateKeyLength(), generatePrivateKeyResult.privateKey.size)
+
+ // Reverse operation
+ val retrievedPrivateKey = retrievePrivateKeyWithPassword(PASSWORD,
+ generatePrivateKeyResult.salt,
+ generatePrivateKeyResult.iterations)
+
+ assertEquals(OlmPkDecryption.privateKeyLength(), retrievedPrivateKey.size)
+ assertArrayEquals(generatePrivateKeyResult.privateKey, retrievedPrivateKey)
+ }
+
+ /**
+ * Check generatePrivateKeyWithPassword progress listener behavior
+ */
+ @Test
+ fun passwordConverter_progress_ok() {
+ val progressValues = ArrayList(101)
+ var lastTotal = 0
+
+ generatePrivateKeyWithPassword(PASSWORD, object : ProgressListener {
+ override fun onProgress(progress: Int, total: Int) {
+ if (!progressValues.contains(progress)) {
+ progressValues.add(progress)
+ }
+
+ lastTotal = total
+ }
+ })
+
+ assertEquals(100, lastTotal)
+
+ // Ensure all values are here
+ assertEquals(101, progressValues.size)
+
+ for (i in 0..100) {
+ assertTrue(progressValues[i] == i)
+ }
+ }
+
+ /**
+ * Check KeysBackupPassword utilities, with bad password
+ */
+ @Test
+ fun passwordConverter_badPassword_ok() {
+ val generatePrivateKeyResult = generatePrivateKeyWithPassword(PASSWORD, null)
+
+ assertEquals(32, generatePrivateKeyResult.salt.length)
+ assertEquals(500_000, generatePrivateKeyResult.iterations)
+ assertEquals(OlmPkDecryption.privateKeyLength(), generatePrivateKeyResult.privateKey.size)
+
+ // Reverse operation, with bad password
+ val retrievedPrivateKey = retrievePrivateKeyWithPassword(BAD_PASSWORD,
+ generatePrivateKeyResult.salt,
+ generatePrivateKeyResult.iterations)
+
+ assertEquals(OlmPkDecryption.privateKeyLength(), retrievedPrivateKey.size)
+ assertByteArrayNotEqual(generatePrivateKeyResult.privateKey, retrievedPrivateKey)
+ }
+
+ /**
+ * Check KeysBackupPassword utilities, with bad password
+ */
+ @Test
+ fun passwordConverter_badIteration_ok() {
+ val generatePrivateKeyResult = generatePrivateKeyWithPassword(PASSWORD, null)
+
+ assertEquals(32, generatePrivateKeyResult.salt.length)
+ assertEquals(500_000, generatePrivateKeyResult.iterations)
+ assertEquals(OlmPkDecryption.privateKeyLength(), generatePrivateKeyResult.privateKey.size)
+
+ // Reverse operation, with bad iteration
+ val retrievedPrivateKey = retrievePrivateKeyWithPassword(PASSWORD,
+ generatePrivateKeyResult.salt,
+ 500_001)
+
+ assertEquals(OlmPkDecryption.privateKeyLength(), retrievedPrivateKey.size)
+ assertByteArrayNotEqual(generatePrivateKeyResult.privateKey, retrievedPrivateKey)
+ }
+
+ /**
+ * Check KeysBackupPassword utilities, with bad salt
+ */
+ @Test
+ fun passwordConverter_badSalt_ok() {
+ val generatePrivateKeyResult = generatePrivateKeyWithPassword(PASSWORD, null)
+
+ assertEquals(32, generatePrivateKeyResult.salt.length)
+ assertEquals(500_000, generatePrivateKeyResult.iterations)
+ assertEquals(OlmPkDecryption.privateKeyLength(), generatePrivateKeyResult.privateKey.size)
+
+ // Reverse operation, with bad iteration
+ val retrievedPrivateKey = retrievePrivateKeyWithPassword(PASSWORD,
+ BAD_SALT,
+ generatePrivateKeyResult.iterations)
+
+ assertEquals(OlmPkDecryption.privateKeyLength(), retrievedPrivateKey.size)
+ assertByteArrayNotEqual(generatePrivateKeyResult.privateKey, retrievedPrivateKey)
+ }
+
+ /**
+ * Check [retrievePrivateKeyWithPassword] with data coming from another platform (RiotWeb).
+ */
+ @Test
+ fun passwordConverter_crossPlatform_ok() {
+ val password = "This is a passphrase!"
+ val salt = "TO0lxhQ9aYgGfMsclVWPIAublg8h9Nlu"
+ val iteration = 500_000
+
+ val retrievedPrivateKey = retrievePrivateKeyWithPassword(password, salt, iteration)
+
+ assertEquals(OlmPkDecryption.privateKeyLength(), retrievedPrivateKey.size)
+
+ // Data from RiotWeb
+ val privateKeyBytes = byteArrayOf(
+ 116.toByte(), 224.toByte(), 229.toByte(), 224.toByte(), 9.toByte(), 3.toByte(), 178.toByte(), 162.toByte(),
+ 120.toByte(), 23.toByte(), 108.toByte(), 218.toByte(), 22.toByte(), 61.toByte(), 241.toByte(), 200.toByte(),
+ 235.toByte(), 173.toByte(), 236.toByte(), 100.toByte(), 115.toByte(), 247.toByte(), 33.toByte(), 132.toByte(),
+ 195.toByte(), 154.toByte(), 64.toByte(), 158.toByte(), 184.toByte(), 148.toByte(), 20.toByte(), 85.toByte())
+
+ assertArrayEquals(privateKeyBytes, retrievedPrivateKey)
+ }
+
+ companion object {
+ private const val PASSWORD = "password"
+ private const val BAD_PASSWORD = "passw0rd"
+
+ private const val BAD_SALT = "AA0lxhQ9aYgGfMsclVWPIAublg8h9Nlu"
+ }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt
new file mode 100644
index 0000000000..15deebdab1
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt
@@ -0,0 +1,1417 @@
+/*
+ * Copyright 2018 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.matrix.android.internal.crypto.keysbackup
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import im.vector.matrix.android.InstrumentedTest
+import im.vector.matrix.android.api.MatrixCallback
+import im.vector.matrix.android.api.listeners.ProgressListener
+import im.vector.matrix.android.api.listeners.StepProgressListener
+import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
+import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
+import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener
+import im.vector.matrix.android.common.*
+import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
+import im.vector.matrix.android.internal.crypto.MegolmSessionData
+import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
+import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrust
+import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
+import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion
+import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
+import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
+import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
+import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
+import org.junit.Assert.*
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import java.util.*
+import java.util.concurrent.CountDownLatch
+
+@RunWith(AndroidJUnit4::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class KeysBackupTest : InstrumentedTest {
+
+ private val mTestHelper = CommonTestHelper(context())
+ private val mCryptoTestHelper = CryptoTestHelper(mTestHelper)
+
+ private val defaultSessionParams = SessionTestParams(withInitialSync = false)
+ private val defaultSessionParamsWithInitialSync = SessionTestParams(withInitialSync = true)
+
+ /**
+ * - 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 cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
+
+ // From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys
+ val cryptoStore = (cryptoTestData.firstSession.getKeysBackupService() as KeysBackup).store
+ val sessions = cryptoStore.inboundGroupSessionsToBackup(100)
+ val sessionsCount = sessions.size
+
+ assertFalse(sessions.isEmpty())
+ assertEquals(sessionsCount, cryptoTestData.firstSession.inboundGroupSessionsCount(false))
+ assertEquals(0, cryptoTestData.firstSession.inboundGroupSessionsCount(true))
+
+ // - Check backup keys after having marked one as backed up
+ val session = sessions[0]
+
+ cryptoStore.markBackupDoneForInboundGroupSessions(Collections.singletonList(session))
+
+ assertEquals(sessionsCount, cryptoTestData.firstSession.inboundGroupSessionsCount(false))
+ assertEquals(1, cryptoTestData.firstSession.inboundGroupSessionsCount(true))
+
+ val sessions2 = cryptoStore.inboundGroupSessionsToBackup(100)
+ assertEquals(sessionsCount - 1, sessions2.size)
+
+ // - Reset keys backup markers
+ cryptoStore.resetBackupMarkers()
+
+ val sessions3 = cryptoStore.inboundGroupSessionsToBackup(100)
+ assertEquals(sessionsCount, sessions3.size)
+ assertEquals(sessionsCount, cryptoTestData.firstSession.inboundGroupSessionsCount(false))
+ assertEquals(0, cryptoTestData.firstSession.inboundGroupSessionsCount(true))
+ }
+
+ /**
+ * Check that prepareKeysBackupVersionWithPassword returns valid data
+ */
+ @Test
+ fun prepareKeysBackupVersionTest() {
+ val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, defaultSessionParams)
+
+ assertNotNull(bobSession.getKeysBackupService())
+
+ val keysBackup = bobSession.getKeysBackupService()
+
+ val stateObserver = StateObserver(keysBackup)
+
+ assertFalse(keysBackup.isEnabled)
+
+ val latch = CountDownLatch(1)
+
+ keysBackup.prepareKeysBackupVersion(null, null, object : MatrixCallback {
+ override fun onSuccess(data: MegolmBackupCreationInfo) {
+ assertNotNull(data)
+
+ assertEquals(MXCRYPTO_ALGORITHM_MEGOLM_BACKUP, data.algorithm)
+ assertNotNull(data.authData)
+ assertNotNull(data.authData!!.publicKey)
+ assertNotNull(data.authData!!.signatures)
+ assertNotNull(data.recoveryKey)
+
+ latch.countDown()
+ }
+
+ override fun onFailure(failure: Throwable) {
+ fail(failure.localizedMessage)
+
+ latch.countDown()
+ }
+ })
+ mTestHelper.await(latch)
+
+ stateObserver.stopAndCheckStates(null)
+ bobSession.close()
+ }
+
+ /**
+ * Test creating a keys backup version and check that createKeysBackupVersion() returns valid data
+ */
+ @Test
+ fun createKeysBackupVersionTest() {
+ val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, defaultSessionParams)
+
+ val keysBackup = bobSession.getKeysBackupService()
+
+ val stateObserver = StateObserver(keysBackup)
+
+ assertFalse(keysBackup.isEnabled)
+
+ var megolmBackupCreationInfo: MegolmBackupCreationInfo? = null
+ val latch = CountDownLatch(1)
+ keysBackup.prepareKeysBackupVersion(null, null, object : MatrixCallback {
+ override fun onSuccess(data: MegolmBackupCreationInfo) {
+ megolmBackupCreationInfo = data
+
+ latch.countDown()
+ }
+
+ override fun onFailure(failure: Throwable) {
+ fail(failure.localizedMessage)
+
+ latch.countDown()
+ }
+ })
+ mTestHelper.await(latch)
+
+ assertNotNull(megolmBackupCreationInfo)
+
+ assertFalse(keysBackup.isEnabled)
+
+ val latch2 = CountDownLatch(1)
+
+ // Create the version
+ keysBackup.createKeysBackupVersion(megolmBackupCreationInfo!!, object : TestMatrixCallback(latch2) {
+ override fun onSuccess(data: KeysVersion) {
+ assertNotNull(data)
+ assertNotNull(data.version)
+
+ super.onSuccess(data)
+ }
+ })
+ mTestHelper.await(latch2)
+
+ // Backup must be enable now
+ assertTrue(keysBackup.isEnabled)
+
+ stateObserver.stopAndCheckStates(null)
+ bobSession.close()
+ }
+
+ /**
+ * - Check that createKeysBackupVersion() launches the backup
+ * - Check the backup completes
+ */
+ @Test
+ fun backupAfterCreateKeysBackupVersionTest() {
+ val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
+
+ val keysBackup = cryptoTestData.firstSession.getKeysBackupService()
+
+ val latch = CountDownLatch(1)
+
+ assertEquals(2, cryptoTestData.firstSession.inboundGroupSessionsCount(false))
+ assertEquals(0, cryptoTestData.firstSession.inboundGroupSessionsCount(true))
+
+ val stateObserver = StateObserver(keysBackup, latch, 5)
+
+ prepareAndCreateKeysBackupData(keysBackup)
+
+ mTestHelper.await(latch)
+
+ val nbOfKeys = cryptoTestData.firstSession.inboundGroupSessionsCount(false)
+ val backedUpKeys = cryptoTestData.firstSession.inboundGroupSessionsCount(true)
+
+ assertEquals(2, nbOfKeys)
+ assertEquals("All keys must have been marked as backed up", nbOfKeys, backedUpKeys)
+
+ // Check the several backup state changes
+ stateObserver.stopAndCheckStates(
+ listOf(
+ KeysBackupState.Enabling,
+ KeysBackupState.ReadyToBackUp,
+ KeysBackupState.WillBackUp,
+ KeysBackupState.BackingUp,
+ KeysBackupState.ReadyToBackUp
+ )
+ )
+ cryptoTestData.close()
+ }
+
+ /**
+ * Check that backupAllGroupSessions() returns valid data
+ */
+ @Test
+ fun backupAllGroupSessionsTest() {
+ val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
+
+ val keysBackup = cryptoTestData.firstSession.getKeysBackupService()
+
+ val stateObserver = StateObserver(keysBackup)
+
+ prepareAndCreateKeysBackupData(keysBackup)
+
+ // Check that backupAllGroupSessions returns valid data
+ val nbOfKeys = cryptoTestData.firstSession.inboundGroupSessionsCount(false)
+
+ assertEquals(2, nbOfKeys)
+
+ val latch = CountDownLatch(1)
+
+ var lastBackedUpKeysProgress = 0
+
+ keysBackup.backupAllGroupSessions(object : ProgressListener {
+ override fun onProgress(progress: Int, total: Int) {
+ assertEquals(nbOfKeys, total)
+ lastBackedUpKeysProgress = progress
+ }
+ }, TestMatrixCallback(latch))
+
+ mTestHelper.await(latch)
+ assertEquals(nbOfKeys, lastBackedUpKeysProgress)
+
+ val backedUpKeys = cryptoTestData.firstSession.inboundGroupSessionsCount(true)
+
+ assertEquals("All keys must have been marked as backed up", nbOfKeys, backedUpKeys)
+
+ stateObserver.stopAndCheckStates(null)
+ cryptoTestData.close()
+ }
+
+ /**
+ * Check encryption and decryption of megolm keys in the backup.
+ * - Pick a megolm key
+ * - Check [MXKeyBackup encryptGroupSession] returns stg
+ * - Check [MXKeyBackup pkDecryptionFromRecoveryKey] is able to create a OLMPkDecryption
+ * - Check [MXKeyBackup decryptKeyBackupData] returns stg
+ * - Compare the decrypted megolm key with the original one
+ */
+ @Test
+ fun testEncryptAndDecryptKeysBackupData() {
+ val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
+
+ val keysBackup = cryptoTestData.firstSession.getKeysBackupService() as KeysBackup
+
+ val stateObserver = StateObserver(keysBackup)
+
+ // - Pick a megolm key
+ val session = keysBackup.store.inboundGroupSessionsToBackup(1)[0]
+
+ val keyBackupCreationInfo = prepareAndCreateKeysBackupData(keysBackup).megolmBackupCreationInfo
+
+ // - Check encryptGroupSession() returns stg
+ val keyBackupData = keysBackup.encryptGroupSession(session)
+ assertNotNull(keyBackupData)
+ assertNotNull(keyBackupData.sessionData)
+
+ // - Check pkDecryptionFromRecoveryKey() is able to create a OlmPkDecryption
+ val decryption = keysBackup.pkDecryptionFromRecoveryKey(keyBackupCreationInfo.recoveryKey)
+ assertNotNull(decryption)
+ // - Check decryptKeyBackupData() returns stg
+ val sessionData = keysBackup.decryptKeyBackupData(keyBackupData, session.olmInboundGroupSession!!.sessionIdentifier(), cryptoTestData.roomId, decryption!!)
+ assertNotNull(sessionData)
+ // - Compare the decrypted megolm key with the original one
+ assertKeysEquals(session.exportKeys(), sessionData)
+
+ stateObserver.stopAndCheckStates(null)
+ cryptoTestData.close()
+ }
+
+ /**
+ * - Do an e2e backup to the homeserver with a recovery key
+ * - Log Alice on a new device
+ * - Restore the e2e backup from the homeserver with the recovery key
+ * - Restore must be successful
+ */
+ @Test
+ fun restoreKeysBackupTest() {
+ val testData = createKeysBackupScenarioWithPassword(null)
+
+ // - Restore the e2e backup from the homeserver
+ val latch2 = CountDownLatch(1)
+ var importRoomKeysResult: ImportRoomKeysResult? = null
+ testData.aliceSession2.getKeysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.getKeysBackupService().keysBackupVersion!!,
+ testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey,
+ null,
+ null,
+ null,
+ object : TestMatrixCallback(latch2) {
+ override fun onSuccess(data: ImportRoomKeysResult) {
+ importRoomKeysResult = data
+ super.onSuccess(data)
+ }
+ }
+ )
+ mTestHelper.await(latch2)
+
+ checkRestoreSuccess(testData, importRoomKeysResult!!.totalNumberOfKeys, importRoomKeysResult!!.successfullyNumberOfImportedKeys)
+
+ testData.cryptoTestData.close()
+ }
+
+ /**
+ *
+ * This is the same as `testRestoreKeyBackup` but this test checks that pending key
+ * share requests are cancelled.
+ *
+ * - Do an e2e backup to the homeserver with a recovery key
+ * - Log Alice on a new device
+ * - *** Check the SDK sent key share requests
+ * - Restore the e2e backup from the homeserver with the recovery key
+ * - Restore must be successful
+ * - *** There must be no more pending key share requests
+ */
+ @Test
+ fun restoreKeysBackupAndKeyShareRequestTest() {
+ val testData = createKeysBackupScenarioWithPassword(null)
+
+ // - Check the SDK sent key share requests
+ val cryptoStore2 = (testData.aliceSession2.getKeysBackupService() as KeysBackup).store
+ val unsentRequest = cryptoStore2
+ .getOutgoingRoomKeyRequestByState(setOf(OutgoingRoomKeyRequest.RequestState.UNSENT))
+ val sentRequest = cryptoStore2
+ .getOutgoingRoomKeyRequestByState(setOf(OutgoingRoomKeyRequest.RequestState.SENT))
+
+ // Request is either sent or unsent
+ assertTrue(unsentRequest != null || sentRequest != null)
+
+ // - Restore the e2e backup from the homeserver
+ val latch2 = CountDownLatch(1)
+ var importRoomKeysResult: ImportRoomKeysResult? = null
+ testData.aliceSession2.getKeysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.getKeysBackupService().keysBackupVersion!!,
+ testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey,
+ null,
+ null,
+ null,
+ object : TestMatrixCallback(latch2) {
+ override fun onSuccess(data: ImportRoomKeysResult) {
+ importRoomKeysResult = data
+ super.onSuccess(data)
+ }
+ }
+ )
+ mTestHelper.await(latch2)
+
+ checkRestoreSuccess(testData, importRoomKeysResult!!.totalNumberOfKeys, importRoomKeysResult!!.successfullyNumberOfImportedKeys)
+
+ // - There must be no more pending key share requests
+ val unsentRequestAfterRestoration = cryptoStore2
+ .getOutgoingRoomKeyRequestByState(setOf(OutgoingRoomKeyRequest.RequestState.UNSENT))
+ val sentRequestAfterRestoration = cryptoStore2
+ .getOutgoingRoomKeyRequestByState(setOf(OutgoingRoomKeyRequest.RequestState.SENT))
+
+ // Request is either sent or unsent
+ assertTrue(unsentRequestAfterRestoration == null && sentRequestAfterRestoration == null)
+
+ testData.cryptoTestData.close()
+ }
+
+ /**
+ * - Do an e2e backup to the homeserver with a recovery key
+ * - And log Alice on a new device
+ * - The new device must see the previous backup as not trusted
+ * - Trust the backup from the new device
+ * - Backup must be enabled on the new device
+ * - Retrieve the last version from the server
+ * - It must be the same
+ * - It must be trusted and must have with 2 signatures now
+ */
+ @Test
+ fun trustKeyBackupVersionTest() {
+ // - Do an e2e backup to the homeserver with a recovery key
+ // - And log Alice on a new device
+ val testData = createKeysBackupScenarioWithPassword(null)
+
+ val stateObserver = StateObserver(testData.aliceSession2.getKeysBackupService())
+
+ // - The new device must see the previous backup as not trusted
+ assertNotNull(testData.aliceSession2.getKeysBackupService().keysBackupVersion)
+ assertFalse(testData.aliceSession2.getKeysBackupService().isEnabled)
+ assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.getKeysBackupService().state)
+
+ // - Trust the backup from the new device
+ val latch = CountDownLatch(1)
+ testData.aliceSession2.getKeysBackupService().trustKeysBackupVersion(
+ testData.aliceSession2.getKeysBackupService().keysBackupVersion!!,
+ true,
+ TestMatrixCallback(latch)
+ )
+ mTestHelper.await(latch)
+
+ // Wait for backup state to be ReadyToBackUp
+ waitForKeysBackupToBeInState(testData.aliceSession2, KeysBackupState.ReadyToBackUp)
+
+ // - Backup must be enabled on the new device, on the same version
+ assertEquals(testData.prepareKeysBackupDataResult.version, testData.aliceSession2.getKeysBackupService().keysBackupVersion?.version)
+ assertTrue(testData.aliceSession2.getKeysBackupService().isEnabled)
+
+ // - Retrieve the last version from the server
+ val latch2 = CountDownLatch(1)
+ var keysVersionResult: KeysVersionResult? = null
+ testData.aliceSession2.getKeysBackupService().getCurrentVersion(
+ object : TestMatrixCallback(latch2) {
+ override fun onSuccess(data: KeysVersionResult?) {
+ keysVersionResult = data
+ super.onSuccess(data)
+ }
+ }
+ )
+ mTestHelper.await(latch2)
+
+ // - It must be the same
+ assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version)
+
+ val latch3 = CountDownLatch(1)
+ var keysBackupVersionTrust: KeysBackupVersionTrust? = null
+ testData.aliceSession2.getKeysBackupService().getKeysBackupTrust(keysVersionResult!!,
+ object : TestMatrixCallback(latch3) {
+ override fun onSuccess(data: KeysBackupVersionTrust) {
+ keysBackupVersionTrust = data
+ super.onSuccess(data)
+ }
+ })
+ mTestHelper.await(latch3)
+
+ // - It must be trusted and must have 2 signatures now
+ assertTrue(keysBackupVersionTrust!!.usable)
+ assertEquals(2, keysBackupVersionTrust!!.signatures.size)
+
+ stateObserver.stopAndCheckStates(null)
+ testData.cryptoTestData.close()
+ }
+
+ /**
+ * - Do an e2e backup to the homeserver with a recovery key
+ * - And log Alice on a new device
+ * - The new device must see the previous backup as not trusted
+ * - Trust the backup from the new device with the recovery key
+ * - Backup must be enabled on the new device
+ * - Retrieve the last version from the server
+ * - It must be the same
+ * - It must be trusted and must have with 2 signatures now
+ */
+ @Test
+ fun trustKeyBackupVersionWithRecoveryKeyTest() {
+ // - Do an e2e backup to the homeserver with a recovery key
+ // - And log Alice on a new device
+ val testData = createKeysBackupScenarioWithPassword(null)
+
+ val stateObserver = StateObserver(testData.aliceSession2.getKeysBackupService())
+
+ // - The new device must see the previous backup as not trusted
+ assertNotNull(testData.aliceSession2.getKeysBackupService().keysBackupVersion)
+ assertFalse(testData.aliceSession2.getKeysBackupService().isEnabled)
+ assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.getKeysBackupService().state)
+
+ // - Trust the backup from the new device with the recovery key
+ val latch = CountDownLatch(1)
+ testData.aliceSession2.getKeysBackupService().trustKeysBackupVersionWithRecoveryKey(
+ testData.aliceSession2.getKeysBackupService().keysBackupVersion!!,
+ testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey,
+ TestMatrixCallback(latch)
+ )
+ mTestHelper.await(latch)
+
+ // Wait for backup state to be ReadyToBackUp
+ waitForKeysBackupToBeInState(testData.aliceSession2, KeysBackupState.ReadyToBackUp)
+
+ // - Backup must be enabled on the new device, on the same version
+ assertEquals(testData.prepareKeysBackupDataResult.version, testData.aliceSession2.getKeysBackupService().keysBackupVersion?.version)
+ assertTrue(testData.aliceSession2.getKeysBackupService().isEnabled)
+
+ // - Retrieve the last version from the server
+ val latch2 = CountDownLatch(1)
+ var keysVersionResult: KeysVersionResult? = null
+ testData.aliceSession2.getKeysBackupService().getCurrentVersion(
+ object : TestMatrixCallback(latch2) {
+ override fun onSuccess(data: KeysVersionResult?) {
+ keysVersionResult = data
+ super.onSuccess(data)
+ }
+ }
+ )
+ mTestHelper.await(latch2)
+
+ // - It must be the same
+ assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version)
+
+ val latch3 = CountDownLatch(1)
+ var keysBackupVersionTrust: KeysBackupVersionTrust? = null
+ testData.aliceSession2.getKeysBackupService().getKeysBackupTrust(keysVersionResult!!,
+ object : TestMatrixCallback(latch3) {
+ override fun onSuccess(data: KeysBackupVersionTrust) {
+ keysBackupVersionTrust = data
+ super.onSuccess(data)
+ }
+ })
+ mTestHelper.await(latch3)
+
+ // - It must be trusted and must have 2 signatures now
+ assertTrue(keysBackupVersionTrust!!.usable)
+ assertEquals(2, keysBackupVersionTrust!!.signatures.size)
+
+ stateObserver.stopAndCheckStates(null)
+ testData.cryptoTestData.close()
+ }
+
+ /**
+ * - Do an e2e backup to the homeserver with a recovery key
+ * - And log Alice on a new device
+ * - The new device must see the previous backup as not trusted
+ * - Try to trust the backup from the new device with a wrong recovery key
+ * - It must fail
+ * - The backup must still be untrusted and disabled
+ */
+ @Test
+ fun trustKeyBackupVersionWithWrongRecoveryKeyTest() {
+ // - Do an e2e backup to the homeserver with a recovery key
+ // - And log Alice on a new device
+ val testData = createKeysBackupScenarioWithPassword(null)
+
+ val stateObserver = StateObserver(testData.aliceSession2.getKeysBackupService())
+
+ // - The new device must see the previous backup as not trusted
+ assertNotNull(testData.aliceSession2.getKeysBackupService().keysBackupVersion)
+ assertFalse(testData.aliceSession2.getKeysBackupService().isEnabled)
+ assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.getKeysBackupService().state)
+
+ // - Try to trust the backup from the new device with a wrong recovery key
+ val latch = CountDownLatch(1)
+ testData.aliceSession2.getKeysBackupService().trustKeysBackupVersionWithRecoveryKey(
+ testData.aliceSession2.getKeysBackupService().keysBackupVersion!!,
+ "Bad recovery key",
+ TestMatrixCallback(latch, false)
+ )
+ mTestHelper.await(latch)
+
+ // - The new device must still see the previous backup as not trusted
+ assertNotNull(testData.aliceSession2.getKeysBackupService().keysBackupVersion)
+ assertFalse(testData.aliceSession2.getKeysBackupService().isEnabled)
+ assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.getKeysBackupService().state)
+
+ stateObserver.stopAndCheckStates(null)
+ testData.cryptoTestData.close()
+ }
+
+ /**
+ * - Do an e2e backup to the homeserver with a password
+ * - And log Alice on a new device
+ * - The new device must see the previous backup as not trusted
+ * - Trust the backup from the new device with the password
+ * - Backup must be enabled on the new device
+ * - Retrieve the last version from the server
+ * - It must be the same
+ * - It must be trusted and must have with 2 signatures now
+ */
+ @Test
+ fun trustKeyBackupVersionWithPasswordTest() {
+ val password = "Password"
+
+ // - Do an e2e backup to the homeserver with a password
+ // - And log Alice on a new device
+ val testData = createKeysBackupScenarioWithPassword(password)
+
+ val stateObserver = StateObserver(testData.aliceSession2.getKeysBackupService())
+
+ // - The new device must see the previous backup as not trusted
+ assertNotNull(testData.aliceSession2.getKeysBackupService().keysBackupVersion)
+ assertFalse(testData.aliceSession2.getKeysBackupService().isEnabled)
+ assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.getKeysBackupService().state)
+
+ // - Trust the backup from the new device with the password
+ val latch = CountDownLatch(1)
+ testData.aliceSession2.getKeysBackupService().trustKeysBackupVersionWithPassphrase(
+ testData.aliceSession2.getKeysBackupService().keysBackupVersion!!,
+ password,
+ TestMatrixCallback(latch)
+ )
+ mTestHelper.await(latch)
+
+ // Wait for backup state to be ReadyToBackUp
+ waitForKeysBackupToBeInState(testData.aliceSession2, KeysBackupState.ReadyToBackUp)
+
+ // - Backup must be enabled on the new device, on the same version
+ assertEquals(testData.prepareKeysBackupDataResult.version, testData.aliceSession2.getKeysBackupService().keysBackupVersion?.version)
+ assertTrue(testData.aliceSession2.getKeysBackupService().isEnabled)
+
+ // - Retrieve the last version from the server
+ val latch2 = CountDownLatch(1)
+ var keysVersionResult: KeysVersionResult? = null
+ testData.aliceSession2.getKeysBackupService().getCurrentVersion(
+ object : TestMatrixCallback(latch2) {
+ override fun onSuccess(data: KeysVersionResult?) {
+ keysVersionResult = data
+ super.onSuccess(data)
+ }
+ }
+ )
+ mTestHelper.await(latch2)
+
+ // - It must be the same
+ assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version)
+
+ val latch3 = CountDownLatch(1)
+ var keysBackupVersionTrust: KeysBackupVersionTrust? = null
+ testData.aliceSession2.getKeysBackupService().getKeysBackupTrust(keysVersionResult!!,
+ object : TestMatrixCallback(latch3) {
+ override fun onSuccess(data: KeysBackupVersionTrust) {
+ keysBackupVersionTrust = data
+ super.onSuccess(data)
+ }
+ })
+ mTestHelper.await(latch3)
+
+ // - It must be trusted and must have 2 signatures now
+ assertTrue(keysBackupVersionTrust!!.usable)
+ assertEquals(2, keysBackupVersionTrust!!.signatures.size)
+
+ stateObserver.stopAndCheckStates(null)
+ testData.cryptoTestData.close()
+ }
+
+ /**
+ * - Do an e2e backup to the homeserver with a password
+ * - And log Alice on a new device
+ * - The new device must see the previous backup as not trusted
+ * - Try to trust the backup from the new device with a wrong password
+ * - It must fail
+ * - The backup must still be untrusted and disabled
+ */
+ @Test
+ fun trustKeyBackupVersionWithWrongPasswordTest() {
+ val password = "Password"
+ val badPassword = "Bad Password"
+
+ // - Do an e2e backup to the homeserver with a password
+ // - And log Alice on a new device
+ val testData = createKeysBackupScenarioWithPassword(password)
+
+ val stateObserver = StateObserver(testData.aliceSession2.getKeysBackupService())
+
+ // - The new device must see the previous backup as not trusted
+ assertNotNull(testData.aliceSession2.getKeysBackupService().keysBackupVersion)
+ assertFalse(testData.aliceSession2.getKeysBackupService().isEnabled)
+ assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.getKeysBackupService().state)
+
+ // - Try to trust the backup from the new device with a wrong password
+ val latch = CountDownLatch(1)
+ testData.aliceSession2.getKeysBackupService().trustKeysBackupVersionWithPassphrase(
+ testData.aliceSession2.getKeysBackupService().keysBackupVersion!!,
+ badPassword,
+ TestMatrixCallback(latch, false)
+ )
+ mTestHelper.await(latch)
+
+ // - The new device must still see the previous backup as not trusted
+ assertNotNull(testData.aliceSession2.getKeysBackupService().keysBackupVersion)
+ assertFalse(testData.aliceSession2.getKeysBackupService().isEnabled)
+ assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.getKeysBackupService().state)
+
+ stateObserver.stopAndCheckStates(null)
+ testData.cryptoTestData.close()
+ }
+
+ /**
+ * - Do an e2e backup to the homeserver with a recovery key
+ * - Log Alice on a new device
+ * - Try to restore the e2e backup with a wrong recovery key
+ * - It must fail
+ */
+ @Test
+ fun restoreKeysBackupWithAWrongRecoveryKeyTest() {
+ val testData = createKeysBackupScenarioWithPassword(null)
+
+ // - Try to restore the e2e backup with a wrong recovery key
+ val latch2 = CountDownLatch(1)
+ var importRoomKeysResult: ImportRoomKeysResult? = null
+ testData.aliceSession2.getKeysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.getKeysBackupService().keysBackupVersion!!,
+ "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
+ null,
+ null,
+ null,
+ object : TestMatrixCallback(latch2, false) {
+ override fun onSuccess(data: ImportRoomKeysResult) {
+ importRoomKeysResult = data
+ super.onSuccess(data)
+ }
+ }
+ )
+ mTestHelper.await(latch2)
+
+ // onSuccess may not have been called
+ assertNull(importRoomKeysResult)
+
+ testData.cryptoTestData.close()
+ }
+
+ /**
+ * - Do an e2e backup to the homeserver with a password
+ * - Log Alice on a new device
+ * - Restore the e2e backup with the password
+ * - Restore must be successful
+ */
+ @Test
+ fun testBackupWithPassword() {
+ val password = "password"
+
+ val testData = createKeysBackupScenarioWithPassword(password)
+
+ // - Restore the e2e backup with the password
+ val latch2 = CountDownLatch(1)
+ var importRoomKeysResult: ImportRoomKeysResult? = null
+ val steps = ArrayList()
+
+ testData.aliceSession2.getKeysBackupService().restoreKeyBackupWithPassword(testData.aliceSession2.getKeysBackupService().keysBackupVersion!!,
+ password,
+ null,
+ null,
+ object : StepProgressListener {
+ override fun onStepProgress(step: StepProgressListener.Step) {
+ steps.add(step)
+ }
+ },
+ object : TestMatrixCallback(latch2) {
+ override fun onSuccess(data: ImportRoomKeysResult) {
+ importRoomKeysResult = data
+ super.onSuccess(data)
+ }
+ }
+ )
+ mTestHelper.await(latch2)
+
+ // Check steps
+ assertEquals(105, steps.size)
+
+ for (i in 0..100) {
+ assertTrue(steps[i] is StepProgressListener.Step.ComputingKey)
+ assertEquals(i, (steps[i] as StepProgressListener.Step.ComputingKey).progress)
+ assertEquals(100, (steps[i] as StepProgressListener.Step.ComputingKey).total)
+ }
+
+ assertTrue(steps[101] is StepProgressListener.Step.DownloadingKey)
+
+ // 2 Keys to import, value will be 0%, 50%, 100%
+ for (i in 102..104) {
+ assertTrue(steps[i] is StepProgressListener.Step.ImportingKey)
+ assertEquals(100, (steps[i] as StepProgressListener.Step.ImportingKey).total)
+ }
+
+ assertEquals(0, (steps[102] as StepProgressListener.Step.ImportingKey).progress)
+ assertEquals(50, (steps[103] as StepProgressListener.Step.ImportingKey).progress)
+ assertEquals(100, (steps[104] as StepProgressListener.Step.ImportingKey).progress)
+
+ checkRestoreSuccess(testData, importRoomKeysResult!!.totalNumberOfKeys, importRoomKeysResult!!.successfullyNumberOfImportedKeys)
+
+ testData.cryptoTestData.close()
+ }
+
+ /**
+ * - Do an e2e backup to the homeserver with a password
+ * - Log Alice on a new device
+ * - Try to restore the e2e backup with a wrong password
+ * - It must fail
+ */
+ @Test
+ fun restoreKeysBackupWithAWrongPasswordTest() {
+ val password = "password"
+ val wrongPassword = "passw0rd"
+
+ val testData = createKeysBackupScenarioWithPassword(password)
+
+ // - Try to restore the e2e backup with a wrong password
+ val latch2 = CountDownLatch(1)
+ var importRoomKeysResult: ImportRoomKeysResult? = null
+ testData.aliceSession2.getKeysBackupService().restoreKeyBackupWithPassword(testData.aliceSession2.getKeysBackupService().keysBackupVersion!!,
+ wrongPassword,
+ null,
+ null,
+ null,
+ object : TestMatrixCallback(latch2, false) {
+ override fun onSuccess(data: ImportRoomKeysResult) {
+ importRoomKeysResult = data
+ super.onSuccess(data)
+ }
+ }
+ )
+ mTestHelper.await(latch2)
+
+ // onSuccess may not have been called
+ assertNull(importRoomKeysResult)
+
+ testData.cryptoTestData.close()
+ }
+
+ /**
+ * - Do an e2e backup to the homeserver with a password
+ * - Log Alice on a new device
+ * - Restore the e2e backup with the recovery key.
+ * - Restore must be successful
+ */
+ @Test
+ fun testUseRecoveryKeyToRestoreAPasswordBasedKeysBackup() {
+ val password = "password"
+
+ val testData = createKeysBackupScenarioWithPassword(password)
+
+ // - Restore the e2e backup with the recovery key.
+ val latch2 = CountDownLatch(1)
+ var importRoomKeysResult: ImportRoomKeysResult? = null
+ testData.aliceSession2.getKeysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.getKeysBackupService().keysBackupVersion!!,
+ testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey,
+ null,
+ null,
+ null,
+ object : TestMatrixCallback(latch2) {
+ override fun onSuccess(data: ImportRoomKeysResult) {
+ importRoomKeysResult = data
+ super.onSuccess(data)
+ }
+ }
+ )
+ mTestHelper.await(latch2)
+
+ checkRestoreSuccess(testData, importRoomKeysResult!!.totalNumberOfKeys, importRoomKeysResult!!.successfullyNumberOfImportedKeys)
+
+ testData.cryptoTestData.close()
+ }
+
+ /**
+ * - Do an e2e backup to the homeserver with a recovery key
+ * - And log Alice on a new device
+ * - Try to restore the e2e backup with a password
+ * - It must fail
+ */
+ @Test
+ fun testUsePasswordToRestoreARecoveryKeyBasedKeysBackup() {
+ val testData = createKeysBackupScenarioWithPassword(null)
+
+ // - Try to restore the e2e backup with a password
+ val latch2 = CountDownLatch(1)
+ var importRoomKeysResult: ImportRoomKeysResult? = null
+ testData.aliceSession2.getKeysBackupService().restoreKeyBackupWithPassword(testData.aliceSession2.getKeysBackupService().keysBackupVersion!!,
+ "password",
+ null,
+ null,
+ null,
+ object : TestMatrixCallback(latch2, false) {
+ override fun onSuccess(data: ImportRoomKeysResult) {
+ importRoomKeysResult = data
+ super.onSuccess(data)
+ }
+ }
+ )
+ mTestHelper.await(latch2)
+
+ // onSuccess may not have been called
+ assertNull(importRoomKeysResult)
+
+ testData.cryptoTestData.close()
+ }
+
+ /**
+ * - Create a backup version
+ * - Check the returned KeysVersionResult is trusted
+ */
+ @Test
+ fun testIsKeysBackupTrusted() {
+ // - Create a backup version
+ val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
+
+ val keysBackup = cryptoTestData.firstSession.getKeysBackupService()
+
+ val stateObserver = StateObserver(keysBackup)
+
+ // - Do an e2e backup to the homeserver
+ prepareAndCreateKeysBackupData(keysBackup)
+
+ // Get key backup version from the home server
+ var keysVersionResult: KeysVersionResult? = null
+ val lock = CountDownLatch(1)
+ keysBackup.getCurrentVersion(object : TestMatrixCallback(lock) {
+ override fun onSuccess(data: KeysVersionResult?) {
+ keysVersionResult = data
+ super.onSuccess(data)
+ }
+ })
+ mTestHelper.await(lock)
+
+ assertNotNull(keysVersionResult)
+
+ // - Check the returned KeyBackupVersion is trusted
+ val latch = CountDownLatch(1)
+ var keysBackupVersionTrust: KeysBackupVersionTrust? = null
+ keysBackup.getKeysBackupTrust(keysVersionResult!!, object : MatrixCallback {
+ override fun onSuccess(data: KeysBackupVersionTrust) {
+ keysBackupVersionTrust = data
+ latch.countDown()
+ }
+
+ override fun onFailure(failure: Throwable) {
+ super.onFailure(failure)
+ latch.countDown()
+ }
+ })
+ mTestHelper.await(latch)
+
+ assertNotNull(keysBackupVersionTrust)
+ assertTrue(keysBackupVersionTrust!!.usable)
+ assertEquals(1, keysBackupVersionTrust!!.signatures.size)
+
+ val signature = keysBackupVersionTrust!!.signatures[0]
+ assertTrue(signature.valid)
+ assertNotNull(signature.device)
+ assertEquals(cryptoTestData.firstSession.getMyDevice().deviceId, signature.deviceId)
+ assertEquals(signature.device!!.deviceId, cryptoTestData.firstSession.sessionParams.credentials.deviceId)
+
+ stateObserver.stopAndCheckStates(null)
+ cryptoTestData.close()
+ }
+
+ /**
+ * Check backup starts automatically if there is an existing and compatible backup
+ * version on the homeserver.
+ * - Create a backup version
+ * - Restart alice session
+ * -> The new alice session must back up to the same version
+ */
+ @Test
+ fun testCheckAndStartKeysBackupWhenRestartingAMatrixSession() {
+ // - Create a backup version
+ val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
+
+ val keysBackup = cryptoTestData.firstSession.getKeysBackupService()
+
+ val stateObserver = StateObserver(keysBackup)
+
+ assertFalse(keysBackup.isEnabled)
+
+ val keyBackupCreationInfo = prepareAndCreateKeysBackupData(keysBackup)
+
+ assertTrue(keysBackup.isEnabled)
+
+ // - Restart alice session
+ // - Log Alice on a new device
+ val aliceSession2 = mTestHelper.logIntoAccount(cryptoTestData.firstSession.myUserId, defaultSessionParamsWithInitialSync)
+
+ cryptoTestData.close()
+
+ val keysBackup2 = aliceSession2.getKeysBackupService()
+
+ val stateObserver2 = StateObserver(keysBackup2)
+
+ // -> The new alice session must back up to the same version
+ val latch = CountDownLatch(1)
+ var count = 0
+ keysBackup2.addListener(object : KeysBackupStateListener {
+ override fun onStateChange(newState: KeysBackupState) {
+ // Check the backup completes
+ if (keysBackup.state == KeysBackupState.ReadyToBackUp) {
+ count++
+
+ if (count == 2) {
+ // Remove itself from the list of listeners
+ keysBackup.removeListener(this)
+
+ latch.countDown()
+ }
+ }
+ }
+ })
+ mTestHelper.await(latch)
+
+ assertEquals(keyBackupCreationInfo.version, keysBackup2.currentBackupVersion)
+
+ stateObserver.stopAndCheckStates(null)
+ stateObserver2.stopAndCheckStates(null)
+ aliceSession2.close()
+ }
+
+ /**
+ * Check WrongBackUpVersion state
+ *
+ * - Make alice back up her keys to her homeserver
+ * - Create a new backup with fake data on the homeserver
+ * - Make alice back up all her keys again
+ * -> That must fail and her backup state must be WrongBackUpVersion
+ */
+ @Test
+ fun testBackupWhenAnotherBackupWasCreated() {
+ // - Create a backup version
+ val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
+
+ val keysBackup = cryptoTestData.firstSession.getKeysBackupService()
+
+ val stateObserver = StateObserver(keysBackup)
+
+ assertFalse(keysBackup.isEnabled)
+
+ // Wait for keys backup to be finished
+ val latch0 = CountDownLatch(1)
+ var count = 0
+ keysBackup.addListener(object : KeysBackupStateListener {
+ override fun onStateChange(newState: KeysBackupState) {
+ // Check the backup completes
+ if (newState == KeysBackupState.ReadyToBackUp) {
+ count++
+
+ if (count == 2) {
+ // Remove itself from the list of listeners
+ keysBackup.removeListener(this)
+
+ latch0.countDown()
+ }
+ }
+ }
+ })
+
+ // - Make alice back up her keys to her homeserver
+ prepareAndCreateKeysBackupData(keysBackup)
+
+ assertTrue(keysBackup.isEnabled)
+
+ mTestHelper.await(latch0)
+
+ // - Create a new backup with fake data on the homeserver, directly using the rest client
+ val latch = CountDownLatch(1)
+
+ val megolmBackupCreationInfo = mCryptoTestHelper.createFakeMegolmBackupCreationInfo()
+ (keysBackup as KeysBackup).createFakeKeysBackupVersion(megolmBackupCreationInfo, TestMatrixCallback(latch))
+ mTestHelper.await(latch)
+
+ // Reset the store backup status for keys
+ (cryptoTestData.firstSession.getKeysBackupService() as KeysBackup).store.resetBackupMarkers()
+
+ // - Make alice back up all her keys again
+ val latch2 = CountDownLatch(1)
+ keysBackup.backupAllGroupSessions(object : ProgressListener {
+ override fun onProgress(progress: Int, total: Int) {
+ }
+ }, TestMatrixCallback(latch2, false))
+ mTestHelper.await(latch2)
+
+ // -> That must fail and her backup state must be WrongBackUpVersion
+ assertEquals(KeysBackupState.WrongBackUpVersion, keysBackup.state)
+ assertFalse(keysBackup.isEnabled)
+
+ stateObserver.stopAndCheckStates(null)
+ cryptoTestData.close()
+ }
+
+ /**
+ * - Do an e2e backup to the homeserver
+ * - Log Alice on a new device
+ * - Post a message to have a new megolm session
+ * - Try to backup all
+ * -> It must fail. Backup state must be NotTrusted
+ * - Validate the old device from the new one
+ * -> Backup should automatically enable on the new device
+ * -> It must use the same backup version
+ * - Try to backup all again
+ * -> It must success
+ */
+ @Test
+ fun testBackupAfterVerifyingADevice() {
+ // - Create a backup version
+ val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
+
+ val keysBackup = cryptoTestData.firstSession.getKeysBackupService()
+
+ val stateObserver = StateObserver(keysBackup)
+
+ // - Make alice back up her keys to her homeserver
+ prepareAndCreateKeysBackupData(keysBackup)
+
+ // Wait for keys backup to finish by asking again to backup keys.
+ val latch = CountDownLatch(1)
+ keysBackup.backupAllGroupSessions(object : ProgressListener {
+ override fun onProgress(progress: Int, total: Int) {
+ }
+ }, TestMatrixCallback(latch))
+ mTestHelper.await(latch)
+
+ val oldDeviceId = cryptoTestData.firstSession.sessionParams.credentials.deviceId!!
+ val oldKeyBackupVersion = keysBackup.currentBackupVersion
+ val aliceUserId = cryptoTestData.firstSession.myUserId
+
+ // Close first Alice session, else they will share the same Crypto store and the test fails.
+ cryptoTestData.firstSession.close()
+
+ // - Log Alice on a new device
+ val aliceSession2 = mTestHelper.logIntoAccount(aliceUserId, defaultSessionParamsWithInitialSync)
+
+ // - Post a message to have a new megolm session
+ aliceSession2.setWarnOnUnknownDevices(false)
+
+ val room2 = aliceSession2.getRoom(cryptoTestData.roomId)!!
+
+ mTestHelper.sendTextMessage(room2, "New key", 1)
+
+ // - Try to backup all in aliceSession2, it must fail
+ val keysBackup2 = aliceSession2.getKeysBackupService()
+
+ val stateObserver2 = StateObserver(keysBackup2)
+
+ var isSuccessful = false
+ val latch2 = CountDownLatch(1)
+ keysBackup2.backupAllGroupSessions(object : ProgressListener {
+ override fun onProgress(progress: Int, total: Int) {
+ }
+ }, object : TestMatrixCallback(latch2, false) {
+ override fun onSuccess(data: Unit) {
+ isSuccessful = true
+ super.onSuccess(data)
+ }
+ })
+ mTestHelper.await(latch2)
+
+ assertFalse(isSuccessful)
+
+ // Backup state must be NotTrusted
+ assertEquals(KeysBackupState.NotTrusted, keysBackup2.state)
+ assertFalse(keysBackup2.isEnabled)
+
+ // - Validate the old device from the new one
+ aliceSession2.setDeviceVerification(MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED, oldDeviceId, aliceSession2.myUserId)
+
+ // -> Backup should automatically enable on the new device
+ val latch4 = CountDownLatch(1)
+ keysBackup2.addListener(object : KeysBackupStateListener {
+ override fun onStateChange(newState: KeysBackupState) {
+ // Check the backup completes
+ if (keysBackup2.state == KeysBackupState.ReadyToBackUp) {
+ // Remove itself from the list of listeners
+ keysBackup2.removeListener(this)
+
+ latch4.countDown()
+ }
+ }
+ })
+ mTestHelper.await(latch4)
+
+ // -> It must use the same backup version
+ assertEquals(oldKeyBackupVersion, aliceSession2.getKeysBackupService().currentBackupVersion)
+
+ val latch5 = CountDownLatch(1)
+ aliceSession2.getKeysBackupService().backupAllGroupSessions(null, TestMatrixCallback(latch5))
+ mTestHelper.await(latch5)
+
+ // -> It must success
+ assertTrue(aliceSession2.getKeysBackupService().isEnabled)
+
+ stateObserver.stopAndCheckStates(null)
+ stateObserver2.stopAndCheckStates(null)
+ aliceSession2.close()
+ cryptoTestData.close()
+ }
+
+ /**
+ * - Do an e2e backup to the homeserver with a recovery key
+ * - Delete the backup
+ */
+ @Test
+ fun deleteKeysBackupTest() {
+ // - Create a backup version
+ val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
+
+ val keysBackup = cryptoTestData.firstSession.getKeysBackupService()
+
+ val stateObserver = StateObserver(keysBackup)
+
+ assertFalse(keysBackup.isEnabled)
+
+ val keyBackupCreationInfo = prepareAndCreateKeysBackupData(keysBackup)
+
+ assertTrue(keysBackup.isEnabled)
+
+ val latch = CountDownLatch(1)
+
+ // Delete the backup
+ keysBackup.deleteBackup(keyBackupCreationInfo.version, TestMatrixCallback(latch))
+
+ mTestHelper.await(latch)
+
+ // Backup is now disabled
+ assertFalse(keysBackup.isEnabled)
+
+ stateObserver.stopAndCheckStates(null)
+ cryptoTestData.close()
+ }
+
+ /* ==========================================================================================
+ * Private
+ * ========================================================================================== */
+
+ /**
+ * As KeysBackup is doing asynchronous call to update its internal state, this method help to wait for the
+ * KeysBackup object to be in the specified state
+ */
+ private fun waitForKeysBackupToBeInState(session: Session, state: KeysBackupState) {
+ // If already in the wanted state, return
+ if (session.getKeysBackupService().state == state) {
+ return
+ }
+
+ // Else observe state changes
+ val latch = CountDownLatch(1)
+
+ session.getKeysBackupService().addListener(object : KeysBackupStateListener {
+ override fun onStateChange(newState: KeysBackupState) {
+ if (newState == state) {
+ session.getKeysBackupService().removeListener(this)
+ latch.countDown()
+ }
+ }
+ })
+
+ mTestHelper.await(latch)
+ }
+
+ private data class PrepareKeysBackupDataResult(val megolmBackupCreationInfo: MegolmBackupCreationInfo,
+ val version: String)
+
+ private fun prepareAndCreateKeysBackupData(keysBackup: KeysBackupService,
+ password: String? = null): PrepareKeysBackupDataResult {
+ val stateObserver = StateObserver(keysBackup)
+
+ var megolmBackupCreationInfo: MegolmBackupCreationInfo? = null
+ val latch = CountDownLatch(1)
+ keysBackup.prepareKeysBackupVersion(password, null, object : MatrixCallback {
+ override fun onSuccess(data: MegolmBackupCreationInfo) {
+ megolmBackupCreationInfo = data
+
+ latch.countDown()
+ }
+
+ override fun onFailure(failure: Throwable) {
+ fail(failure.localizedMessage)
+
+ latch.countDown()
+ }
+ })
+ mTestHelper.await(latch)
+
+ assertNotNull(megolmBackupCreationInfo)
+
+ assertFalse(keysBackup.isEnabled)
+
+ val latch2 = CountDownLatch(1)
+
+ // Create the version
+ var version: String? = null
+ keysBackup.createKeysBackupVersion(megolmBackupCreationInfo!!, object : TestMatrixCallback(latch2) {
+ override fun onSuccess(data: KeysVersion) {
+ assertNotNull(data)
+ assertNotNull(data.version)
+
+ version = data.version
+
+ super.onSuccess(data)
+ }
+ })
+ mTestHelper.await(latch2)
+
+ // Backup must be enable now
+ assertTrue(keysBackup.isEnabled)
+ assertNotNull(version)
+
+ stateObserver.stopAndCheckStates(null)
+ return PrepareKeysBackupDataResult(megolmBackupCreationInfo!!, version!!)
+ }
+
+ private fun assertKeysEquals(keys1: MegolmSessionData?, keys2: MegolmSessionData?) {
+ assertNotNull(keys1)
+ assertNotNull(keys2)
+
+ assertEquals(keys1?.algorithm, keys2?.algorithm)
+ assertEquals(keys1?.roomId, keys2?.roomId)
+ // No need to compare the shortcut
+ // assertEquals(keys1?.sender_claimed_ed25519_key, keys2?.sender_claimed_ed25519_key)
+ assertEquals(keys1?.senderKey, keys2?.senderKey)
+ assertEquals(keys1?.sessionId, keys2?.sessionId)
+ assertEquals(keys1?.sessionKey, keys2?.sessionKey)
+
+ assertListEquals(keys1?.forwardingCurve25519KeyChain, keys2?.forwardingCurve25519KeyChain)
+ assertDictEquals(keys1?.senderClaimedKeys, keys2?.senderClaimedKeys)
+ }
+
+ /**
+ * Data class to store result of [createKeysBackupScenarioWithPassword]
+ */
+ private data class KeysBackupScenarioData(val cryptoTestData: CryptoTestData,
+ val aliceKeys: List,
+ val prepareKeysBackupDataResult: PrepareKeysBackupDataResult,
+ val aliceSession2: Session)
+
+ /**
+ * Common initial condition
+ * - Do an e2e backup to the homeserver
+ * - Log Alice on a new device, and wait for its keysBackup object to be ready (in state NotTrusted)
+ *
+ * @param password optional password
+ */
+ private fun createKeysBackupScenarioWithPassword(password: String?): KeysBackupScenarioData {
+ val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
+
+ val cryptoStore = (cryptoTestData.firstSession.getKeysBackupService() as KeysBackup).store
+ val keysBackup = cryptoTestData.firstSession.getKeysBackupService()
+
+ val stateObserver = StateObserver(keysBackup)
+
+ val aliceKeys = cryptoStore.inboundGroupSessionsToBackup(100)
+
+ // - Do an e2e backup to the homeserver
+ val prepareKeysBackupDataResult = prepareAndCreateKeysBackupData(keysBackup, password)
+
+ val latch = CountDownLatch(1)
+ var lastProgress = 0
+ var lastTotal = 0
+ keysBackup.backupAllGroupSessions(object : ProgressListener {
+ override fun onProgress(progress: Int, total: Int) {
+ lastProgress = progress
+ lastTotal = total
+ }
+ }, TestMatrixCallback(latch))
+ mTestHelper.await(latch)
+
+ assertEquals(2, lastProgress)
+ assertEquals(2, lastTotal)
+
+ val aliceUserId = cryptoTestData.firstSession.myUserId
+
+ // Logout first Alice session, else they will share the same Crypto store and some tests may fail.
+ val latch2 = CountDownLatch(1)
+ cryptoTestData.firstSession.signOut(true, TestMatrixCallback(latch2))
+ mTestHelper.await(latch2)
+
+ // - Log Alice on a new device
+ val aliceSession2 = mTestHelper.logIntoAccount(aliceUserId, defaultSessionParamsWithInitialSync)
+
+ // Test check: aliceSession2 has no keys at login
+ assertEquals(0, aliceSession2.inboundGroupSessionsCount(false))
+
+ // Wait for backup state to be NotTrusted
+ waitForKeysBackupToBeInState(aliceSession2, KeysBackupState.NotTrusted)
+
+ stateObserver.stopAndCheckStates(null)
+
+ return KeysBackupScenarioData(cryptoTestData,
+ aliceKeys,
+ prepareKeysBackupDataResult,
+ aliceSession2)
+ }
+
+ /**
+ * Common restore success check after [createKeysBackupScenarioWithPassword]:
+ * - Imported keys number must be correct
+ * - The new device must have the same count of megolm keys
+ * - Alice must have the same keys on both devices
+ */
+ private fun checkRestoreSuccess(testData: KeysBackupScenarioData,
+ total: Int,
+ imported: Int) {
+ // - Imported keys number must be correct
+ assertEquals(testData.aliceKeys.size, total)
+ assertEquals(total, imported)
+
+ // - The new device must have the same count of megolm keys
+ assertEquals(testData.aliceKeys.size, testData.aliceSession2.inboundGroupSessionsCount(false))
+
+ // - Alice must have the same keys on both devices
+ for (aliceKey1 in testData.aliceKeys) {
+ val aliceKey2 = (testData.aliceSession2.getKeysBackupService() as KeysBackup).store
+ .getInboundGroupSession(aliceKey1.olmInboundGroupSession!!.sessionIdentifier(), aliceKey1.senderKey!!)
+ assertNotNull(aliceKey2)
+ assertKeysEquals(aliceKey1.exportKeys(), aliceKey2!!.exportKeys())
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/StateObserver.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/StateObserver.kt
new file mode 100644
index 0000000000..3f2e33d73b
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/StateObserver.kt
@@ -0,0 +1,104 @@
+/*
+ * 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.matrix.android.internal.crypto.keysbackup
+
+import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
+import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
+import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import java.util.concurrent.CountDownLatch
+
+/**
+ * This class observe the state change of a KeysBackup object and provide a method to check the several state change
+ * It checks all state transitions and detected forbidden transition
+ */
+internal class StateObserver(private val keysBackup: KeysBackupService,
+ private val latch: CountDownLatch? = null,
+ private val expectedStateChange: Int = -1) : KeysBackupStateListener {
+
+ private val allowedStateTransitions = listOf(
+ KeysBackupState.BackingUp to KeysBackupState.ReadyToBackUp,
+ KeysBackupState.BackingUp to KeysBackupState.WrongBackUpVersion,
+
+ KeysBackupState.CheckingBackUpOnHomeserver to KeysBackupState.Disabled,
+ KeysBackupState.CheckingBackUpOnHomeserver to KeysBackupState.NotTrusted,
+ KeysBackupState.CheckingBackUpOnHomeserver to KeysBackupState.ReadyToBackUp,
+ KeysBackupState.CheckingBackUpOnHomeserver to KeysBackupState.Unknown,
+ KeysBackupState.CheckingBackUpOnHomeserver to KeysBackupState.WrongBackUpVersion,
+
+ KeysBackupState.Disabled to KeysBackupState.Enabling,
+
+ KeysBackupState.Enabling to KeysBackupState.Disabled,
+ KeysBackupState.Enabling to KeysBackupState.ReadyToBackUp,
+
+ KeysBackupState.NotTrusted to KeysBackupState.CheckingBackUpOnHomeserver,
+ // This transition happens when we trust the device
+ KeysBackupState.NotTrusted to KeysBackupState.ReadyToBackUp,
+
+ KeysBackupState.ReadyToBackUp to KeysBackupState.WillBackUp,
+
+ KeysBackupState.Unknown to KeysBackupState.CheckingBackUpOnHomeserver,
+
+ KeysBackupState.WillBackUp to KeysBackupState.BackingUp,
+
+ KeysBackupState.WrongBackUpVersion to KeysBackupState.CheckingBackUpOnHomeserver,
+
+ // FIXME These transitions are observed during test, and I'm not sure they should occur. Don't have time to investigate now
+ KeysBackupState.ReadyToBackUp to KeysBackupState.BackingUp,
+ KeysBackupState.ReadyToBackUp to KeysBackupState.ReadyToBackUp,
+ KeysBackupState.WillBackUp to KeysBackupState.ReadyToBackUp,
+ KeysBackupState.WillBackUp to KeysBackupState.Unknown
+ )
+
+ private val stateList = ArrayList()
+ private var lastTransitionError: String? = null
+
+ init {
+ keysBackup.addListener(this)
+ }
+
+ // TODO Make expectedStates mandatory to enforce test
+ fun stopAndCheckStates(expectedStates: List?) {
+ keysBackup.removeListener(this)
+
+ expectedStates?.let {
+ assertEquals(it.size, stateList.size)
+
+ for (i in it.indices) {
+ assertEquals("The state $i is not correct. states: " + stateList.joinToString(separator = " "), it[i], stateList[i])
+ }
+ }
+
+ assertNull("states: " + stateList.joinToString(separator = " "), lastTransitionError)
+ }
+
+ override fun onStateChange(newState: KeysBackupState) {
+ stateList.add(newState)
+
+ // Check that state transition is valid
+ if (stateList.size >= 2
+ && !allowedStateTransitions.contains(stateList[stateList.size - 2] to newState)) {
+ // Forbidden transition detected
+ lastTransitionError = "Forbidden transition detected from " + stateList[stateList.size - 2] + " to " + newState
+ }
+
+ if (expectedStateChange == stateList.size) {
+ latch?.countDown()
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/SASTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/SASTest.kt
new file mode 100644
index 0000000000..c05523f009
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/SASTest.kt
@@ -0,0 +1,525 @@
+/*
+ * 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.matrix.android.internal.crypto.verification
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import im.vector.matrix.android.InstrumentedTest
+import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.api.session.crypto.sas.*
+import im.vector.matrix.android.api.session.events.model.Event
+import im.vector.matrix.android.api.session.events.model.toModel
+import im.vector.matrix.android.common.CommonTestHelper
+import im.vector.matrix.android.common.CryptoTestHelper
+import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
+import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
+import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationAccept
+import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationCancel
+import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart
+import org.junit.Assert.*
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import java.util.*
+import java.util.concurrent.CountDownLatch
+
+@RunWith(AndroidJUnit4::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class SASTest : InstrumentedTest {
+ private val mTestHelper = CommonTestHelper(context())
+ private val mCryptoTestHelper = CryptoTestHelper(mTestHelper)
+
+ @Test
+ fun test_aliceStartThenAliceCancel() {
+ val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
+
+ val aliceSession = cryptoTestData.firstSession
+ val bobSession = cryptoTestData.secondSession
+
+ val aliceSasMgr = aliceSession.getSasVerificationService()
+ val bobSasMgr = bobSession!!.getSasVerificationService()
+
+ val bobTxCreatedLatch = CountDownLatch(1)
+ val bobListener = object : SasVerificationService.SasVerificationListener {
+ override fun transactionCreated(tx: SasVerificationTransaction) {}
+
+ override fun transactionUpdated(tx: SasVerificationTransaction) {
+ bobTxCreatedLatch.countDown()
+ }
+
+ override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
+ }
+ bobSasMgr.addListener(bobListener)
+
+ val txID = aliceSasMgr.beginKeyVerificationSAS(bobSession.myUserId, bobSession.getMyDevice().deviceId)
+ assertNotNull("Alice should have a started transaction", txID)
+
+ val aliceKeyTx = aliceSasMgr.getExistingTransaction(bobSession.myUserId, txID!!)
+ assertNotNull("Alice should have a started transaction", aliceKeyTx)
+
+ mTestHelper.await(bobTxCreatedLatch)
+ bobSasMgr.removeListener(bobListener)
+
+ val bobKeyTx = bobSasMgr.getExistingTransaction(aliceSession.myUserId, txID)
+
+ assertNotNull("Bob should have started verif transaction", bobKeyTx)
+ assertTrue(bobKeyTx is SASVerificationTransaction)
+ assertNotNull("Bob should have starting a SAS transaction", bobKeyTx)
+ assertTrue(aliceKeyTx is SASVerificationTransaction)
+ assertEquals("Alice and Bob have same transaction id", aliceKeyTx!!.transactionId, bobKeyTx!!.transactionId)
+
+ val aliceSasTx = aliceKeyTx as SASVerificationTransaction?
+ val bobSasTx = bobKeyTx as SASVerificationTransaction?
+
+ assertEquals("Alice state should be started", SasVerificationTxState.Started, aliceSasTx!!.state)
+ assertEquals("Bob state should be started by alice", SasVerificationTxState.OnStarted, bobSasTx!!.state)
+
+ // Let's cancel from alice side
+ val cancelLatch = CountDownLatch(1)
+
+ val bobListener2 = object : SasVerificationService.SasVerificationListener {
+ override fun transactionCreated(tx: SasVerificationTransaction) {}
+
+ override fun transactionUpdated(tx: SasVerificationTransaction) {
+ if (tx.transactionId == txID) {
+ if ((tx as SASVerificationTransaction).state === SasVerificationTxState.OnCancelled) {
+ cancelLatch.countDown()
+ }
+ }
+ }
+
+ override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
+ }
+ bobSasMgr.addListener(bobListener2)
+
+ aliceSasTx.cancel(CancelCode.User)
+ mTestHelper.await(cancelLatch)
+
+ assertEquals("Should be cancelled on alice side",
+ SasVerificationTxState.Cancelled, aliceSasTx.state)
+ assertEquals("Should be cancelled on bob side",
+ SasVerificationTxState.OnCancelled, bobSasTx.state)
+
+ assertEquals("Should be User cancelled on alice side",
+ CancelCode.User, aliceSasTx.cancelledReason)
+ assertEquals("Should be User cancelled on bob side",
+ CancelCode.User, aliceSasTx.cancelledReason)
+
+ assertNull(bobSasMgr.getExistingTransaction(aliceSession.myUserId, txID))
+ assertNull(aliceSasMgr.getExistingTransaction(bobSession.myUserId, txID))
+
+ cryptoTestData.close()
+ }
+
+ @Test
+ fun test_key_agreement_protocols_must_include_curve25519() {
+ val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
+
+ val bobSession = cryptoTestData.secondSession!!
+
+ val protocols = listOf("meh_dont_know")
+ val tid = "00000000"
+
+ // Bob should receive a cancel
+ var canceledToDeviceEvent: Event? = null
+ val cancelLatch = CountDownLatch(1)
+ // TODO bobSession!!.dataHandler.addListener(object : MXEventListener() {
+ // TODO override fun onToDeviceEvent(event: Event?) {
+ // TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) {
+ // TODO if (event.contentAsJsonObject?.get("transaction_id")?.asString == tid) {
+ // TODO canceledToDeviceEvent = event
+ // TODO cancelLatch.countDown()
+ // TODO }
+ // TODO }
+ // TODO }
+ // TODO })
+
+ val aliceSession = cryptoTestData.firstSession
+ val aliceUserID = aliceSession.myUserId
+ val aliceDevice = aliceSession.getMyDevice().deviceId
+
+ val aliceListener = object : SasVerificationService.SasVerificationListener {
+ override fun transactionCreated(tx: SasVerificationTransaction) {}
+
+ override fun transactionUpdated(tx: SasVerificationTransaction) {
+ if ((tx as IncomingSASVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) {
+ (tx as IncomingSASVerificationTransaction).performAccept()
+ }
+ }
+
+ override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
+ }
+ aliceSession.getSasVerificationService().addListener(aliceListener)
+
+ fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, protocols = protocols)
+
+ mTestHelper.await(cancelLatch)
+
+ val cancelReq = canceledToDeviceEvent!!.content.toModel()!!
+ assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code)
+
+ cryptoTestData.close()
+ }
+
+ @Test
+ fun test_key_agreement_macs_Must_include_hmac_sha256() {
+ val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
+
+ val bobSession = cryptoTestData.secondSession!!
+
+ val mac = listOf("shaBit")
+ val tid = "00000000"
+
+ // Bob should receive a cancel
+ var canceledToDeviceEvent: Event? = null
+ val cancelLatch = CountDownLatch(1)
+ // TODO bobSession!!.dataHandler.addListener(object : MXEventListener() {
+ // TODO override fun onToDeviceEvent(event: Event?) {
+ // TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) {
+ // TODO if (event.contentAsJsonObject?.get("transaction_id")?.asString == tid) {
+ // TODO canceledToDeviceEvent = event
+ // TODO cancelLatch.countDown()
+ // TODO }
+ // TODO }
+ // TODO }
+ // TODO })
+
+ val aliceSession = cryptoTestData.firstSession
+ val aliceUserID = aliceSession.myUserId
+ val aliceDevice = aliceSession.getMyDevice().deviceId
+
+ fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, mac = mac)
+
+ mTestHelper.await(cancelLatch)
+
+ val cancelReq = canceledToDeviceEvent!!.content.toModel()!!
+ assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code)
+
+ cryptoTestData.close()
+ }
+
+ @Test
+ fun test_key_agreement_short_code_include_decimal() {
+ val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
+
+ val bobSession = cryptoTestData.secondSession!!
+
+ val codes = listOf("bin", "foo", "bar")
+ val tid = "00000000"
+
+ // Bob should receive a cancel
+ var canceledToDeviceEvent: Event? = null
+ val cancelLatch = CountDownLatch(1)
+ // TODO bobSession!!.dataHandler.addListener(object : MXEventListener() {
+ // TODO override fun onToDeviceEvent(event: Event?) {
+ // TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) {
+ // TODO if (event.contentAsJsonObject?.get("transaction_id")?.asString == tid) {
+ // TODO canceledToDeviceEvent = event
+ // TODO cancelLatch.countDown()
+ // TODO }
+ // TODO }
+ // TODO }
+ // TODO })
+
+ val aliceSession = cryptoTestData.firstSession
+ val aliceUserID = aliceSession.myUserId
+ val aliceDevice = aliceSession.getMyDevice().deviceId
+
+ fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, codes = codes)
+
+ mTestHelper.await(cancelLatch)
+
+ val cancelReq = canceledToDeviceEvent!!.content.toModel()!!
+ assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code)
+
+ cryptoTestData.close()
+ }
+
+ private fun fakeBobStart(bobSession: Session,
+ aliceUserID: String?,
+ aliceDevice: String?,
+ tid: String,
+ protocols: List = SASVerificationTransaction.KNOWN_AGREEMENT_PROTOCOLS,
+ hashes: List = SASVerificationTransaction.KNOWN_HASHES,
+ mac: List = SASVerificationTransaction.KNOWN_MACS,
+ codes: List = SASVerificationTransaction.KNOWN_SHORT_CODES) {
+ val startMessage = KeyVerificationStart()
+ startMessage.fromDevice = bobSession.getMyDevice().deviceId
+ startMessage.method = KeyVerificationStart.VERIF_METHOD_SAS
+ startMessage.transactionID = tid
+ startMessage.keyAgreementProtocols = protocols
+ startMessage.hashes = hashes
+ startMessage.messageAuthenticationCodes = mac
+ startMessage.shortAuthenticationStrings = codes
+
+ val contentMap = MXUsersDevicesMap()
+ contentMap.setObject(aliceUserID, aliceDevice, startMessage)
+
+ // TODO val sendLatch = CountDownLatch(1)
+ // TODO bobSession.cryptoRestClient.sendToDevice(
+ // TODO EventType.KEY_VERIFICATION_START,
+ // TODO contentMap,
+ // TODO tid,
+ // TODO TestMatrixCallback(sendLatch)
+ // TODO )
+ }
+
+ // 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 cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
+
+ val aliceSession = cryptoTestData.firstSession
+ val bobSession = cryptoTestData.secondSession
+
+ val aliceSasMgr = aliceSession.getSasVerificationService()
+
+ val aliceCreatedLatch = CountDownLatch(2)
+ val aliceCancelledLatch = CountDownLatch(2)
+ val createdTx = ArrayList()
+ val aliceListener = object : SasVerificationService.SasVerificationListener {
+ override fun transactionCreated(tx: SasVerificationTransaction) {
+ createdTx.add(tx as SASVerificationTransaction)
+ aliceCreatedLatch.countDown()
+ }
+
+ override fun transactionUpdated(tx: SasVerificationTransaction) {
+ if ((tx as SASVerificationTransaction).state === SasVerificationTxState.OnCancelled) {
+ aliceCancelledLatch.countDown()
+ }
+ }
+
+ override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
+ }
+ aliceSasMgr.addListener(aliceListener)
+
+ val bobUserId = bobSession!!.myUserId
+ val bobDeviceId = bobSession.getMyDevice().deviceId
+ aliceSasMgr.beginKeyVerificationSAS(bobUserId, bobDeviceId)
+ aliceSasMgr.beginKeyVerificationSAS(bobUserId, bobDeviceId)
+
+ mTestHelper.await(aliceCreatedLatch)
+ mTestHelper.await(aliceCancelledLatch)
+
+ cryptoTestData.close()
+ }
+
+ /**
+ * Test that when alice starts a 'correct' request, bob agrees.
+ */
+ @Test
+ fun test_aliceAndBobAgreement() {
+ val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
+
+ val aliceSession = cryptoTestData.firstSession
+ val bobSession = cryptoTestData.secondSession
+
+ val aliceSasMgr = aliceSession.getSasVerificationService()
+ val bobSasMgr = bobSession!!.getSasVerificationService()
+
+ var accepted: KeyVerificationAccept? = null
+ var startReq: KeyVerificationStart? = null
+
+ val aliceAcceptedLatch = CountDownLatch(1)
+ val aliceListener = object : SasVerificationService.SasVerificationListener {
+ override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
+
+ override fun transactionCreated(tx: SasVerificationTransaction) {}
+
+ override fun transactionUpdated(tx: SasVerificationTransaction) {
+ if ((tx as SASVerificationTransaction).state === SasVerificationTxState.OnAccepted) {
+ val at = tx as SASVerificationTransaction
+ accepted = at.accepted
+ startReq = at.startReq
+ aliceAcceptedLatch.countDown()
+ }
+ }
+ }
+ aliceSasMgr.addListener(aliceListener)
+
+ val bobListener = object : SasVerificationService.SasVerificationListener {
+ override fun transactionCreated(tx: SasVerificationTransaction) {}
+
+ override fun transactionUpdated(tx: SasVerificationTransaction) {
+ if ((tx as IncomingSASVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) {
+ val at = tx as IncomingSASVerificationTransaction
+ at.performAccept()
+ }
+ }
+
+ override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
+ }
+ bobSasMgr.addListener(bobListener)
+
+ val bobUserId = bobSession.myUserId
+ val bobDeviceId = bobSession.getMyDevice().deviceId
+ aliceSasMgr.beginKeyVerificationSAS(bobUserId, bobDeviceId)
+ mTestHelper.await(aliceAcceptedLatch)
+
+ assertTrue("Should have receive a commitment", accepted!!.commitment?.trim()?.isEmpty() == false)
+
+ // check that agreement is valid
+ assertTrue("Agreed Protocol should be Valid", accepted!!.isValid())
+ assertTrue("Agreed Protocol should be known by alice", startReq!!.keyAgreementProtocols!!.contains(accepted!!.keyAgreementProtocol))
+ assertTrue("Hash should be known by alice", startReq!!.hashes!!.contains(accepted!!.hash))
+ assertTrue("Hash should be known by alice", startReq!!.messageAuthenticationCodes!!.contains(accepted!!.messageAuthenticationCode))
+
+ accepted!!.shortAuthenticationStrings?.forEach {
+ assertTrue("all agreed Short Code should be known by alice", startReq!!.shortAuthenticationStrings!!.contains(it))
+ }
+
+ cryptoTestData.close()
+ }
+
+ @Test
+ fun test_aliceAndBobSASCode() {
+ val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
+
+ val aliceSession = cryptoTestData.firstSession
+ val bobSession = cryptoTestData.secondSession
+
+ val aliceSasMgr = aliceSession.getSasVerificationService()
+ val bobSasMgr = bobSession!!.getSasVerificationService()
+
+ val aliceSASLatch = CountDownLatch(1)
+ val aliceListener = object : SasVerificationService.SasVerificationListener {
+ override fun transactionCreated(tx: SasVerificationTransaction) {}
+
+ override fun transactionUpdated(tx: SasVerificationTransaction) {
+ val uxState = (tx as OutgoingSASVerificationRequest).uxState
+ when (uxState) {
+ OutgoingSasVerificationRequest.UxState.SHOW_SAS -> {
+ aliceSASLatch.countDown()
+ }
+ else -> Unit
+ }
+ }
+
+ override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
+ }
+ aliceSasMgr.addListener(aliceListener)
+
+ val bobSASLatch = CountDownLatch(1)
+ val bobListener = object : SasVerificationService.SasVerificationListener {
+ override fun transactionCreated(tx: SasVerificationTransaction) {}
+
+ override fun transactionUpdated(tx: SasVerificationTransaction) {
+ val uxState = (tx as IncomingSASVerificationTransaction).uxState
+ when (uxState) {
+ IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> {
+ tx.performAccept()
+ }
+ else -> Unit
+ }
+ if (uxState === IncomingSasVerificationTransaction.UxState.SHOW_SAS) {
+ bobSASLatch.countDown()
+ }
+ }
+
+ override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
+ }
+ bobSasMgr.addListener(bobListener)
+
+ val bobUserId = bobSession.myUserId
+ val bobDeviceId = bobSession.getMyDevice().deviceId
+ val verificationSAS = aliceSasMgr.beginKeyVerificationSAS(bobUserId, bobDeviceId)
+ mTestHelper.await(aliceSASLatch)
+ mTestHelper.await(bobSASLatch)
+
+ val aliceTx = aliceSasMgr.getExistingTransaction(bobUserId, verificationSAS!!) as SASVerificationTransaction
+ val bobTx = bobSasMgr.getExistingTransaction(aliceSession.myUserId, verificationSAS) as SASVerificationTransaction
+
+ assertEquals("Should have same SAS", aliceTx.getShortCodeRepresentation(SasMode.DECIMAL),
+ bobTx.getShortCodeRepresentation(SasMode.DECIMAL))
+
+ cryptoTestData.close()
+ }
+
+ @Test
+ fun test_happyPath() {
+ val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
+
+ val aliceSession = cryptoTestData.firstSession
+ val bobSession = cryptoTestData.secondSession
+
+ val aliceSasMgr = aliceSession.getSasVerificationService()
+ val bobSasMgr = bobSession!!.getSasVerificationService()
+
+ val aliceSASLatch = CountDownLatch(1)
+ val aliceListener = object : SasVerificationService.SasVerificationListener {
+ override fun transactionCreated(tx: SasVerificationTransaction) {}
+
+ override fun transactionUpdated(tx: SasVerificationTransaction) {
+ val uxState = (tx as OutgoingSASVerificationRequest).uxState
+ when (uxState) {
+ OutgoingSasVerificationRequest.UxState.SHOW_SAS -> {
+ tx.userHasVerifiedShortCode()
+ }
+ OutgoingSasVerificationRequest.UxState.VERIFIED -> {
+ aliceSASLatch.countDown()
+ }
+ else -> Unit
+ }
+ }
+
+ override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
+ }
+ aliceSasMgr.addListener(aliceListener)
+
+ val bobSASLatch = CountDownLatch(1)
+ val bobListener = object : SasVerificationService.SasVerificationListener {
+ override fun transactionCreated(tx: SasVerificationTransaction) {}
+
+ override fun transactionUpdated(tx: SasVerificationTransaction) {
+ val uxState = (tx as IncomingSASVerificationTransaction).uxState
+ when (uxState) {
+ IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> {
+ tx.performAccept()
+ }
+ IncomingSasVerificationTransaction.UxState.SHOW_SAS -> {
+ tx.userHasVerifiedShortCode()
+ }
+ IncomingSasVerificationTransaction.UxState.VERIFIED -> {
+ bobSASLatch.countDown()
+ }
+ else -> Unit
+ }
+ }
+
+ override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
+ }
+ bobSasMgr.addListener(bobListener)
+
+ val bobUserId = bobSession.myUserId
+ val bobDeviceId = bobSession.getMyDevice().deviceId
+ aliceSasMgr.beginKeyVerificationSAS(bobUserId, bobDeviceId)
+ mTestHelper.await(aliceSASLatch)
+ mTestHelper.await(bobSASLatch)
+
+ // Assert that devices are verified
+ val bobDeviceInfoFromAlicePOV: MXDeviceInfo? = aliceSession.getDeviceInfo(bobUserId, bobDeviceId)
+ val aliceDeviceInfoFromBobPOV: MXDeviceInfo? = bobSession.getDeviceInfo(aliceSession.myUserId, aliceSession.getMyDevice().deviceId)
+
+ // latch wait a bit again
+ Thread.sleep(1000)
+
+ 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.close()
+ }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt
index 592086b0ec..3980094175 100644
--- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt
@@ -19,7 +19,10 @@ package im.vector.matrix.android.session.room.timeline
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.InstrumentedTest
-import im.vector.matrix.android.internal.database.helper.*
+import im.vector.matrix.android.api.session.events.model.Event
+import im.vector.matrix.android.internal.database.helper.add
+import im.vector.matrix.android.internal.database.helper.lastStateIndex
+import im.vector.matrix.android.internal.database.helper.merge
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.SessionRealmModule
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
@@ -29,7 +32,6 @@ import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeR
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.kotlin.createObject
-import org.amshove.kluent.shouldBeFalse
import org.amshove.kluent.shouldBeTrue
import org.amshove.kluent.shouldEqual
import org.junit.Before
@@ -146,30 +148,6 @@ internal class ChunkEntityTest : InstrumentedTest {
}
}
- @Test
- fun merge_shouldEventsBeLinked_whenMergingLinkedWithUnlinked() {
- monarchy.runTransactionSync { realm ->
- val chunk1: ChunkEntity = realm.createObject()
- val chunk2: ChunkEntity = realm.createObject()
- chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
- chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = false)
- chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS)
- chunk1.isUnlinked().shouldBeFalse()
- }
- }
-
- @Test
- fun merge_shouldEventsBeUnlinked_whenMergingUnlinkedWithUnlinked() {
- monarchy.runTransactionSync { realm ->
- val chunk1: ChunkEntity = realm.createObject()
- val chunk2: ChunkEntity = realm.createObject()
- chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
- chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
- chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS)
- chunk1.isUnlinked().shouldBeTrue()
- }
- }
-
@Test
fun merge_shouldPrevTokenMerged_whenMergingForwards() {
monarchy.runTransactionSync { realm ->
@@ -177,8 +155,8 @@ internal class ChunkEntityTest : InstrumentedTest {
val chunk2: ChunkEntity = realm.createObject()
val prevToken = "prev_token"
chunk1.prevToken = prevToken
- chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
- chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
+ chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
+ chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk1.merge("roomId", chunk2, PaginationDirection.FORWARDS)
chunk1.prevToken shouldEqual prevToken
}
@@ -191,10 +169,19 @@ internal class ChunkEntityTest : InstrumentedTest {
val chunk2: ChunkEntity = realm.createObject()
val nextToken = "next_token"
chunk1.nextToken = nextToken
- chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
- chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
+ chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
+ chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS)
chunk1.nextToken shouldEqual nextToken
}
}
+
+ private fun ChunkEntity.addAll(roomId: String,
+ events: List,
+ direction: PaginationDirection,
+ stateIndexOffset: Int = 0) {
+ events.forEach { event ->
+ add(roomId, event, direction, stateIndexOffset)
+ }
+ }
}
diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt
index 8a8ee11854..dd4daee9cd 100644
--- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt
+++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt
@@ -16,7 +16,6 @@
package im.vector.matrix.android.session.room.timeline
-import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
@@ -25,12 +24,6 @@ import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.session.room.model.message.MessageType
-import im.vector.matrix.android.internal.database.helper.addAll
-import im.vector.matrix.android.internal.database.helper.addOrUpdate
-import im.vector.matrix.android.internal.database.model.ChunkEntity
-import im.vector.matrix.android.internal.database.model.RoomEntity
-import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
-import io.realm.kotlin.createObject
import kotlin.random.Random
object RoomDataHelper {
@@ -73,19 +66,4 @@ object RoomDataHelper {
val roomMember = RoomMember(Membership.JOIN, "Fake name #${Random.nextLong()}").toContent()
return createFakeEvent(EventType.STATE_ROOM_MEMBER, roomMember)
}
-
- fun fakeInitialSync(monarchy: Monarchy, roomId: String) {
- monarchy.runTransactionSync { realm ->
- val roomEntity = realm.createObject(roomId)
- roomEntity.membership = Membership.JOIN
- val eventList = createFakeListOfEvents(10)
- val chunkEntity = realm.createObject().apply {
- nextToken = null
- prevToken = Random.nextLong(System.currentTimeMillis()).toString()
- isLastForward = true
- }
- chunkEntity.addAll(roomId, eventList, PaginationDirection.FORWARDS)
- roomEntity.addOrUpdate(chunkEntity)
- }
- }
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt
index cf0302166f..72affe24bb 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt
@@ -18,6 +18,7 @@ package im.vector.matrix.android.api.auth.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
+import im.vector.matrix.android.internal.util.md5
/**
* This data class hold credentials user data.
@@ -34,3 +35,7 @@ data class Credentials(
// Optional data that may contain info to override home server and/or identity server
@Json(name = "well_known") val wellKnown: WellKnown? = null
)
+
+internal fun Credentials.sessionId(): String {
+ return (if (deviceId.isNullOrBlank()) userId else "$userId|$deviceId").md5()
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/MatrixSdkExtensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/MatrixSdkExtensions.kt
index 685a522f60..bada3f86a1 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/MatrixSdkExtensions.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/MatrixSdkExtensions.kt
@@ -28,6 +28,12 @@ fun MXDeviceInfo.getFingerprintHumanReadable() = fingerprint()
?.chunked(4)
?.joinToString(separator = " ")
-fun MutableList.sortByLastSeen() {
- sortWith(DatedObjectComparators.descComparator)
+/* ==========================================================================================
+ * DeviceInfo
+ * ========================================================================================== */
+
+fun List.sortByLastSeen(): List {
+ val list = toMutableList()
+ list.sortWith(DatedObjectComparators.descComparator)
+ return list
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/query/QueryStringValue.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/query/QueryStringValue.kt
new file mode 100644
index 0000000000..5d3e76f1d3
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/query/QueryStringValue.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2020 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.matrix.android.api.query
+
+/**
+ * Basic query language. All these cases are mutually exclusive.
+ */
+sealed class QueryStringValue {
+ object NoCondition : QueryStringValue()
+ object IsNull : QueryStringValue()
+ object IsNotNull : QueryStringValue()
+ object IsEmpty : QueryStringValue()
+ object IsNotEmpty : QueryStringValue()
+ data class Equals(val string: String, val case: Case) : QueryStringValue()
+ data class Contains(val string: String, val case: Case) : QueryStringValue()
+
+ enum class Case {
+ SENSITIVE,
+ INSENSITIVE
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt
index ab545dbce6..339e6ac4a8 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt
@@ -73,6 +73,11 @@ interface Session :
val myUserId: String
get() = sessionParams.credentials.userId
+ /**
+ * The sessionId
+ */
+ val sessionId: String
+
/**
* This method allow to open a session. It does start some service on the background.
*/
@@ -107,7 +112,7 @@ interface Session :
* This method allows to listen the sync state.
* @return a [LiveData] of [SyncState].
*/
- fun syncState(): LiveData
+ fun getSyncStateLive(): LiveData
/**
* This methods return true if an initial sync has been processed
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt
index 706f89dfc9..986cbb698b 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt
@@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
+import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
@@ -89,6 +90,8 @@ interface CryptoService {
fun getDevicesList(callback: MatrixCallback)
+ fun getDeviceInfo(deviceId: String, callback: MatrixCallback)
+
fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int
fun isRoomEncrypted(roomId: String): Boolean
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt
index 1939b1f0e0..41548710eb 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt
@@ -25,7 +25,6 @@ object EventType {
const val MESSAGE = "m.room.message"
const val STICKER = "m.sticker"
const val ENCRYPTED = "m.room.encrypted"
- const val ENCRYPTION = "m.room.encryption"
const val FEEDBACK = "m.room.message.feedback"
const val TYPING = "m.typing"
const val REDACTION = "m.room.redaction"
@@ -54,6 +53,7 @@ object EventType {
const val STATE_ROOM_HISTORY_VISIBILITY = "m.room.history_visibility"
const val STATE_ROOM_RELATED_GROUPS = "m.room.related_groups"
const val STATE_ROOM_PINNED_EVENT = "m.room.pinned_events"
+ const val STATE_ROOM_ENCRYPTION = "m.room.encryption"
// Call Events
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupService.kt
index 2d55d0be57..c01e5b5cd8 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupService.kt
@@ -38,9 +38,15 @@ interface GroupService {
*/
fun getGroupSummary(groupId: String): GroupSummary?
+ /**
+ * Get a list of group summaries. This list is a snapshot of the data.
+ * @return the list of [GroupSummary]
+ */
+ fun getGroupSummaries(groupSummaryQueryParams: GroupSummaryQueryParams): List
+
/**
* Get a live list of group summaries. This list is refreshed as soon as the data changes.
* @return the [LiveData] of [GroupSummary]
*/
- fun liveGroupSummaries(): LiveData>
+ fun getGroupSummariesLive(groupSummaryQueryParams: GroupSummaryQueryParams): LiveData>
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupSummaryQueryParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupSummaryQueryParams.kt
new file mode 100644
index 0000000000..702b8c2523
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupSummaryQueryParams.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2020 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.matrix.android.api.session.group
+
+import im.vector.matrix.android.api.query.QueryStringValue
+import im.vector.matrix.android.api.session.room.model.Membership
+
+fun groupSummaryQueryParams(init: (GroupSummaryQueryParams.Builder.() -> Unit) = {}): GroupSummaryQueryParams {
+ return GroupSummaryQueryParams.Builder().apply(init).build()
+}
+
+/**
+ * This class can be used to filter group summaries
+ */
+data class GroupSummaryQueryParams(
+ val displayName: QueryStringValue,
+ val memberships: List
+) {
+
+ class Builder {
+
+ var displayName: QueryStringValue = QueryStringValue.IsNotEmpty
+ var memberships: List = Membership.all()
+
+ fun build() = GroupSummaryQueryParams(
+ displayName = displayName,
+ memberships = memberships
+ )
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/pushers/PushersService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/pushers/PushersService.kt
index d082faa7c7..129bfa3011 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/pushers/PushersService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/pushers/PushersService.kt
@@ -58,7 +58,7 @@ interface PushersService {
const val EVENT_ID_ONLY = "event_id_only"
}
- fun livePushers(): LiveData>
+ fun getPushersLive(): LiveData>
fun pushers() : List
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt
index 90790a6ab0..3221c355e8 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt
@@ -56,5 +56,8 @@ interface Room :
*/
fun getRoomSummaryLive(): LiveData>
+ /**
+ * A current snapshot of [RoomSummary] associated with the room
+ */
fun roomSummary(): RoomSummary?
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt
index fe110b7b9c..9fec605bd9 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt
@@ -60,16 +60,28 @@ interface RoomService {
fun getRoomSummary(roomIdOrAlias: String): RoomSummary?
/**
- * Get a live list of room summaries. This list is refreshed as soon as the data changes.
- * @return the [LiveData] of [RoomSummary]
+ * Get a snapshot list of room summaries.
+ * @return the immutable list of [RoomSummary]
*/
- fun liveRoomSummaries(): LiveData>
+ fun getRoomSummaries(queryParams: RoomSummaryQueryParams): List
+
+ /**
+ * Get a live list of room summaries. This list is refreshed as soon as the data changes.
+ * @return the [LiveData] of List[RoomSummary]
+ */
+ fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams): LiveData>
+
+ /**
+ * Get a snapshot list of Breadcrumbs
+ * @return the immutable list of [RoomSummary]
+ */
+ fun getBreadcrumbs(): List
/**
* Get a live list of Breadcrumbs
* @return the [LiveData] of [RoomSummary]
*/
- fun liveBreadcrumbs(): LiveData>
+ fun getBreadcrumbsLive(): LiveData>
/**
* Inform the Matrix SDK that a room is displayed.
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomSummaryQueryParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomSummaryQueryParams.kt
new file mode 100644
index 0000000000..6983bda225
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomSummaryQueryParams.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2020 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.matrix.android.api.session.room
+
+import im.vector.matrix.android.api.query.QueryStringValue
+import im.vector.matrix.android.api.session.room.model.Membership
+
+fun roomSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): RoomSummaryQueryParams {
+ return RoomSummaryQueryParams.Builder().apply(init).build()
+}
+
+/**
+ * This class can be used to filter room summaries to use with:
+ * [im.vector.matrix.android.api.session.room.Room] and [im.vector.matrix.android.api.session.room.RoomService]
+ */
+data class RoomSummaryQueryParams(
+ val displayName: QueryStringValue,
+ val canonicalAlias: QueryStringValue,
+ val memberships: List
+) {
+
+ class Builder {
+
+ var displayName: QueryStringValue = QueryStringValue.IsNotEmpty
+ var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition
+ var memberships: List = Membership.all()
+
+ fun build() = RoomSummaryQueryParams(
+ displayName = displayName,
+ canonicalAlias = canonicalAlias,
+ memberships = memberships
+ )
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/crypto/RoomCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/crypto/RoomCryptoService.kt
index f8c15fde47..124b2aef17 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/crypto/RoomCryptoService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/crypto/RoomCryptoService.kt
@@ -16,6 +16,8 @@
package im.vector.matrix.android.api.session.room.crypto
+import im.vector.matrix.android.api.MatrixCallback
+
interface RoomCryptoService {
fun isEncrypted(): Boolean
@@ -23,4 +25,6 @@ interface RoomCryptoService {
fun encryptionAlgorithm(): String?
fun shouldEncryptForInvitedMembers(): Boolean
+
+ fun enableEncryptionWithAlgorithm(algorithm: String, callback: MatrixCallback)
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt
index 34af2cf572..6c117d3be7 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt
@@ -41,11 +41,18 @@ interface MembershipService {
fun getRoomMember(userId: String): RoomMember?
/**
- * Return all the roomMembers ids of the room
- *
+ * Return all the roomMembers of the room with params
+ * @param queryParams the params to query for
+ * @return a roomMember list.
+ */
+ fun getRoomMembers(queryParams: RoomMemberQueryParams): List
+
+ /**
+ * Return all the roomMembers of the room filtered by memberships
+ * @param queryParams the params to query for
* @return a [LiveData] of roomMember list.
*/
- fun getRoomMemberIdsLive(): LiveData>
+ fun getRoomMembersLive(queryParams: RoomMemberQueryParams): LiveData>
fun getNumberOfJoinedMembers(): Int
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMemberQueryParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMemberQueryParams.kt
new file mode 100644
index 0000000000..19003632ca
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMemberQueryParams.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2020 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.matrix.android.api.session.room.members
+
+import im.vector.matrix.android.api.query.QueryStringValue
+import im.vector.matrix.android.api.session.room.model.Membership
+
+fun roomMemberQueryParams(init: (RoomMemberQueryParams.Builder.() -> Unit) = {}): RoomMemberQueryParams {
+ return RoomMemberQueryParams.Builder().apply(init).build()
+}
+
+/**
+ * This class can be used to filter room members
+ */
+data class RoomMemberQueryParams(
+ val displayName: QueryStringValue,
+ val memberships: List
+) {
+
+ class Builder {
+
+ var displayName: QueryStringValue = QueryStringValue.IsNotEmpty
+ var memberships: List = Membership.all()
+
+ fun build() = RoomMemberQueryParams(
+ displayName = displayName,
+ memberships = memberships
+ )
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt
index 1894effc7a..7c6a931373 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt
@@ -43,4 +43,14 @@ enum class Membership(val value: String) {
fun isLeft(): Boolean {
return this == KNOCK || this == LEAVE || this == BAN
}
+
+ companion object {
+ fun activeMemberships(): List {
+ return listOf(INVITE, JOIN)
+ }
+
+ fun all(): List {
+ return values().asList()
+ }
+ }
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMember.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMember.kt
index 6a4d8e3c94..994c27be4d 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMember.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMember.kt
@@ -16,23 +16,12 @@
package im.vector.matrix.android.api.session.room.model
-import com.squareup.moshi.Json
-import com.squareup.moshi.JsonClass
-import im.vector.matrix.android.api.session.events.model.UnsignedData
-
/**
- * Class representing the EventType.STATE_ROOM_MEMBER state event content
+ * Class representing a simplified version of EventType.STATE_ROOM_MEMBER state event content
*/
-@JsonClass(generateAdapter = true)
data class RoomMember(
- @Json(name = "membership") val membership: Membership,
- @Json(name = "reason") val reason: String? = null,
- @Json(name = "displayname") val displayName: String? = null,
- @Json(name = "avatar_url") val avatarUrl: String? = null,
- @Json(name = "is_direct") val isDirect: Boolean = false,
- @Json(name = "third_party_invite") val thirdPartyInvite: Invite? = null,
- @Json(name = "unsigned") val unsignedData: UnsignedData? = null
-) {
- val safeReason
- get() = reason?.takeIf { it.isNotBlank() }
-}
+ val membership: Membership,
+ val userId: String,
+ val displayName: String? = null,
+ val avatarUrl: String? = null
+)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMemberContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMemberContent.kt
new file mode 100644
index 0000000000..deeeb8ba52
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMemberContent.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.matrix.android.api.session.room.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import im.vector.matrix.android.api.session.events.model.UnsignedData
+
+/**
+ * Class representing the EventType.STATE_ROOM_MEMBER state event content
+ */
+@JsonClass(generateAdapter = true)
+data class RoomMemberContent(
+ @Json(name = "membership") val membership: Membership,
+ @Json(name = "reason") val reason: String? = null,
+ @Json(name = "displayname") val displayName: String? = null,
+ @Json(name = "avatar_url") val avatarUrl: String? = null,
+ @Json(name = "is_direct") val isDirect: Boolean = false,
+ @Json(name = "third_party_invite") val thirdPartyInvite: Invite? = null,
+ @Json(name = "unsigned") val unsignedData: UnsignedData? = null
+) {
+ val safeReason
+ get() = reason?.takeIf { it.isNotBlank() }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt
index 129c35a17e..c18645ddbd 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt
@@ -41,7 +41,8 @@ data class RoomSummary(
val membership: Membership = Membership.NONE,
val versioningState: VersioningState = VersioningState.NONE,
val readMarkerId: String? = null,
- val userDrafts: List = emptyList()
+ val userDrafts: List = emptyList(),
+ var isEncrypted: Boolean
) {
val isVersioned: Boolean
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt
index bc1e941698..dbdd5b5a34 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt
@@ -125,7 +125,7 @@ class CreateRoomParams {
val contentMap = HashMap()
contentMap["algorithm"] = algorithm
- val algoEvent = Event(type = EventType.ENCRYPTION,
+ val algoEvent = Event(type = EventType.STATE_ROOM_ENCRYPTION,
stateKey = "",
content = contentMap.toContent()
)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt
index 7d8f2f0bc1..31ed4e9986 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt
@@ -108,5 +108,17 @@ interface RelationService {
replyText: CharSequence,
autoMarkdown: Boolean = false): Cancelable?
- fun getEventSummaryLive(eventId: String): LiveData>
+ /**
+ * Get the current EventAnnotationsSummary
+ * @param eventId the eventId to look for EventAnnotationsSummary
+ * @return the EventAnnotationsSummary found
+ */
+ fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary?
+
+ /**
+ * Get a LiveData of EventAnnotationsSummary for the specified eventId
+ * @param eventId the eventId to look for EventAnnotationsSummary
+ * @return the LiveData of EventAnnotationsSummary
+ */
+ fun getEventAnnotationsSummaryLive(eventId: String): LiveData>
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt
index 06f4a9c7ee..3372eb874c 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt
@@ -26,5 +26,10 @@ interface StateService {
*/
fun updateTopic(topic: String, callback: MatrixCallback)
+ /**
+ * Enable encryption of the room
+ */
+ fun enableEncryption(algorithm: String, callback: MatrixCallback)
+
fun getStateEvent(eventType: String): Event?
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/signout/SignOutService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/signout/SignOutService.kt
index 76ca9291ec..3fb086ac45 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/signout/SignOutService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/signout/SignOutService.kt
@@ -40,8 +40,8 @@ interface SignOutService {
/**
* Sign out, and release the session, clear all the session data, including crypto data
- * @param sigOutFromHomeserver true if the sign out request has to be done
+ * @param signOutFromHomeserver true if the sign out request has to be done
*/
- fun signOut(sigOutFromHomeserver: Boolean,
+ fun signOut(signOutFromHomeserver: Boolean,
callback: MatrixCallback): Cancelable
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt
index 2a93a876f6..453400bc99 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt
@@ -50,25 +50,25 @@ interface UserService {
* @param userId the userId to look for.
* @return a LiveData of user with userId
*/
- fun liveUser(userId: String): LiveData>
+ fun getUserLive(userId: String): LiveData>
/**
* Observe a live list of users sorted alphabetically
* @return a Livedata of users
*/
- fun liveUsers(): LiveData>
+ fun getUsersLive(): LiveData>
/**
* Observe a live [PagedList] of users sorted alphabetically. You can filter the users.
* @param filter the filter. It will look into userId and displayName.
* @return a Livedata of users
*/
- fun livePagedUsers(filter: String? = null): LiveData>
+ fun getPagedUsersLive(filter: String? = null): LiveData>
/**
* Get list of ignored users
*/
- fun liveIgnoredUsers(): LiveData>
+ fun getIgnoredUsersLive(): LiveData>
/**
* Ignore users
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt
index 4c8082b77e..d6ef522f41 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt
@@ -18,6 +18,7 @@ package im.vector.matrix.android.api.util
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.session.group.model.GroupSummary
+import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.matrix.android.api.session.user.model.User
@@ -146,3 +147,4 @@ fun GroupSummary.toMatrixItem() = MatrixItem.GroupItem(groupId, displayName, ava
fun RoomSummary.toMatrixItem() = MatrixItem.RoomItem(roomId, displayName, avatarUrl)
fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlias ?: roomId, displayName, avatarUrl)
fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name, avatarUrl)
+fun RoomMember.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/SessionManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/SessionManager.kt
index c813a6813f..918f5f2f55 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/SessionManager.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/SessionManager.kt
@@ -17,6 +17,7 @@
package im.vector.matrix.android.internal
import im.vector.matrix.android.api.auth.data.SessionParams
+import im.vector.matrix.android.api.auth.data.sessionId
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.di.MatrixComponent
@@ -29,10 +30,11 @@ import javax.inject.Inject
internal class SessionManager @Inject constructor(private val matrixComponent: MatrixComponent,
private val sessionParamsStore: SessionParamsStore) {
+ // SessionId -> SessionComponent
private val sessionComponents = HashMap()
- fun getSessionComponent(userId: String): SessionComponent? {
- val sessionParams = sessionParamsStore.get(userId) ?: return null
+ fun getSessionComponent(sessionId: String): SessionComponent? {
+ val sessionParams = sessionParamsStore.get(sessionId) ?: return null
return getOrCreateSessionComponent(sessionParams)
}
@@ -40,17 +42,17 @@ internal class SessionManager @Inject constructor(private val matrixComponent: M
return getOrCreateSessionComponent(sessionParams).session()
}
- fun releaseSession(userId: String) {
- if (sessionComponents.containsKey(userId).not()) {
- throw RuntimeException("You don't have a session for the user $userId")
+ fun releaseSession(sessionId: String) {
+ if (sessionComponents.containsKey(sessionId).not()) {
+ throw RuntimeException("You don't have a session for id $sessionId")
}
- sessionComponents.remove(userId)?.also {
+ sessionComponents.remove(sessionId)?.also {
it.session().close()
}
}
private fun getOrCreateSessionComponent(sessionParams: SessionParams): SessionComponent {
- return sessionComponents.getOrPut(sessionParams.credentials.userId) {
+ return sessionComponents.getOrPut(sessionParams.credentials.sessionId()) {
DaggerSessionComponent
.factory()
.create(matrixComponent, sessionParams)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt
index 93349f4bbc..d5dd7e2959 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt
@@ -45,14 +45,15 @@ import okhttp3.OkHttpClient
import javax.inject.Inject
import javax.net.ssl.HttpsURLConnection
-internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated
- private val okHttpClient: Lazy,
- private val retrofitFactory: RetrofitFactory,
- private val coroutineDispatchers: MatrixCoroutineDispatchers,
- private val sessionParamsStore: SessionParamsStore,
- private val sessionManager: SessionManager,
- private val sessionCreator: SessionCreator,
- private val pendingSessionStore: PendingSessionStore
+internal class DefaultAuthenticationService @Inject constructor(
+ @Unauthenticated
+ private val okHttpClient: Lazy,
+ private val retrofitFactory: RetrofitFactory,
+ private val coroutineDispatchers: MatrixCoroutineDispatchers,
+ private val sessionParamsStore: SessionParamsStore,
+ private val sessionManager: SessionManager,
+ private val sessionCreator: SessionCreator,
+ private val pendingSessionStore: PendingSessionStore
) : AuthenticationService {
private var pendingSessionData: PendingSessionData? = pendingSessionStore.getPendingSessionData()
@@ -112,7 +113,7 @@ internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated
// First check the homeserver version
runCatching {
- executeRequest {
+ executeRequest(null) {
apiCall = authAPI.versions()
}
}
@@ -141,7 +142,7 @@ internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated
val authAPI = buildAuthAPI(homeServerConnectionConfig)
// Ok, try to get the config.json file of a RiotWeb client
- val riotConfig = executeRequest {
+ val riotConfig = executeRequest(null) {
apiCall = authAPI.getRiotConfig()
}
@@ -153,7 +154,7 @@ internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated
val newAuthAPI = buildAuthAPI(newHomeServerConnectionConfig)
- val versions = executeRequest {
+ val versions = executeRequest(null) {
apiCall = newAuthAPI.versions()
}
@@ -167,7 +168,7 @@ internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated
private suspend fun getLoginFlowResult(authAPI: AuthAPI, versions: Versions, homeServerUrl: String): LoginFlowResult {
return if (versions.isSupportedBySdk()) {
// Get the login flow
- val loginFlowResponse = executeRequest {
+ val loginFlowResponse = executeRequest(null) {
apiCall = authAPI.getLoginFlows()
}
LoginFlowResult.Success(loginFlowResponse, versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionParamsStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionParamsStore.kt
index 57c22b0053..f99b95c2b3 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionParamsStore.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionParamsStore.kt
@@ -21,7 +21,7 @@ import im.vector.matrix.android.api.auth.data.SessionParams
internal interface SessionParamsStore {
- fun get(userId: String): SessionParams?
+ fun get(sessionId: String): SessionParams?
fun getLast(): SessionParams?
@@ -29,11 +29,11 @@ internal interface SessionParamsStore {
suspend fun save(sessionParams: SessionParams)
- suspend fun setTokenInvalid(userId: String)
+ suspend fun setTokenInvalid(sessionId: String)
suspend fun updateCredentials(newCredentials: Credentials)
- suspend fun delete(userId: String)
+ suspend fun delete(sessionId: String)
suspend fun deleteAll()
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt
index 83bf7b7822..e5e77cb14a 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt
@@ -16,6 +16,9 @@
package im.vector.matrix.android.internal.auth.db
+import im.vector.matrix.android.api.auth.data.Credentials
+import im.vector.matrix.android.api.auth.data.sessionId
+import im.vector.matrix.android.internal.di.MoshiProvider
import io.realm.DynamicRealm
import io.realm.RealmMigration
import timber.log.Timber
@@ -23,35 +26,59 @@ import timber.log.Timber
internal object AuthRealmMigration : RealmMigration {
// Current schema version
- const val SCHEMA_VERSION = 2L
+ const val SCHEMA_VERSION = 3L
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.d("Migrating Auth Realm from $oldVersion to $newVersion")
- if (oldVersion <= 0) {
- Timber.d("Step 0 -> 1")
- Timber.d("Create PendingSessionEntity")
+ if (oldVersion <= 0) migrateTo1(realm)
+ if (oldVersion <= 1) migrateTo2(realm)
+ if (oldVersion <= 2) migrateTo3(realm)
+ }
- realm.schema.create("PendingSessionEntity")
- .addField(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, String::class.java)
- .setRequired(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, true)
- .addField(PendingSessionEntityFields.CLIENT_SECRET, String::class.java)
- .setRequired(PendingSessionEntityFields.CLIENT_SECRET, true)
- .addField(PendingSessionEntityFields.SEND_ATTEMPT, Integer::class.java)
- .setRequired(PendingSessionEntityFields.SEND_ATTEMPT, true)
- .addField(PendingSessionEntityFields.RESET_PASSWORD_DATA_JSON, String::class.java)
- .addField(PendingSessionEntityFields.CURRENT_SESSION, String::class.java)
- .addField(PendingSessionEntityFields.IS_REGISTRATION_STARTED, Boolean::class.java)
- .addField(PendingSessionEntityFields.CURRENT_THREE_PID_DATA_JSON, String::class.java)
- }
+ private fun migrateTo1(realm: DynamicRealm) {
+ Timber.d("Step 0 -> 1")
+ Timber.d("Create PendingSessionEntity")
- if (oldVersion <= 1) {
- Timber.d("Step 1 -> 2")
- Timber.d("Add boolean isTokenValid in SessionParamsEntity, with value true")
+ realm.schema.create("PendingSessionEntity")
+ .addField(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, String::class.java)
+ .setRequired(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, true)
+ .addField(PendingSessionEntityFields.CLIENT_SECRET, String::class.java)
+ .setRequired(PendingSessionEntityFields.CLIENT_SECRET, true)
+ .addField(PendingSessionEntityFields.SEND_ATTEMPT, Integer::class.java)
+ .setRequired(PendingSessionEntityFields.SEND_ATTEMPT, true)
+ .addField(PendingSessionEntityFields.RESET_PASSWORD_DATA_JSON, String::class.java)
+ .addField(PendingSessionEntityFields.CURRENT_SESSION, String::class.java)
+ .addField(PendingSessionEntityFields.IS_REGISTRATION_STARTED, Boolean::class.java)
+ .addField(PendingSessionEntityFields.CURRENT_THREE_PID_DATA_JSON, String::class.java)
+ }
- realm.schema.get("SessionParamsEntity")
- ?.addField(SessionParamsEntityFields.IS_TOKEN_VALID, Boolean::class.java)
- ?.transform { it.set(SessionParamsEntityFields.IS_TOKEN_VALID, true) }
- }
+ private fun migrateTo2(realm: DynamicRealm) {
+ Timber.d("Step 1 -> 2")
+ Timber.d("Add boolean isTokenValid in SessionParamsEntity, with value true")
+
+ realm.schema.get("SessionParamsEntity")
+ ?.addField(SessionParamsEntityFields.IS_TOKEN_VALID, Boolean::class.java)
+ ?.transform { it.set(SessionParamsEntityFields.IS_TOKEN_VALID, true) }
+ }
+
+ private fun migrateTo3(realm: DynamicRealm) {
+ Timber.d("Step 2 -> 3")
+ Timber.d("Update SessionParamsEntity primary key, to allow several sessions with the same userId")
+
+ realm.schema.get("SessionParamsEntity")
+ ?.removePrimaryKey()
+ ?.addField(SessionParamsEntityFields.SESSION_ID, String::class.java)
+ ?.setRequired(SessionParamsEntityFields.SESSION_ID, true)
+ ?.transform {
+ val credentialsJson = it.getString(SessionParamsEntityFields.CREDENTIALS_JSON)
+
+ val credentials = MoshiProvider.providesMoshi()
+ .adapter(Credentials::class.java)
+ .fromJson(credentialsJson)
+
+ it.set(SessionParamsEntityFields.SESSION_ID, credentials!!.sessionId())
+ }
+ ?.addPrimaryKey(SessionParamsEntityFields.SESSION_ID)
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmSessionParamsStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmSessionParamsStore.kt
index a4774c632a..9491d5737c 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmSessionParamsStore.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmSessionParamsStore.kt
@@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.auth.db
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.SessionParams
+import im.vector.matrix.android.api.auth.data.sessionId
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.database.awaitTransaction
import im.vector.matrix.android.internal.di.AuthDatabase
@@ -42,11 +43,11 @@ internal class RealmSessionParamsStore @Inject constructor(private val mapper: S
}
}
- override fun get(userId: String): SessionParams? {
+ override fun get(sessionId: String): SessionParams? {
return Realm.getInstance(realmConfiguration).use { realm ->
realm
.where(SessionParamsEntity::class.java)
- .equalTo(SessionParamsEntityFields.USER_ID, userId)
+ .equalTo(SessionParamsEntityFields.SESSION_ID, sessionId)
.findAll()
.map { mapper.map(it) }
.firstOrNull()
@@ -76,17 +77,17 @@ internal class RealmSessionParamsStore @Inject constructor(private val mapper: S
}
}
- override suspend fun setTokenInvalid(userId: String) {
+ override suspend fun setTokenInvalid(sessionId: String) {
awaitTransaction(realmConfiguration) { realm ->
val currentSessionParams = realm
.where(SessionParamsEntity::class.java)
- .equalTo(SessionParamsEntityFields.USER_ID, userId)
+ .equalTo(SessionParamsEntityFields.SESSION_ID, sessionId)
.findAll()
.firstOrNull()
if (currentSessionParams == null) {
// Should not happen
- "Session param not found for user $userId"
+ "Session param not found for id $sessionId"
.let { Timber.w(it) }
.also { error(it) }
} else {
@@ -99,14 +100,14 @@ internal class RealmSessionParamsStore @Inject constructor(private val mapper: S
awaitTransaction(realmConfiguration) { realm ->
val currentSessionParams = realm
.where(SessionParamsEntity::class.java)
- .equalTo(SessionParamsEntityFields.USER_ID, newCredentials.userId)
+ .equalTo(SessionParamsEntityFields.SESSION_ID, newCredentials.sessionId())
.findAll()
.map { mapper.map(it) }
.firstOrNull()
if (currentSessionParams == null) {
// Should not happen
- "Session param not found for user ${newCredentials.userId}"
+ "Session param not found for id ${newCredentials.sessionId()}"
.let { Timber.w(it) }
.also { error(it) }
} else {
@@ -123,10 +124,10 @@ internal class RealmSessionParamsStore @Inject constructor(private val mapper: S
}
}
- override suspend fun delete(userId: String) {
+ override suspend fun delete(sessionId: String) {
awaitTransaction(realmConfiguration) {
it.where(SessionParamsEntity::class.java)
- .equalTo(SessionParamsEntityFields.USER_ID, userId)
+ .equalTo(SessionParamsEntityFields.SESSION_ID, sessionId)
.findAll()
.deleteAllFromRealm()
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsEntity.kt
index 92511dccf7..72eed95fcc 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsEntity.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsEntity.kt
@@ -20,7 +20,8 @@ import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
internal open class SessionParamsEntity(
- @PrimaryKey var userId: String = "",
+ @PrimaryKey var sessionId: String = "",
+ var userId: String = "",
var credentialsJson: String = "",
var homeServerConnectionConfigJson: String = "",
// Set to false when the token is invalid and the user has been soft logged out
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt
index 72e8087f3f..ebd50a6924 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt
@@ -20,6 +20,7 @@ import com.squareup.moshi.Moshi
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.auth.data.SessionParams
+import im.vector.matrix.android.api.auth.data.sessionId
import javax.inject.Inject
internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
@@ -49,6 +50,7 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
return null
}
return SessionParamsEntity(
+ sessionParams.credentials.sessionId(),
sessionParams.credentials.userId,
credentialsJson,
homeServerConnectionConfigJson,
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DefaultLoginWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DefaultLoginWizard.kt
index b847773682..4d98ddcf08 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DefaultLoginWizard.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DefaultLoginWizard.kt
@@ -72,7 +72,7 @@ internal class DefaultLoginWizard(
} else {
PasswordLoginParams.userIdentifier(login, password, deviceName)
}
- val credentials = executeRequest {
+ val credentials = executeRequest(null) {
apiCall = authAPI.login(loginParams)
}
@@ -95,7 +95,7 @@ internal class DefaultLoginWizard(
pendingSessionData = pendingSessionData.copy(sendAttempt = pendingSessionData.sendAttempt + 1)
.also { pendingSessionStore.savePendingSessionData(it) }
- val result = executeRequest {
+ val result = executeRequest(null) {
apiCall = authAPI.resetPassword(AddThreePidRegistrationParams.from(param))
}
@@ -120,7 +120,7 @@ internal class DefaultLoginWizard(
resetPasswordData.newPassword
)
- executeRequest {
+ executeRequest(null) {
apiCall = authAPI.resetPasswordMailConfirmed(param)
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegisterAddThreePidTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegisterAddThreePidTask.kt
index 0246075153..c455ccf48c 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegisterAddThreePidTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegisterAddThreePidTask.kt
@@ -29,11 +29,12 @@ internal interface RegisterAddThreePidTask : Task {
)
}
-internal class DefaultRegisterTask(private val authAPI: AuthAPI)
- : RegisterTask {
+internal class DefaultRegisterTask(
+ private val authAPI: AuthAPI
+) : RegisterTask {
override suspend fun execute(params: RegisterTask.Params): Credentials {
try {
- return executeRequest {
+ return executeRequest(null) {
apiCall = authAPI.register(params.registrationParams)
}
} catch (throwable: Throwable) {
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/ValidateCodeTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/ValidateCodeTask.kt
index da75b839a6..30f9aaa705 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/ValidateCodeTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/ValidateCodeTask.kt
@@ -27,11 +27,12 @@ internal interface ValidateCodeTask : Task onRoomEncryptionEvent(roomId, event)
+ event.getClearType() == EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
event.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
}
@@ -157,7 +159,7 @@ internal class DefaultCryptoService @Inject constructor(
fun onLiveEvent(roomId: String, event: Event) {
when {
- event.getClearType() == EventType.ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
+ event.getClearType() == EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
event.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
}
@@ -203,6 +205,14 @@ internal class DefaultCryptoService @Inject constructor(
.executeBy(taskExecutor)
}
+ override fun getDeviceInfo(deviceId: String, callback: MatrixCallback) {
+ getDeviceInfoTask
+ .configureWith(GetDeviceInfoTask.Params(deviceId)) {
+ this.callback = callback
+ }
+ .executeBy(taskExecutor)
+ }
+
override fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int {
return cryptoStore.inboundGroupSessionsCount(onlyBackedUp)
}
@@ -476,7 +486,7 @@ internal class DefaultCryptoService @Inject constructor(
*/
override fun isRoomEncrypted(roomId: String): Boolean {
val encryptionEvent = monarchy.fetchCopied {
- EventEntity.where(it, roomId = roomId, type = EventType.ENCRYPTION).findFirst()
+ EventEntity.where(it, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION).findFirst()
}
return encryptionEvent != null
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/SetDeviceVerificationAction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/SetDeviceVerificationAction.kt
index 3eafa73fab..2d0c77c768 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/SetDeviceVerificationAction.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/SetDeviceVerificationAction.kt
@@ -22,9 +22,10 @@ import im.vector.matrix.android.internal.di.UserId
import timber.log.Timber
import javax.inject.Inject
-internal class SetDeviceVerificationAction @Inject constructor(private val cryptoStore: IMXCryptoStore,
- @UserId private val userId: String,
- private val keysBackup: KeysBackup) {
+internal class SetDeviceVerificationAction @Inject constructor(
+ private val cryptoStore: IMXCryptoStore,
+ @UserId private val userId: String,
+ private val keysBackup: KeysBackup) {
fun handle(verificationStatus: Int, deviceId: String, userId: String) {
val device = cryptoStore.getUserDevice(deviceId, userId)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/api/CryptoApi.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/api/CryptoApi.kt
index f4821f8ef3..b2e880c2f3 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/api/CryptoApi.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/api/CryptoApi.kt
@@ -25,11 +25,18 @@ internal interface CryptoApi {
/**
* Get the devices list
- * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-devices
+ * Doc: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-devices
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices")
fun getDevices(): Call
+ /**
+ * Get the device info by id
+ * Doc: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-devices-deviceid
+ */
+ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices/{deviceId}")
+ fun getDeviceInfo(@Path("deviceId") deviceId: String): Call
+
/**
* Upload device and/or one-time keys.
* Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-upload
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt
index 91b3d6b056..99267ee89c 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt
@@ -1221,7 +1221,7 @@ internal class KeysBackup @Inject constructor(
// Do not stay in KeysBackupState.WrongBackUpVersion but check what is available on the homeserver
checkAndStartKeysBackup()
}
- else ->
+ else ->
// Come back to the ready state so that we will retry on the next received key
keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
}
@@ -1339,6 +1339,31 @@ internal class KeysBackup @Inject constructor(
return sessionBackupData
}
+ /* ==========================================================================================
+ * For test only
+ * ========================================================================================== */
+
+ // Direct access for test only
+ @VisibleForTesting
+ val store
+ get() = cryptoStore
+
+ @VisibleForTesting
+ fun createFakeKeysBackupVersion(keysBackupCreationInfo: MegolmBackupCreationInfo,
+ callback: MatrixCallback) {
+ val createKeysBackupVersionBody = CreateKeysBackupVersionBody()
+ createKeysBackupVersionBody.algorithm = keysBackupCreationInfo.algorithm
+ @Suppress("UNCHECKED_CAST")
+ createKeysBackupVersionBody.authData = MoshiProvider.providesMoshi().adapter(Map::class.java)
+ .fromJson(keysBackupCreationInfo.authData?.toJsonString() ?: "") as JsonDict?
+
+ createKeysBackupVersionTask
+ .configureWith(createKeysBackupVersionBody) {
+ this.callback = callback
+ }
+ .executeBy(taskExecutor)
+ }
+
companion object {
// Maximum delay in ms in {@link maybeBackupKeys}
private const val KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS = 10_000L
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/CreateKeysBackupVersionTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/CreateKeysBackupVersionTask.kt
index 78ae0f7ea6..9b8183bd02 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/CreateKeysBackupVersionTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/CreateKeysBackupVersionTask.kt
@@ -21,15 +21,18 @@ import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.CreateKeys
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
+import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
internal interface CreateKeysBackupVersionTask : Task
-internal class DefaultCreateKeysBackupVersionTask @Inject constructor(private val roomKeysApi: RoomKeysApi)
- : CreateKeysBackupVersionTask {
+internal class DefaultCreateKeysBackupVersionTask @Inject constructor(
+ private val roomKeysApi: RoomKeysApi,
+ private val eventBus: EventBus
+) : CreateKeysBackupVersionTask {
override suspend fun execute(params: CreateKeysBackupVersionBody): KeysVersion {
- return executeRequest {
+ return executeRequest(eventBus) {
apiCall = roomKeysApi.createKeysBackupVersion(params)
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteBackupTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteBackupTask.kt
index 2b1f3df353..9712bb099b 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteBackupTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteBackupTask.kt
@@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.crypto.keysbackup.tasks
import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
+import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
internal interface DeleteBackupTask : Task {
@@ -27,11 +28,13 @@ internal interface DeleteBackupTask : Task {
)
}
-internal class DefaultDeleteBackupTask @Inject constructor(private val roomKeysApi: RoomKeysApi)
- : DeleteBackupTask {
+internal class DefaultDeleteBackupTask @Inject constructor(
+ private val roomKeysApi: RoomKeysApi,
+ private val eventBus: EventBus
+) : DeleteBackupTask {
override suspend fun execute(params: DeleteBackupTask.Params) {
- return executeRequest {
+ return executeRequest(eventBus) {
apiCall = roomKeysApi.deleteBackup(params.version)
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteRoomSessionDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteRoomSessionDataTask.kt
index ccb3645ef3..72173ec7f4 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteRoomSessionDataTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteRoomSessionDataTask.kt
@@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.crypto.keysbackup.tasks
import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
+import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
internal interface DeleteRoomSessionDataTask : Task {
@@ -29,11 +30,13 @@ internal interface DeleteRoomSessionDataTask : Task {
@@ -28,11 +29,13 @@ internal interface DeleteRoomSessionsDataTask : Task {
@@ -27,11 +28,13 @@ internal interface DeleteSessionsDataTask : Task
-internal class DefaultGetKeysBackupLastVersionTask @Inject constructor(private val roomKeysApi: RoomKeysApi)
- : GetKeysBackupLastVersionTask {
+internal class DefaultGetKeysBackupLastVersionTask @Inject constructor(
+ private val roomKeysApi: RoomKeysApi,
+ private val eventBus: EventBus
+) : GetKeysBackupLastVersionTask {
override suspend fun execute(params: Unit): KeysVersionResult {
- return executeRequest {
+ return executeRequest(eventBus) {
apiCall = roomKeysApi.getKeysBackupLastVersion()
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt
index ea0b9b9f3a..70cc7472a9 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt
@@ -20,15 +20,18 @@ import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
+import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
internal interface GetKeysBackupVersionTask : Task
-internal class DefaultGetKeysBackupVersionTask @Inject constructor(private val roomKeysApi: RoomKeysApi)
- : GetKeysBackupVersionTask {
+internal class DefaultGetKeysBackupVersionTask @Inject constructor(
+ private val roomKeysApi: RoomKeysApi,
+ private val eventBus: EventBus
+) : GetKeysBackupVersionTask {
override suspend fun execute(params: String): KeysVersionResult {
- return executeRequest {
+ return executeRequest(eventBus) {
apiCall = roomKeysApi.getKeysBackupVersion(params)
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetRoomSessionDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetRoomSessionDataTask.kt
index a36850ba08..327836ed5f 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetRoomSessionDataTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetRoomSessionDataTask.kt
@@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeyBackupData
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
+import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
internal interface GetRoomSessionDataTask : Task {
@@ -30,11 +31,13 @@ internal interface GetRoomSessionDataTask : Task {
@@ -29,11 +30,13 @@ internal interface GetRoomSessionsDataTask : Task {
@@ -28,11 +29,13 @@ internal interface GetSessionsDataTask : Task {
@@ -32,11 +33,13 @@ internal interface StoreRoomSessionDataTask : Task {
@@ -31,11 +32,13 @@ internal interface StoreRoomSessionsDataTask : Task {
@@ -30,11 +31,13 @@ internal interface StoreSessionsDataTask : Task {
@@ -29,11 +30,13 @@ internal interface UpdateKeysBackupVersionTask : Task {
val body = KeysClaimBody(oneTimeKeys = params.usersDevicesKeyTypesMap.map)
- val keysClaimResponse = executeRequest {
+ val keysClaimResponse = executeRequest(eventBus) {
apiCall = cryptoApi.claimOneTimeKeysForUsersDevices(body)
}
val map = MXUsersDevicesMap()
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceTask.kt
index 0e52c118d9..fbbaa0e0f7 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceTask.kt
@@ -23,6 +23,7 @@ import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceParams
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
+import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
internal interface DeleteDeviceTask : Task {
@@ -31,12 +32,14 @@ internal interface DeleteDeviceTask : Task {
)
}
-internal class DefaultDeleteDeviceTask @Inject constructor(private val cryptoApi: CryptoApi)
- : DeleteDeviceTask {
+internal class DefaultDeleteDeviceTask @Inject constructor(
+ private val cryptoApi: CryptoApi,
+ private val eventBus: EventBus
+) : DeleteDeviceTask {
override suspend fun execute(params: DeleteDeviceTask.Params) {
try {
- executeRequest {
+ executeRequest(eventBus) {
apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams())
}
} catch (throwable: Throwable) {
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt
index eb23f02275..19e0f6efb5 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt
@@ -23,6 +23,7 @@ import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceParams
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
+import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
internal interface DeleteDeviceWithUserPasswordTask : Task {
@@ -33,12 +34,14 @@ internal interface DeleteDeviceWithUserPasswordTask : Task {
@@ -31,8 +32,10 @@ internal interface DownloadKeysForUsersTask : Task() }.orEmpty()
@@ -45,7 +48,7 @@ internal class DefaultDownloadKeysForUsers @Inject constructor(private val crypt
body.token = params.token
}
- return executeRequest {
+ return executeRequest(eventBus) {
apiCall = cryptoApi.downloadKeysForUsers(body)
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetDeviceInfoTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetDeviceInfoTask.kt
new file mode 100644
index 0000000000..9d9513b773
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetDeviceInfoTask.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.matrix.android.internal.crypto.tasks
+
+import im.vector.matrix.android.internal.crypto.api.CryptoApi
+import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
+import im.vector.matrix.android.internal.network.executeRequest
+import im.vector.matrix.android.internal.task.Task
+import org.greenrobot.eventbus.EventBus
+import javax.inject.Inject
+
+internal interface GetDeviceInfoTask : Task {
+ data class Params(val deviceId: String)
+}
+
+internal class DefaultGetDeviceInfoTask @Inject constructor(
+ private val cryptoApi: CryptoApi,
+ private val eventBus: EventBus
+) : GetDeviceInfoTask {
+
+ override suspend fun execute(params: GetDeviceInfoTask.Params): DeviceInfo {
+ return executeRequest(eventBus) {
+ apiCall = cryptoApi.getDeviceInfo(params.deviceId)
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetDevicesTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetDevicesTask.kt
index d6e82adb4e..7a805f6a08 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetDevicesTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetDevicesTask.kt
@@ -20,15 +20,18 @@ import im.vector.matrix.android.internal.crypto.api.CryptoApi
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
+import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
internal interface GetDevicesTask : Task
-internal class DefaultGetDevicesTask @Inject constructor(private val cryptoApi: CryptoApi)
- : GetDevicesTask {
+internal class DefaultGetDevicesTask @Inject constructor(
+ private val cryptoApi: CryptoApi,
+ private val eventBus: EventBus
+) : GetDevicesTask {
override suspend fun execute(params: Unit): DevicesListResponse {
- return executeRequest {
+ return executeRequest(eventBus) {
apiCall = cryptoApi.getDevices()
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetKeyChangesTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetKeyChangesTask.kt
index 42c36bd1e7..84e2c293b9 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetKeyChangesTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetKeyChangesTask.kt
@@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.crypto.api.CryptoApi
import im.vector.matrix.android.internal.crypto.model.rest.KeyChangesResponse
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
+import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
internal interface GetKeyChangesTask : Task {
@@ -31,11 +32,13 @@ internal interface GetKeyChangesTask : Task {
)
}
-internal class DefaultSendToDeviceTask @Inject constructor(private val cryptoApi: CryptoApi)
- : SendToDeviceTask {
+internal class DefaultSendToDeviceTask @Inject constructor(
+ private val cryptoApi: CryptoApi,
+ private val eventBus: EventBus
+) : SendToDeviceTask {
override suspend fun execute(params: SendToDeviceTask.Params) {
val sendToDeviceBody = SendToDeviceBody()
sendToDeviceBody.messages = params.contentMap.map
- return executeRequest {
+ return executeRequest(eventBus) {
apiCall = cryptoApi.sendToDevice(
params.eventType,
params.transactionId ?: Random.nextInt(Integer.MAX_VALUE).toString(),
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SetDeviceNameTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SetDeviceNameTask.kt
index 47f3050b88..74757c5cb3 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SetDeviceNameTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SetDeviceNameTask.kt
@@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.crypto.api.CryptoApi
import im.vector.matrix.android.internal.crypto.model.rest.UpdateDeviceInfoBody
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
+import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
internal interface SetDeviceNameTask : Task {
@@ -31,14 +32,16 @@ internal interface SetDeviceNameTask : Task {
)
}
-internal class DefaultSetDeviceNameTask @Inject constructor(private val cryptoApi: CryptoApi)
- : SetDeviceNameTask {
+internal class DefaultSetDeviceNameTask @Inject constructor(
+ private val cryptoApi: CryptoApi,
+ private val eventBus: EventBus
+) : SetDeviceNameTask {
override suspend fun execute(params: SetDeviceNameTask.Params) {
val body = UpdateDeviceInfoBody(
displayName = params.deviceName
)
- return executeRequest {
+ return executeRequest(eventBus) {
apiCall = cryptoApi.updateDeviceInfo(params.deviceId, body)
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt
index db05f473b1..d8bfe73eda 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt
@@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.convertToUTF8
+import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
internal interface UploadKeysTask : Task {
@@ -36,8 +37,10 @@ internal interface UploadKeysTask : Task(private val realmConfiguration: RealmConfiguration,
- private val realmQueryBuilder: (Realm) -> RealmQuery) {
+internal suspend fun awaitNotEmptyResult(realmConfiguration: RealmConfiguration,
+ timeoutMillis: Long,
+ builder: (Realm) -> RealmQuery) {
+ withTimeout(timeoutMillis) {
+ // Confine Realm interaction to a single thread with Looper.
+ withContext(Dispatchers.Main) {
+ val latch = CompletableDeferred()
- private companion object {
- val QUERY_LATCH_HANDLER = createBackgroundHandler("REALM_QUERY_LATCH")
- }
+ Realm.getInstance(realmConfiguration).use { realm ->
+ val result = builder(realm).findAllAsync()
- @Throws(InterruptedException::class)
- fun await(timeout: Long, timeUnit: TimeUnit) {
- val realmRef = AtomicReference()
- val latch = CountDownLatch(1)
- QUERY_LATCH_HANDLER.post {
- val realm = Realm.getInstance(realmConfiguration)
- realmRef.set(realm)
- val result = realmQueryBuilder(realm).findAllAsync()
- result.addChangeListener(object : RealmChangeListener> {
- override fun onChange(t: RealmResults) {
- if (t.isNotEmpty()) {
- result.removeChangeListener(this)
- latch.countDown()
+ val listener = object : RealmChangeListener> {
+ override fun onChange(it: RealmResults) {
+ if (it.isNotEmpty()) {
+ result.removeChangeListener(this)
+ latch.complete(Unit)
+ }
}
}
- })
- }
- try {
- latch.await(timeout, timeUnit)
- } catch (exception: InterruptedException) {
- throw exception
- } finally {
- QUERY_LATCH_HANDLER.post {
- realmRef.getAndSet(null).close()
+
+ result.addChangeListener(listener)
+ try {
+ latch.await()
+ } catch (e: CancellationException) {
+ result.removeChangeListener(listener)
+ throw e
+ }
}
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt
index bc806a56a4..ddc7f5e8e6 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt
@@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.database
import android.content.Context
import im.vector.matrix.android.internal.database.model.SessionRealmModule
+import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.di.UserCacheDirectory
import im.vector.matrix.android.internal.di.UserMd5
import im.vector.matrix.android.internal.session.SessionModule
@@ -37,13 +38,14 @@ private const val REALM_NAME = "disk_store.realm"
*/
internal class SessionRealmConfigurationFactory @Inject constructor(private val realmKeysUtils: RealmKeysUtils,
@UserCacheDirectory val directory: File,
+ @SessionId val sessionId: String,
@UserMd5 val userMd5: String,
context: Context) {
private val sharedPreferences = context.getSharedPreferences("im.vector.matrix.android.realm", Context.MODE_PRIVATE)
fun create(): RealmConfiguration {
- val shouldClearRealm = sharedPreferences.getBoolean("$REALM_SHOULD_CLEAR_FLAG_$userMd5", false)
+ val shouldClearRealm = sharedPreferences.getBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", false)
if (shouldClearRealm) {
Timber.v("************************************************************")
Timber.v("The realm file session was corrupted and couldn't be loaded.")
@@ -53,14 +55,15 @@ internal class SessionRealmConfigurationFactory @Inject constructor(private val
}
sharedPreferences
.edit()
- .putBoolean("$REALM_SHOULD_CLEAR_FLAG_$userMd5", true)
+ .putBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", true)
.apply()
val realmConfiguration = RealmConfiguration.Builder()
+ .compactOnLaunch()
.directory(directory)
.name(REALM_NAME)
.apply {
- realmKeysUtils.configureEncryption(this, "${SessionModule.DB_ALIAS_PREFIX}$userMd5")
+ realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5))
}
.modules(SessionRealmModule())
.deleteRealmIfMigrationNeeded()
@@ -71,7 +74,7 @@ internal class SessionRealmConfigurationFactory @Inject constructor(private val
Timber.v("Successfully create realm instance")
sharedPreferences
.edit()
- .putBoolean("$REALM_SHOULD_CLEAR_FLAG_$userMd5", false)
+ .putBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", false)
.apply()
}
return realmConfiguration
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt
index f05fa01444..3fa355fe3c 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt
@@ -28,15 +28,7 @@ import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.extensions.assertIsManaged
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import io.realm.Sort
-
-// By default if a chunk is empty we consider it unlinked
-internal fun ChunkEntity.isUnlinked(): Boolean {
- assertIsManaged()
- return timelineEvents.where()
- .equalTo(TimelineEventEntityFields.ROOT.IS_UNLINKED, false)
- .findAll()
- .isEmpty()
-}
+import io.realm.kotlin.createObject
internal fun ChunkEntity.deleteOnCascade() {
assertIsManaged()
@@ -46,11 +38,10 @@ internal fun ChunkEntity.deleteOnCascade() {
internal fun ChunkEntity.merge(roomId: String,
chunkToMerge: ChunkEntity,
- direction: PaginationDirection) {
+ direction: PaginationDirection): List {
assertIsManaged()
- val isChunkToMergeUnlinked = chunkToMerge.isUnlinked()
- val isCurrentChunkUnlinked = this.isUnlinked()
- val isUnlinked = isCurrentChunkUnlinked && isChunkToMergeUnlinked
+ val isChunkToMergeUnlinked = chunkToMerge.isUnlinked
+ val isCurrentChunkUnlinked = isUnlinked
if (isCurrentChunkUnlinked && !isChunkToMergeUnlinked) {
this.timelineEvents.forEach { it.root?.isUnlinked = false }
@@ -65,49 +56,21 @@ internal fun ChunkEntity.merge(roomId: String,
this.isLastBackward = chunkToMerge.isLastBackward
eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING)
}
- val events = eventsToMerge.mapNotNull { it.root?.asDomain() }
- val eventIds = ArrayList()
- events.forEach { event ->
- add(roomId, event, direction, isUnlinked = isUnlinked)
- if (event.eventId != null) {
- eventIds.add(event.eventId)
- }
- }
- updateSenderDataFor(eventIds)
-}
-
-internal fun ChunkEntity.addAll(roomId: String,
- events: List,
- direction: PaginationDirection,
- stateIndexOffset: Int = 0,
- // Set to true for Event retrieved from a Permalink (i.e. not linked to live Chunk)
- isUnlinked: Boolean = false) {
- assertIsManaged()
- val eventIds = ArrayList()
- events.forEach { event ->
- add(roomId, event, direction, stateIndexOffset, isUnlinked)
- if (event.eventId != null) {
- eventIds.add(event.eventId)
- }
- }
- updateSenderDataFor(eventIds)
-}
-
-internal fun ChunkEntity.updateSenderDataFor(eventIds: List) {
- for (eventId in eventIds) {
- val timelineEventEntity = timelineEvents.find(eventId) ?: continue
- timelineEventEntity.updateSenderData()
- }
+ return eventsToMerge
+ .mapNotNull {
+ val event = it.root?.asDomain() ?: return@mapNotNull null
+ add(roomId, event, direction)
+ }
}
internal fun ChunkEntity.add(roomId: String,
event: Event,
direction: PaginationDirection,
- stateIndexOffset: Int = 0,
- isUnlinked: Boolean = false) {
+ stateIndexOffset: Int = 0
+): TimelineEventEntity? {
assertIsManaged()
if (event.eventId != null && timelineEvents.find(event.eventId) != null) {
- return
+ return null
}
var currentDisplayIndex = lastDisplayIndex(direction, 0)
if (direction == PaginationDirection.FORWARDS) {
@@ -129,12 +92,15 @@ internal fun ChunkEntity.add(roomId: String,
}
}
+ val isChunkUnlinked = isUnlinked
val localId = TimelineEventEntity.nextId(realm)
val eventId = event.eventId ?: ""
val senderId = event.senderId ?: ""
val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst()
- ?: ReadReceiptsSummaryEntity(eventId, roomId)
+ ?: realm.createObject(eventId).apply {
+ this.roomId = roomId
+ }
// Update RR for the sender of a new message with a dummy one
@@ -151,13 +117,15 @@ internal fun ChunkEntity.add(roomId: String,
}
}
- val eventEntity = TimelineEventEntity(localId).also {
- it.root = event.toEntity(roomId).apply {
- this.stateIndex = currentStateIndex
- this.isUnlinked = isUnlinked
- this.displayIndex = currentDisplayIndex
- this.sendState = SendState.SYNCED
- }
+ val rootEvent = event.toEntity(roomId).apply {
+ this.stateIndex = currentStateIndex
+ this.displayIndex = currentDisplayIndex
+ this.sendState = SendState.SYNCED
+ this.isUnlinked = isChunkUnlinked
+ }
+ val eventEntity = realm.createObject().also {
+ it.localId = localId
+ it.root = realm.copyToRealm(rootEvent)
it.eventId = eventId
it.roomId = roomId
it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
@@ -165,6 +133,7 @@ internal fun ChunkEntity.add(roomId: String,
}
val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size
timelineEvents.add(position, eventEntity)
+ return eventEntity
}
internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt
index 948af2af96..19c4715faa 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt
@@ -60,7 +60,7 @@ internal fun RoomEntity.addSendingEvent(event: Event) {
this.sendState = SendState.UNSENT
}
val roomMembers = RoomMembers(realm, roomId)
- val myUser = roomMembers.get(senderId)
+ val myUser = roomMembers.getLastRoomMember(senderId)
val localId = TimelineEventEntity.nextId(realm)
val timelineEventEntity = TimelineEventEntity(localId).also {
it.root = eventEntity
@@ -69,7 +69,6 @@ internal fun RoomEntity.addSendingEvent(event: Event) {
it.senderName = myUser?.displayName
it.senderAvatar = myUser?.avatarUrl
it.isUniqueDisplayName = roomMembers.isUniqueDisplayName(myUser?.displayName)
- it.senderMembershipEvent = roomMembers.queryRoomMemberEvent(senderId).findFirst()
}
sendingTimelineEvents.add(0, timelineEventEntity)
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt
index 36ed2f7edf..0bf02aa92f 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt
@@ -16,74 +16,9 @@
package im.vector.matrix.android.internal.database.helper
-import im.vector.matrix.android.api.session.events.model.EventType
-import im.vector.matrix.android.api.session.events.model.toModel
-import im.vector.matrix.android.api.session.room.model.RoomMember
-import im.vector.matrix.android.internal.database.mapper.ContentMapper
-import im.vector.matrix.android.internal.database.model.*
-import im.vector.matrix.android.internal.database.query.next
-import im.vector.matrix.android.internal.database.query.prev
-import im.vector.matrix.android.internal.database.query.where
-import im.vector.matrix.android.internal.extensions.assertIsManaged
-import im.vector.matrix.android.internal.session.room.membership.RoomMembers
+import im.vector.matrix.android.internal.database.model.TimelineEventEntity
+import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
import io.realm.Realm
-import io.realm.RealmList
-import io.realm.RealmQuery
-
-internal fun TimelineEventEntity.updateSenderData() {
- assertIsManaged()
- val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return
- val stateIndex = root?.stateIndex ?: return
- val senderId = root?.sender ?: return
- val chunkEntity = chunk?.firstOrNull() ?: return
- val isUnlinked = chunkEntity.isUnlinked()
- var senderMembershipEvent: EventEntity?
- var senderRoomMemberContent: String?
- var senderRoomMemberPrevContent: String?
- when {
- stateIndex <= 0 -> {
- senderMembershipEvent = chunkEntity.timelineEvents.buildQuery(senderId, isUnlinked).next(from = stateIndex)?.root
- senderRoomMemberContent = senderMembershipEvent?.prevContent
- senderRoomMemberPrevContent = senderMembershipEvent?.content
- }
- else -> {
- senderMembershipEvent = chunkEntity.timelineEvents.buildQuery(senderId, isUnlinked).prev(since = stateIndex)?.root
- senderRoomMemberContent = senderMembershipEvent?.content
- senderRoomMemberPrevContent = senderMembershipEvent?.prevContent
- }
- }
-
- // We fallback to untimelinedStateEvents if we can't find membership events in timeline
- if (senderMembershipEvent == null) {
- senderMembershipEvent = roomEntity.untimelinedStateEvents
- .where()
- .equalTo(EventEntityFields.STATE_KEY, senderId)
- .equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_MEMBER)
- .prev(since = stateIndex)
- senderRoomMemberContent = senderMembershipEvent?.content
- senderRoomMemberPrevContent = senderMembershipEvent?.prevContent
- }
-
- ContentMapper.map(senderRoomMemberContent).toModel()?.also {
- this.senderAvatar = it.avatarUrl
- this.senderName = it.displayName
- this.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName)
- }
-
- // We try to fallback on prev content if we got a room member state events with null fields
- if (root?.type == EventType.STATE_ROOM_MEMBER) {
- ContentMapper.map(senderRoomMemberPrevContent).toModel()?.also {
- if (this.senderAvatar == null && it.avatarUrl != null) {
- this.senderAvatar = it.avatarUrl
- }
- if (this.senderName == null && it.displayName != null) {
- this.senderName = it.displayName
- this.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName)
- }
- }
- }
- this.senderMembershipEvent = senderMembershipEvent
-}
internal fun TimelineEventEntity.Companion.nextId(realm: Realm): Long {
val currentIdNum = realm.where(TimelineEventEntity::class.java).max(TimelineEventEntityFields.LOCAL_ID)
@@ -93,10 +28,3 @@ internal fun TimelineEventEntity.Companion.nextId(realm: Realm): Long {
currentIdNum.toLong() + 1
}
}
-
-private fun RealmList.buildQuery(sender: String, isUnlinked: Boolean): RealmQuery {
- return where()
- .equalTo(TimelineEventEntityFields.ROOT.STATE_KEY, sender)
- .equalTo(TimelineEventEntityFields.ROOT.TYPE, EventType.STATE_ROOM_MEMBER)
- .equalTo(TimelineEventEntityFields.ROOT.IS_UNLINKED, isUnlinked)
-}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventSenderVisitor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventSenderVisitor.kt
new file mode 100644
index 0000000000..983de3a50f
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventSenderVisitor.kt
@@ -0,0 +1,148 @@
+/*
+ * 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.matrix.android.internal.database.helper
+
+import im.vector.matrix.android.api.session.events.model.EventType
+import im.vector.matrix.android.api.session.events.model.toModel
+import im.vector.matrix.android.api.session.room.model.RoomMemberContent
+import im.vector.matrix.android.internal.database.mapper.ContentMapper
+import im.vector.matrix.android.internal.database.model.*
+import im.vector.matrix.android.internal.database.query.next
+import im.vector.matrix.android.internal.database.query.prev
+import im.vector.matrix.android.internal.database.query.where
+import im.vector.matrix.android.internal.extensions.assertIsManaged
+import im.vector.matrix.android.internal.session.SessionScope
+import im.vector.matrix.android.internal.session.room.membership.RoomMembers
+import io.realm.RealmList
+import io.realm.RealmQuery
+import javax.inject.Inject
+
+/**
+ * This is an internal cache to avoid querying all the time the room member events
+ */
+@SessionScope
+internal class TimelineEventSenderVisitor @Inject constructor() {
+
+ internal data class Key(
+ val roomId: String,
+ val stateIndex: Int,
+ val senderId: String
+ )
+
+ internal class Value(
+ var senderAvatar: String? = null,
+ var senderName: String? = null,
+ var isUniqueDisplayName: Boolean = false,
+ var senderMembershipEventId: String? = null
+ )
+
+ private val values = HashMap()
+
+ fun clear() {
+ values.clear()
+ }
+
+ fun clear(roomId: String, senderId: String) {
+ val keysToRemove = values.keys.filter { it.senderId == senderId && it.roomId == roomId }
+ keysToRemove.forEach {
+ values.remove(it)
+ }
+ }
+
+ fun visit(timelineEventEntities: List) = timelineEventEntities.forEach { visit(it) }
+
+ fun visit(timelineEventEntity: TimelineEventEntity) {
+ if (!timelineEventEntity.isValid) {
+ return
+ }
+ val key = Key(
+ roomId = timelineEventEntity.roomId,
+ stateIndex = timelineEventEntity.root?.stateIndex ?: 0,
+ senderId = timelineEventEntity.root?.sender ?: ""
+ )
+ val result = values.getOrPut(key) {
+ timelineEventEntity.computeValue()
+ }
+ timelineEventEntity.apply {
+ this.isUniqueDisplayName = result.isUniqueDisplayName
+ this.senderAvatar = result.senderAvatar
+ this.senderName = result.senderName
+ this.senderMembershipEventId = result.senderMembershipEventId
+ }
+ }
+
+ private fun RealmList.buildQuery(sender: String, isUnlinked: Boolean): RealmQuery {
+ return where()
+ .equalTo(TimelineEventEntityFields.ROOT.STATE_KEY, sender)
+ .equalTo(TimelineEventEntityFields.ROOT.TYPE, EventType.STATE_ROOM_MEMBER)
+ .equalTo(TimelineEventEntityFields.ROOT.IS_UNLINKED, isUnlinked)
+ }
+
+ private fun TimelineEventEntity.computeValue(): Value {
+ assertIsManaged()
+ val result = Value()
+ val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return result
+ val stateIndex = root?.stateIndex ?: return result
+ val senderId = root?.sender ?: return result
+ val chunkEntity = chunk?.firstOrNull() ?: return result
+ val isUnlinked = chunkEntity.isUnlinked
+ var senderMembershipEvent: EventEntity?
+ var senderRoomMemberContent: String?
+ var senderRoomMemberPrevContent: String?
+
+ if (stateIndex <= 0) {
+ senderMembershipEvent = chunkEntity.timelineEvents.buildQuery(senderId, isUnlinked).next(from = stateIndex)?.root
+ senderRoomMemberContent = senderMembershipEvent?.prevContent
+ senderRoomMemberPrevContent = senderMembershipEvent?.content
+ } else {
+ senderMembershipEvent = chunkEntity.timelineEvents.buildQuery(senderId, isUnlinked).prev(since = stateIndex)?.root
+ senderRoomMemberContent = senderMembershipEvent?.content
+ senderRoomMemberPrevContent = senderMembershipEvent?.prevContent
+ }
+
+ // We fallback to untimelinedStateEvents if we can't find membership events in timeline
+ if (senderMembershipEvent == null) {
+ senderMembershipEvent = roomEntity.untimelinedStateEvents
+ .where()
+ .equalTo(EventEntityFields.STATE_KEY, senderId)
+ .equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_MEMBER)
+ .prev(since = stateIndex)
+ senderRoomMemberContent = senderMembershipEvent?.content
+ senderRoomMemberPrevContent = senderMembershipEvent?.prevContent
+ }
+
+ ContentMapper.map(senderRoomMemberContent).toModel()?.also {
+ result.senderAvatar = it.avatarUrl
+ result.senderName = it.displayName
+ result.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName)
+ }
+ // We try to fallback on prev content if we got a room member state events with null fields
+ if (root?.type == EventType.STATE_ROOM_MEMBER) {
+ ContentMapper.map(senderRoomMemberPrevContent).toModel()?.also {
+ if (result.senderAvatar == null && it.avatarUrl != null) {
+ result.senderAvatar = it.avatarUrl
+ }
+ if (result.senderName == null && it.displayName != null) {
+ result.senderName = it.displayName
+ result.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName)
+ }
+ }
+ }
+ result.senderMembershipEventId = senderMembershipEvent?.eventId
+ return result
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomMemberMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomMemberMapper.kt
new file mode 100644
index 0000000000..a458c5e506
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomMemberMapper.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.matrix.android.internal.database.mapper
+
+import im.vector.matrix.android.api.session.room.model.RoomMember
+import im.vector.matrix.android.internal.database.model.RoomMemberEntity
+
+internal object RoomMemberMapper {
+
+ fun map(roomMemberEntity: RoomMemberEntity): RoomMember {
+ return RoomMember(
+ userId = roomMemberEntity.userId,
+ avatarUrl = roomMemberEntity.avatarUrl,
+ displayName = roomMemberEntity.displayName,
+ membership = roomMemberEntity.membership
+ )
+ }
+}
+
+internal fun RoomMemberEntity.asDomain(): RoomMember {
+ return RoomMemberMapper.map(this)
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt
index eeb340eacb..7d25a846ff 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt
@@ -70,7 +70,8 @@ internal class RoomSummaryMapper @Inject constructor(
readMarkerId = roomSummaryEntity.readMarkerId,
userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList(),
canonicalAlias = roomSummaryEntity.canonicalAlias,
- aliases = roomSummaryEntity.aliases.toList()
+ aliases = roomSummaryEntity.aliases.toList(),
+ isEncrypted = roomSummaryEntity.isEncrypted
)
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt
index 577c391b3a..94d4a9043f 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt
@@ -30,7 +30,8 @@ internal open class ChunkEntity(@Index var prevToken: String? = null,
var backwardsDisplayIndex: Int? = null,
var forwardsDisplayIndex: Int? = null,
var backwardsStateIndex: Int? = null,
- var forwardsStateIndex: Int? = null
+ var forwardsStateIndex: Int? = null,
+ var isUnlinked: Boolean = false
) : RealmObject() {
fun identifier() = "${prevToken}_$nextToken"
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomMemberEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomMemberEntity.kt
new file mode 100644
index 0000000000..c532857fe1
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomMemberEntity.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.matrix.android.internal.database.model
+
+import im.vector.matrix.android.api.session.room.model.Membership
+import io.realm.RealmObject
+import io.realm.annotations.Index
+import io.realm.annotations.PrimaryKey
+
+internal open class RoomMemberEntity(@PrimaryKey var primaryKey: String = "",
+ @Index var userId: String = "",
+ @Index var roomId: String = "",
+ var displayName: String = "",
+ var avatarUrl: String = "",
+ var reason: String? = null,
+ var isDirect: Boolean = false
+) : RealmObject() {
+
+ private var membershipStr: String = Membership.NONE.name
+ var membership: Membership
+ get() {
+ return Membership.valueOf(membershipStr)
+ }
+ set(value) {
+ membershipStr = value.name
+ }
+
+ companion object
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt
index 406c8700b6..4c99832b39 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt
@@ -42,7 +42,9 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
var breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS,
var canonicalAlias: String? = null,
var aliases: RealmList = RealmList(),
- var flatAliases: String = ""
+ // this is required for querying
+ var flatAliases: String = "",
+ var isEncrypted: Boolean = false
) : RealmObject() {
private var membershipStr: String = Membership.NONE.name
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt
index 4a93819027..752f2a6c23 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt
@@ -50,6 +50,7 @@ import io.realm.annotations.RealmModule
ReadMarkerEntity::class,
UserDraftsEntity::class,
DraftEntity::class,
- HomeServerCapabilitiesEntity::class
+ HomeServerCapabilitiesEntity::class,
+ RoomMemberEntity::class
])
internal class SessionRealmModule
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt
index 235910b1ea..22f4b9c506 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt
@@ -29,7 +29,7 @@ internal open class TimelineEventEntity(var localId: Long = 0,
var senderName: String? = null,
var isUniqueDisplayName: Boolean = false,
var senderAvatar: String? = null,
- var senderMembershipEvent: EventEntity? = null,
+ var senderMembershipEventId: String? = null,
var readReceipts: ReadReceiptsSummaryEntity? = null
) : RealmObject() {
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt
index 69402ac1de..b8c058e667 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt
@@ -57,9 +57,15 @@ internal fun ChunkEntity.Companion.findIncludingEvent(realm: Realm, eventId: Str
return findAllIncludingEvents(realm, listOf(eventId)).firstOrNull()
}
-internal fun ChunkEntity.Companion.create(realm: Realm, prevToken: String?, nextToken: String?): ChunkEntity {
+internal fun ChunkEntity.Companion.create(
+ realm: Realm,
+ prevToken: String?,
+ nextToken: String?,
+ isUnlinked: Boolean
+): ChunkEntity {
return realm.createObject().apply {
this.prevToken = prevToken
this.nextToken = nextToken
+ this.isUnlinked = isUnlinked
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomMemberEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomMemberEntityQueries.kt
new file mode 100644
index 0000000000..2ddade0048
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomMemberEntityQueries.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.matrix.android.internal.database.query
+
+import im.vector.matrix.android.internal.database.model.RoomMemberEntity
+import im.vector.matrix.android.internal.database.model.RoomMemberEntityFields
+import io.realm.Realm
+import io.realm.RealmQuery
+import io.realm.kotlin.where
+
+internal fun RoomMemberEntity.Companion.where(realm: Realm, roomId: String, userId: String? = null): RealmQuery {
+ val query = realm
+ .where()
+ .equalTo(RoomMemberEntityFields.ROOM_ID, roomId)
+
+ if (userId != null) {
+ query.equalTo(RoomMemberEntityFields.USER_ID, userId)
+ }
+ return query
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt
index 3bd035c0b1..221e8ccb46 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt
@@ -54,7 +54,7 @@ internal fun TimelineEventEntity.Companion.where(realm: Realm,
internal fun TimelineEventEntity.Companion.findWithSenderMembershipEvent(realm: Realm, senderMembershipEventId: String): List {
return realm.where()
- .equalTo(TimelineEventEntityFields.SENDER_MEMBERSHIP_EVENT.EVENT_ID, senderMembershipEventId)
+ .equalTo(TimelineEventEntityFields.SENDER_MEMBERSHIP_EVENT_ID, senderMembershipEventId)
.findAll()
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt
index 3444a8fa70..b084ba012d 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt
@@ -38,3 +38,10 @@ internal annotation class DeviceId
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
internal annotation class UserMd5
+
+/**
+ * Used to inject the sessionId, which is defined as md5(userId|deviceId)
+ */
+@Qualifier
+@Retention(AnnotationRetention.RUNTIME)
+internal annotation class SessionId
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/AccessTokenInterceptor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/AccessTokenInterceptor.kt
index e0257bfc83..c802d4b63a 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/AccessTokenInterceptor.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/AccessTokenInterceptor.kt
@@ -17,13 +17,13 @@
package im.vector.matrix.android.internal.network
import im.vector.matrix.android.internal.auth.SessionParamsStore
-import im.vector.matrix.android.internal.di.UserId
+import im.vector.matrix.android.internal.di.SessionId
import okhttp3.Interceptor
import okhttp3.Response
import javax.inject.Inject
internal class AccessTokenInterceptor @Inject constructor(
- @UserId private val userId: String,
+ @SessionId private val sessionId: String,
private val sessionParamsStore: SessionParamsStore) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
@@ -40,5 +40,5 @@ internal class AccessTokenInterceptor @Inject constructor(
}
private val accessToken
- get() = sessionParamsStore.get(userId)?.credentials?.accessToken
+ get() = sessionParamsStore.get(sessionId)?.credentials?.accessToken
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt
index 7f8e6643c3..074a97662b 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt
@@ -18,12 +18,14 @@ package im.vector.matrix.android.internal.network
import im.vector.matrix.android.api.failure.Failure
import kotlinx.coroutines.CancellationException
+import org.greenrobot.eventbus.EventBus
import retrofit2.Call
import java.io.IOException
-internal suspend inline fun executeRequest(block: Request.() -> Unit) = Request().apply(block).execute()
+internal suspend inline fun executeRequest(eventBus: EventBus?,
+ block: Request.() -> Unit) = Request(eventBus).apply(block).execute()
-internal class Request {
+internal class Request(private val eventBus: EventBus?) {
lateinit var apiCall: Call
@@ -34,7 +36,7 @@ internal class Request {
response.body()
?: throw IllegalStateException("The request returned a null body")
} else {
- throw response.toFailure()
+ throw response.toFailure(eventBus)
}
} catch (exception: Throwable) {
throw when (exception) {
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt
index e95c161491..2a2076db6c 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt
@@ -73,18 +73,18 @@ internal suspend fun okhttp3.Call.awaitResponse(): okhttp3.Response {
/**
* Convert a retrofit Response to a Failure, and eventually parse errorBody to convert it to a MatrixError
*/
-internal fun Response.toFailure(): Failure {
- return toFailure(errorBody(), code())
+internal fun Response.toFailure(eventBus: EventBus?): Failure {
+ return toFailure(errorBody(), code(), eventBus)
}
/**
* Convert a okhttp3 Response to a Failure, and eventually parse errorBody to convert it to a MatrixError
*/
-internal fun okhttp3.Response.toFailure(): Failure {
- return toFailure(body, code)
+internal fun okhttp3.Response.toFailure(eventBus: EventBus?): Failure {
+ return toFailure(body, code, eventBus)
}
-private fun toFailure(errorBody: ResponseBody?, httpCode: Int): Failure {
+private fun toFailure(errorBody: ResponseBody?, httpCode: Int, eventBus: EventBus?): Failure {
if (errorBody == null) {
return Failure.Unknown(RuntimeException("errorBody should not be null"))
}
@@ -99,11 +99,11 @@ private fun toFailure(errorBody: ResponseBody?, httpCode: Int): Failure {
if (matrixError != null) {
if (matrixError.code == MatrixError.M_CONSENT_NOT_GIVEN && !matrixError.consentUri.isNullOrBlank()) {
// Also send this error to the bus, for a global management
- EventBus.getDefault().post(GlobalError.ConsentNotGivenError(matrixError.consentUri))
+ eventBus?.post(GlobalError.ConsentNotGivenError(matrixError.consentUri))
} else if (httpCode == HttpURLConnection.HTTP_UNAUTHORIZED /* 401 */
&& matrixError.code == MatrixError.M_UNKNOWN_TOKEN) {
// Also send this error to the bus, for a global management
- EventBus.getDefault().post(GlobalError.InvalidToken(matrixError.isSoftLogout))
+ eventBus?.post(GlobalError.InvalidToken(matrixError.isSoftLogout))
}
return Failure.ServerError(matrixError, httpCode)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryEnumListProcessor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryEnumListProcessor.kt
new file mode 100644
index 0000000000..2bc05eacec
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryEnumListProcessor.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2020 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.matrix.android.internal.query
+
+import io.realm.RealmObject
+import io.realm.RealmQuery
+
+fun > RealmQuery.process(field: String, enums: List>): RealmQuery {
+ val lastEnumValue = enums.lastOrNull()
+ beginGroup()
+ for (enumValue in enums) {
+ equalTo(field, enumValue.name)
+ if (enumValue != lastEnumValue) {
+ or()
+ }
+ }
+ endGroup()
+ return this
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryStringValueProcessor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryStringValueProcessor.kt
new file mode 100644
index 0000000000..ebe10cad9c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryStringValueProcessor.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2020 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.matrix.android.internal.query
+
+import im.vector.matrix.android.api.query.QueryStringValue
+import io.realm.Case
+import io.realm.RealmObject
+import io.realm.RealmQuery
+import timber.log.Timber
+
+fun RealmQuery.process(field: String, queryStringValue: QueryStringValue): RealmQuery {
+ when (queryStringValue) {
+ is QueryStringValue.NoCondition -> Timber.v("No condition to process")
+ is QueryStringValue.IsNotNull -> isNotNull(field)
+ is QueryStringValue.IsNull -> isNull(field)
+ is QueryStringValue.IsEmpty -> isEmpty(field)
+ is QueryStringValue.IsNotEmpty -> isNotEmpty(field)
+ is QueryStringValue.Equals -> equalTo(field, queryStringValue.string, queryStringValue.case.toRealmCase())
+ is QueryStringValue.Contains -> contains(field, queryStringValue.string, queryStringValue.case.toRealmCase())
+ }
+ return this
+}
+
+private fun QueryStringValue.Case.toRealmCase(): Case {
+ return when (this) {
+ QueryStringValue.Case.INSENSITIVE -> Case.INSENSITIVE
+ QueryStringValue.Case.SENSITIVE -> Case.SENSITIVE
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt
index c160ac9b31..66b94cf68d 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt
@@ -25,7 +25,7 @@ import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
-import im.vector.matrix.android.internal.di.UserMd5
+import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.md5
@@ -42,7 +42,7 @@ import java.io.IOException
import javax.inject.Inject
internal class DefaultFileService @Inject constructor(private val context: Context,
- @UserMd5 private val userMd5: String,
+ @SessionId private val sessionId: String,
private val contentUrlResolver: ContentUrlResolver,
private val coroutineDispatchers: MatrixCoroutineDispatchers) : FileService {
@@ -103,9 +103,9 @@ internal class DefaultFileService @Inject constructor(private val context: Conte
return when (downloadMode) {
FileService.DownloadMode.FOR_INTERNAL_USE -> {
// Create dir tree (MF stands for Matrix File):
- // /MF///
+ // /MF/