diff --git a/.editorconfig b/.editorconfig index 19a95806d3..0a49eadc0b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,7 +12,7 @@ max_line_length=off # Comma-separated list of rules to disable (Since 0.34.0) # Note that rules in any ruleset other than the standard ruleset will need to be prefixed # by the ruleset identifier. -disabled_rules=no-wildcard-imports,no-multi-spaces,colon-spacing,chain-wrapping,import-ordering,experimental:annotation +disabled_rules=no-multi-spaces,colon-spacing,chain-wrapping,import-ordering,experimental:annotation # The following (so far identified) rules are kept: # no-blank-line-before-rbrace @@ -30,3 +30,4 @@ disabled_rules=no-wildcard-imports,no-multi-spaces,colon-spacing,chain-wrapping, # no-empty-class-body # experimental:multiline-if-else # experimental:no-empty-first-line-in-method-block +# no-wildcard-imports diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml index 680a1d57cf..f351ec8bfd 100644 --- a/.idea/dictionaries/bmarty.xml +++ b/.idea/dictionaries/bmarty.xml @@ -22,6 +22,7 @@ signin signout signup + ssss threepid diff --git a/CHANGES.md b/CHANGES.md index 5b7c04d3ac..95f35e3e1d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,36 @@ +Changes in RiotX 0.17.0 (2020-02-27) +=================================================== + +Features ✨: + - Secured Shared Storage Support (#984, #936) + - It's now possible to select several rooms (with a possible mix of clear/encrypted rooms) when sharing elements to RiotX (#1010) + - Media preview: media are previewed before being sent to a room (#1010) + - Image edition: it's now possible to edit image before sending: crop, rotate, and delete actions are supported (#1010) + - Sending image: image are sent to rooms with a reduced size. It's still possible to send original image file (#1010) + +Improvements 🙌: + - Migrate to binary QR code verification (#994) + - Share action is added to room profile and room member profile (#858) + - Display avatar in fullscreen (#861) + - Fix some performance issues with crypto + +Bugfix 🐛: + - Account creation: wrongly hints that an email can be used to create an account (#941) + - Fix crash in the room directory, when public room has no name (#1023) + - Fix restoring keys backup with passphrase (#526) + - Fix rotation of full-size image (#647) + - Fix joining rooms from directory via federation isn't working. (#808) + - Leaving a room creates a stuck "leaving room" loading screen. (#1041) + - Fix some invitation handling issues (#1013) + - New direct chat: selecting a participant sometimes results in two breadcrumbs (#1022) + - New direct chat: selecting several participants was not adding the room to the direct chats list + - Room overview shows deleted messages as “Encrypted message” (#758) + +SDK API changes ⚠️: + - Get crypto methods through Session.cryptoService() + - ProgressListener.onProgress() function will be invoked on the background thread instead of UI thread + - Improve CreateRoomParams API (#1070) + Changes in RiotX 0.16.0 (2020-02-14) =================================================== diff --git a/build.gradle b/build.gradle index a2fac55175..74a62f0d17 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,10 @@ allprojects { includeGroupByRegex "com\\.github\\.jaiselrahman" // And monarchy includeGroupByRegex "com\\.github\\.Zhuinden" + // And ucrop + includeGroupByRegex "com\\.github\\.yalantis" + // JsonViewer + includeGroupByRegex 'com\\.github\\.BillCarsonFr' } } maven { 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 2cd2bf2dd3..87ff6f0390 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 @@ -31,6 +31,7 @@ import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.toOptional import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent import io.reactivex.Observable import io.reactivex.Single @@ -94,10 +95,10 @@ class RxSession(private val session: Session) { session.searchUsersDirectory(search, limit, excludedUserIds, it) } - fun joinRoom(roomId: String, + fun joinRoom(roomIdOrAlias: String, reason: String? = null, viaServers: List = emptyList()): Single = singleBuilder { - session.joinRoom(roomId, reason, viaServers, it) + session.joinRoom(roomIdOrAlias, reason, viaServers, it) } fun getRoomIdByAlias(roomAlias: String, @@ -110,15 +111,22 @@ class RxSession(private val session: Session) { } fun liveUserCryptoDevices(userId: String): Observable> { - return session.getLiveCryptoDeviceInfo(userId).asObservable().startWithCallable { - session.getCryptoDeviceInfo(userId) + return session.cryptoService().getLiveCryptoDeviceInfo(userId).asObservable().startWithCallable { + session.cryptoService().getCryptoDeviceInfo(userId) } } fun liveCrossSigningInfo(userId: String): Observable> { - return session.getCrossSigningService().getLiveCrossSigningKeys(userId).asObservable() + return session.cryptoService().crossSigningService().getLiveCrossSigningKeys(userId).asObservable() .startWithCallable { - session.getCrossSigningService().getUserCrossSigningKeys(userId).toOptional() + session.cryptoService().crossSigningService().getUserCrossSigningKeys(userId).toOptional() + } + } + + fun liveAccountData(types: Set): Observable> { + return session.getLiveAccountDataEvents(types).asObservable() + .startWithCallable { + session.getAccountDataEvents(types) } } } diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index ea27df8ab6..062b590acf 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -119,6 +119,7 @@ dependencies { // Image implementation 'androidx.exifinterface:exifinterface:1.1.0' + implementation 'id.zelory:compressor:3.0.0' // Database implementation 'com.github.Zhuinden:realm-monarchy:0.5.1' 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 index c44ac9c47b..679ea8f3fe 100644 --- 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 @@ -38,9 +38,7 @@ class AccountCreationTest : InstrumentedTest { fun createAccountTest() { val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true)) - commonTestHelper.signout(session) - - session.close() + commonTestHelper.signOutAndClose(session) } @Test @@ -50,14 +48,14 @@ class AccountCreationTest : InstrumentedTest { // Log again to the same account val session2 = commonTestHelper.logIntoAccount(session.myUserId, SessionTestParams(withInitialSync = true)) - session.close() - session2.close() + commonTestHelper.signOutAndClose(session) + commonTestHelper.signOutAndClose(session2) } @Test fun simpleE2eTest() { val res = cryptoTestHelper.doE2ETestWithAliceInARoom() - res.close() + res.cleanUp(commonTestHelper) } } 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 index 2e18133071..386787b882 100644 --- 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 @@ -28,7 +28,10 @@ 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.events.model.LocalEcho +import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.Room +import im.vector.matrix.android.api.session.room.model.message.MessageContent 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 @@ -113,7 +116,7 @@ class CommonTestHelper(context: Context) { fun sendTextMessage(room: Room, message: String, nbOfMessages: Int): List { val sentEvents = ArrayList(nbOfMessages) val latch = CountDownLatch(nbOfMessages) - val onEventSentListener = object : Timeline.Listener { + val timelineListener = object : Timeline.Listener { override fun onTimelineFailure(throwable: Throwable) { } @@ -122,20 +125,26 @@ class CommonTestHelper(context: Context) { } 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 }) + val newMessages = snapshot + .filter { LocalEcho.isLocalEchoId(it.eventId).not() } + .filter { it.root.getClearType() == EventType.MESSAGE } + .filter { it.root.getClearContent().toModel()?.body?.startsWith(message) == true } + + if (newMessages.size == nbOfMessages) { + sentEvents.addAll(newMessages) latch.countDown() } } } val timeline = room.createTimeline(null, TimelineSettings(10)) - timeline.addListener(onEventSentListener) + timeline.start() + timeline.addListener(timelineListener) for (i in 0 until nbOfMessages) { room.sendTextMessage(message + " #" + (i + 1)) } await(latch) - timeline.removeListener(onEventSentListener) + timeline.removeListener(timelineListener) + timeline.dispose() // Check that all events has been created assertEquals(nbOfMessages.toLong(), sentEvents.size.toLong()) @@ -283,11 +292,10 @@ class CommonTestHelper(context: Context) { /** * Clear all provided sessions */ - fun Iterable.close() = forEach { it.close() } + fun Iterable.signOutAndClose() = forEach { signOutAndClose(it) } - fun signout(session: Session) { - val lock = CountDownLatch(1) - session.signOut(true, TestMatrixCallback(lock)) - await(lock) + fun signOutAndClose(session: Session) { + doSync { session.signOut(true, it) } + session.close() } } 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 index 8ad9f1ec6f..7eea832160 100644 --- 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 @@ -23,9 +23,9 @@ data class CryptoTestData(val firstSession: Session, val secondSession: Session? = null, val thirdSession: Session? = null) { - fun close() { - firstSession.close() - secondSession?.close() - secondSession?.close() + fun cleanUp(testHelper: CommonTestHelper) { + testHelper.signOutAndClose(firstSession) + secondSession?.let { testHelper.signOutAndClose(it) } + thirdSession?.let { testHelper.signOutAndClose(it) } } } 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 index f94c8455c2..826c70a63f 100644 --- 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 @@ -41,16 +41,15 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Assert.assertTrue -import java.util.Arrays import java.util.HashMap import java.util.concurrent.CountDownLatch -class CryptoTestHelper(val mTestHelper: CommonTestHelper) { +class CryptoTestHelper(private 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.") + private val messagesFromAlice: List = listOf("0 - Hello I'm Alice!", "4 - Go!") + private val messagesFromBob: List = listOf("1 - Hello I'm Bob!", "2 - Isn't life grand?", "3 - Let's go to the opera.") - val defaultSessionParams = SessionTestParams(true) + private val defaultSessionParams = SessionTestParams(true) /** * @return alice session @@ -58,34 +57,23 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) { fun doE2ETestWithAliceInARoom(): CryptoTestData { val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams) - var roomId: String? = null - val lock1 = CountDownLatch(1) + val roomId = mTestHelper.doSync { + aliceSession.createRoom(CreateRoomParams(name = "MyRoom"), it) + } - aliceSession.createRoom(CreateRoomParams(name = "MyRoom"), object : TestMatrixCallback(lock1) { - override fun onSuccess(data: String) { - roomId = data - super.onSuccess(data) - } - }) + val room = aliceSession.getRoom(roomId)!! - mTestHelper.await(lock1) - assertNotNull(roomId) + mTestHelper.doSync { + room.enableEncryption(callback = it) + } - val room = aliceSession.getRoom(roomId!!)!! - - val lock2 = CountDownLatch(1) - room.enableEncryption(callback = TestMatrixCallback(lock2)) - mTestHelper.await(lock2) - - return CryptoTestData(aliceSession, roomId!!) + 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 @@ -94,7 +82,7 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) { val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, defaultSessionParams) - val lock1 = CountDownLatch(2) + val lock1 = CountDownLatch(1) val bobRoomSummariesLive = runBlocking(Dispatchers.Main) { bobSession.getRoomSummariesLive(roomSummaryQueryParams { }) @@ -103,7 +91,6 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) { val newRoomObserver = object : Observer> { override fun onChanged(t: List?) { if (t?.isNotEmpty() == true) { - statuses["onNewRoom"] = "onNewRoom" lock1.countDown() bobRoomSummariesLive.removeObserver(this) } @@ -114,26 +101,20 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) { bobRoomSummariesLive.observeForever(newRoomObserver) } - aliceRoom.invite(bobSession.myUserId, callback = object : TestMatrixCallback(lock1) { - override fun onSuccess(data: Unit) { - statuses["invite"] = "invite" - super.onSuccess(data) - } - }) + mTestHelper.doSync { + aliceRoom.invite(bobSession.myUserId, callback = it) + } mTestHelper.await(lock1) - assertTrue(statuses.containsKey("invite") && statuses.containsKey("onNewRoom")) - - val lock2 = CountDownLatch(2) + val lock = CountDownLatch(1) val roomJoinedObserver = object : Observer> { override fun onChanged(t: List?) { if (bobSession.getRoom(aliceRoomId) ?.getRoomMember(aliceSession.myUserId) ?.membership == Membership.JOIN) { - statuses["AliceJoin"] = "AliceJoin" - lock2.countDown() + lock.countDown() bobRoomSummariesLive.removeObserver(this) } } @@ -143,19 +124,15 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) { bobRoomSummariesLive.observeForever(roomJoinedObserver) } - bobSession.joinRoom(aliceRoomId, callback = TestMatrixCallback(lock2)) + mTestHelper.doSync { bobSession.joinRoom(aliceRoomId, callback = it) } - mTestHelper.await(lock2) + mTestHelper.await(lock) // 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) } @@ -230,14 +207,14 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) { val aliceRoomId = cryptoTestData.roomId val bobSession = cryptoTestData.secondSession!! - bobSession.setWarnOnUnknownDevices(false) + bobSession.cryptoService().setWarnOnUnknownDevices(false) - aliceSession.setWarnOnUnknownDevices(false) + aliceSession.cryptoService().setWarnOnUnknownDevices(false) val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!! val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!! - var lock = CountDownLatch(1) + val lock = CountDownLatch(1) val bobEventsListener = object : Timeline.Listener { override fun onTimelineFailure(throwable: Throwable) { @@ -249,63 +226,35 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) { } override fun onTimelineUpdated(snapshot: List) { - val size = snapshot.filter { it.root.senderId != bobSession.myUserId && it.root.getClearType() == EventType.MESSAGE } - .size + val messages = snapshot.filter { it.root.getClearType() == EventType.MESSAGE } + .groupBy { it.root.senderId!! } - if (size == 3) { + // Alice has sent 2 messages and Bob has sent 3 messages + if (messages[aliceSession.myUserId]?.size == 2 && messages[bobSession.myUserId]?.size == 3) { lock.countDown() } } } - val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(10)) + val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(20)) + bobTimeline.start() 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) + // Bob send 3 messages 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) + bobTimeline.dispose() return cryptoTestData } @@ -340,18 +289,14 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) { fun createFakeMegolmBackupAuthData(): MegolmBackupAuthData { return MegolmBackupAuthData( publicKey = "abcdefg", - signatures = HashMap>().apply { - this["something"] = HashMap().apply { - this["ed25519:something"] = "hijklmnop" - } - } + signatures = mapOf("something" to mapOf("ed25519:something" to "hijklmnop")) ) } fun createFakeMegolmBackupCreationInfo(): MegolmBackupCreationInfo { - return MegolmBackupCreationInfo().apply { - algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP - authData = createFakeMegolmBackupAuthData() - } + return MegolmBackupCreationInfo( + algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP, + authData = createFakeMegolmBackupAuthData() + ) } } 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 index 2a62165210..287a80db72 100644 --- 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 @@ -16,7 +16,10 @@ package im.vector.matrix.android.common -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.fail /** * Compare two lists and their content 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 index 60cc87d330..2346898ca7 100644 --- 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 @@ -22,11 +22,11 @@ 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. 10s + private const val AWAIT_TIME_OUT_MILLIS = 10_000 // 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 + private const val AWAIT_TIME_OUT_WITH_DEBUGGER_MILLIS = 10 * 60_000 const val USER_ALICE = "Alice" const val USER_BOB = "Bob" 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 index 7aedab7e2b..6041114777 100644 --- 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 @@ -22,7 +22,9 @@ 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.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertNotNull import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/CryptoStoreTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/CryptoStoreTest.kt index df503f2486..45589a49a4 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/CryptoStoreTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/CryptoStoreTest.kt @@ -21,7 +21,11 @@ import im.vector.matrix.android.InstrumentedTest import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import io.realm.Realm -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith 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 index 89ed3c2e65..f873e7a809 100644 --- 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 @@ -17,7 +17,9 @@ package im.vector.matrix.android.internal.crypto import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Assert.fail import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/crosssigning/XSigningTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/crosssigning/XSigningTest.kt index c8e7355d7a..f8d30a2679 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/crosssigning/XSigningTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/crosssigning/XSigningTest.kt @@ -2,12 +2,10 @@ package im.vector.matrix.android.internal.crypto.crosssigning 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.common.CommonTestHelper import im.vector.matrix.android.common.CryptoTestHelper import im.vector.matrix.android.common.SessionTestParams import im.vector.matrix.android.common.TestConstants -import im.vector.matrix.android.common.TestMatrixCallback import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth @@ -21,7 +19,6 @@ import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters -import java.util.concurrent.CountDownLatch @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @@ -34,16 +31,15 @@ class XSigningTest : InstrumentedTest { fun test_InitializeAndStoreKeys() { val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) - val aliceLatch = CountDownLatch(1) - aliceSession.getCrossSigningService() - .initializeCrossSigning(UserPasswordAuth( - user = aliceSession.myUserId, - password = TestConstants.PASSWORD - ), TestMatrixCallback(aliceLatch)) + mTestHelper.doSync { + aliceSession.cryptoService().crossSigningService() + .initializeCrossSigning(UserPasswordAuth( + user = aliceSession.myUserId, + password = TestConstants.PASSWORD + ), it) + } - mTestHelper.await(aliceLatch) - - val myCrossSigningKeys = aliceSession.getCrossSigningService().getMyCrossSigningKeys() + val myCrossSigningKeys = aliceSession.cryptoService().crossSigningService().getMyCrossSigningKeys() val masterPubKey = myCrossSigningKeys?.masterKey() assertNotNull("Master key should be stored", masterPubKey?.unpaddedBase64PublicKey) val selfSigningKey = myCrossSigningKeys?.selfSigningKey() @@ -53,9 +49,9 @@ class XSigningTest : InstrumentedTest { assertTrue("Signing Keys should be trusted", myCrossSigningKeys?.isTrusted() == true) - assertTrue("Signing Keys should be trusted", aliceSession.getCrossSigningService().checkUserTrust(aliceSession.myUserId).isVerified()) + assertTrue("Signing Keys should be trusted", aliceSession.cryptoService().crossSigningService().checkUserTrust(aliceSession.myUserId).isVerified()) - mTestHelper.signout(aliceSession) + mTestHelper.signOutAndClose(aliceSession) } @Test @@ -74,30 +70,24 @@ class XSigningTest : InstrumentedTest { password = TestConstants.PASSWORD ) - val latch = CountDownLatch(2) - - aliceSession.getCrossSigningService().initializeCrossSigning(aliceAuthParams, TestMatrixCallback(latch)) - bobSession.getCrossSigningService().initializeCrossSigning(bobAuthParams, TestMatrixCallback(latch)) - - mTestHelper.await(latch) + mTestHelper.doSync { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(aliceAuthParams, it) } + mTestHelper.doSync { bobSession.cryptoService().crossSigningService().initializeCrossSigning(bobAuthParams, it) } // Check that alice can see bob keys - val downloadLatch = CountDownLatch(1) - aliceSession.downloadKeys(listOf(bobSession.myUserId), true, TestMatrixCallback(downloadLatch)) - mTestHelper.await(downloadLatch) + mTestHelper.doSync> { aliceSession.cryptoService().downloadKeys(listOf(bobSession.myUserId), true, it) } - val bobKeysFromAlicePOV = aliceSession.getCrossSigningService().getUserCrossSigningKeys(bobSession.myUserId) + val bobKeysFromAlicePOV = aliceSession.cryptoService().crossSigningService().getUserCrossSigningKeys(bobSession.myUserId) assertNotNull("Alice can see bob Master key", bobKeysFromAlicePOV!!.masterKey()) assertNull("Alice should not see bob User key", bobKeysFromAlicePOV.userKey()) assertNotNull("Alice can see bob SelfSigned key", bobKeysFromAlicePOV.selfSigningKey()) - assertEquals("Bob keys from alice pov should match", bobKeysFromAlicePOV.masterKey()?.unpaddedBase64PublicKey, bobSession.getCrossSigningService().getMyCrossSigningKeys()?.masterKey()?.unpaddedBase64PublicKey) - assertEquals("Bob keys from alice pov should match", bobKeysFromAlicePOV.selfSigningKey()?.unpaddedBase64PublicKey, bobSession.getCrossSigningService().getMyCrossSigningKeys()?.selfSigningKey()?.unpaddedBase64PublicKey) + assertEquals("Bob keys from alice pov should match", bobKeysFromAlicePOV.masterKey()?.unpaddedBase64PublicKey, bobSession.cryptoService().crossSigningService().getMyCrossSigningKeys()?.masterKey()?.unpaddedBase64PublicKey) + assertEquals("Bob keys from alice pov should match", bobKeysFromAlicePOV.selfSigningKey()?.unpaddedBase64PublicKey, bobSession.cryptoService().crossSigningService().getMyCrossSigningKeys()?.selfSigningKey()?.unpaddedBase64PublicKey) assertFalse("Bob keys from alice pov should not be trusted", bobKeysFromAlicePOV.isTrusted()) - mTestHelper.signout(aliceSession) - mTestHelper.signout(bobSession) + mTestHelper.signOutAndClose(aliceSession) + mTestHelper.signOutAndClose(bobSession) } @Test @@ -116,94 +106,56 @@ class XSigningTest : InstrumentedTest { password = TestConstants.PASSWORD ) - val latch = CountDownLatch(2) - - aliceSession.getCrossSigningService().initializeCrossSigning(aliceAuthParams, TestMatrixCallback(latch)) - bobSession.getCrossSigningService().initializeCrossSigning(bobAuthParams, TestMatrixCallback(latch)) - - mTestHelper.await(latch) + mTestHelper.doSync { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(aliceAuthParams, it) } + mTestHelper.doSync { bobSession.cryptoService().crossSigningService().initializeCrossSigning(bobAuthParams, it) } // Check that alice can see bob keys - val downloadLatch = CountDownLatch(1) val bobUserId = bobSession.myUserId - aliceSession.downloadKeys(listOf(bobUserId), true, TestMatrixCallback(downloadLatch)) - mTestHelper.await(downloadLatch) + mTestHelper.doSync> { aliceSession.cryptoService().downloadKeys(listOf(bobUserId), true, it) } - val bobKeysFromAlicePOV = aliceSession.getCrossSigningService().getUserCrossSigningKeys(bobUserId) + val bobKeysFromAlicePOV = aliceSession.cryptoService().crossSigningService().getUserCrossSigningKeys(bobUserId) assertTrue("Bob keys from alice pov should not be trusted", bobKeysFromAlicePOV?.isTrusted() == false) - val trustLatch = CountDownLatch(1) - aliceSession.getCrossSigningService().trustUser(bobUserId, object : MatrixCallback { - override fun onSuccess(data: Unit) { - trustLatch.countDown() - } - - override fun onFailure(failure: Throwable) { - fail("Failed to trust bob") - } - }) - mTestHelper.await(trustLatch) + mTestHelper.doSync { aliceSession.cryptoService().crossSigningService().trustUser(bobUserId, it) } // Now bobs logs in on a new device and verifies it // We will want to test that in alice POV, this new device would be trusted by cross signing val bobSession2 = mTestHelper.logIntoAccount(bobUserId, SessionTestParams(true)) - val bobSecondDeviceId = bobSession2.sessionParams.credentials.deviceId + val bobSecondDeviceId = bobSession2.sessionParams.credentials.deviceId!! // Check that bob first session sees the new login - val bobKeysLatch = CountDownLatch(1) - bobSession.downloadKeys(listOf(bobUserId), true, object : MatrixCallback> { - override fun onFailure(failure: Throwable) { - fail("Failed to get device") - } + val data = mTestHelper.doSync> { + bobSession.cryptoService().downloadKeys(listOf(bobUserId), true, it) + } - override fun onSuccess(data: MXUsersDevicesMap) { - if (data.getUserDeviceIds(bobUserId)?.contains(bobSecondDeviceId!!) == false) { - fail("Bob should see the new device") - } - bobKeysLatch.countDown() - } - }) - mTestHelper.await(bobKeysLatch) + if (data.getUserDeviceIds(bobUserId)?.contains(bobSecondDeviceId) == false) { + fail("Bob should see the new device") + } - val bobSecondDevicePOVFirstDevice = bobSession.getDeviceInfo(bobUserId, bobSecondDeviceId) + val bobSecondDevicePOVFirstDevice = bobSession.cryptoService().getDeviceInfo(bobUserId, bobSecondDeviceId) assertNotNull("Bob Second device should be known and persisted from first", bobSecondDevicePOVFirstDevice) // Manually mark it as trusted from first session - val bobSignLatch = CountDownLatch(1) - bobSession.getCrossSigningService().signDevice(bobSecondDeviceId!!, object : MatrixCallback { - override fun onSuccess(data: Unit) { - bobSignLatch.countDown() - } - - override fun onFailure(failure: Throwable) { - fail("Failed to trust bob ${failure.localizedMessage}") - } - }) - mTestHelper.await(bobSignLatch) + mTestHelper.doSync { + bobSession.cryptoService().crossSigningService().trustDevice(bobSecondDeviceId, it) + } // Now alice should cross trust bob's second device - val aliceKeysLatch = CountDownLatch(1) - aliceSession.downloadKeys(listOf(bobUserId), true, object : MatrixCallback> { - override fun onFailure(failure: Throwable) { - fail("Failed to get device") - } + val data2 = mTestHelper.doSync> { + aliceSession.cryptoService().downloadKeys(listOf(bobUserId), true, it) + } - override fun onSuccess(data: MXUsersDevicesMap) { - // check that the device is seen - if (data.getUserDeviceIds(bobUserId)?.contains(bobSecondDeviceId) == false) { - fail("Alice should see the new device") - } - aliceKeysLatch.countDown() - } - }) - mTestHelper.await(aliceKeysLatch) + // check that the device is seen + if (data2.getUserDeviceIds(bobUserId)?.contains(bobSecondDeviceId) == false) { + fail("Alice should see the new device") + } - val result = aliceSession.getCrossSigningService().checkDeviceTrust(bobUserId, bobSecondDeviceId, null) + val result = aliceSession.cryptoService().crossSigningService().checkDeviceTrust(bobUserId, bobSecondDeviceId, null) assertTrue("Bob second device should be trusted from alice POV", result.isCrossSignedVerified()) - mTestHelper.signout(aliceSession) - mTestHelper.signout(bobSession) - mTestHelper.signout(bobSession2) + mTestHelper.signOutAndClose(aliceSession) + mTestHelper.signOutAndClose(bobSession) + mTestHelper.signOutAndClose(bobSession2) } } 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 index 53e68383ee..b1b111babf 100644 --- 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 @@ -20,7 +20,9 @@ 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.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.FixMethodOrder import org.junit.Test 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 index 312ad03a06..77ba66d341 100644 --- 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 @@ -18,7 +18,6 @@ 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 @@ -58,7 +57,7 @@ import java.util.Collections import java.util.concurrent.CountDownLatch @RunWith(AndroidJUnit4::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@FixMethodOrder(MethodSorters.JVM) class KeysBackupTest : InstrumentedTest { private val mTestHelper = CommonTestHelper(context()) @@ -77,21 +76,21 @@ class KeysBackupTest : InstrumentedTest { val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() // From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys - val cryptoStore = (cryptoTestData.firstSession.getKeysBackupService() as KeysBackup).store + val cryptoStore = (cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService).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)) + assertEquals(sessionsCount, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false)) + assertEquals(0, cryptoTestData.firstSession.cryptoService().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)) + assertEquals(sessionsCount, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false)) + assertEquals(1, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true)) val sessions2 = cryptoStore.inboundGroupSessionsToBackup(100) assertEquals(sessionsCount - 1, sessions2.size) @@ -101,8 +100,10 @@ class KeysBackupTest : InstrumentedTest { val sessions3 = cryptoStore.inboundGroupSessionsToBackup(100) assertEquals(sessionsCount, sessions3.size) - assertEquals(sessionsCount, cryptoTestData.firstSession.inboundGroupSessionsCount(false)) - assertEquals(0, cryptoTestData.firstSession.inboundGroupSessionsCount(true)) + assertEquals(sessionsCount, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false)) + assertEquals(0, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true)) + + cryptoTestData.cleanUp(mTestHelper) } /** @@ -112,39 +113,26 @@ class KeysBackupTest : InstrumentedTest { fun prepareKeysBackupVersionTest() { val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, defaultSessionParams) - assertNotNull(bobSession.getKeysBackupService()) + assertNotNull(bobSession.cryptoService().keysBackupService()) - val keysBackup = bobSession.getKeysBackupService() + val keysBackup = bobSession.cryptoService().keysBackupService() val stateObserver = StateObserver(keysBackup) assertFalse(keysBackup.isEnabled) - val latch = CountDownLatch(1) + val megolmBackupCreationInfo = mTestHelper.doSync { + keysBackup.prepareKeysBackupVersion(null, null, it) + } - 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) + assertEquals(MXCRYPTO_ALGORITHM_MEGOLM_BACKUP, megolmBackupCreationInfo.algorithm) + assertNotNull(megolmBackupCreationInfo.authData) + assertNotNull(megolmBackupCreationInfo.authData!!.publicKey) + assertNotNull(megolmBackupCreationInfo.authData!!.signatures) + assertNotNull(megolmBackupCreationInfo.recoveryKey) stateObserver.stopAndCheckStates(null) - bobSession.close() + mTestHelper.signOutAndClose(bobSession) } /** @@ -154,51 +142,28 @@ class KeysBackupTest : InstrumentedTest { fun createKeysBackupVersionTest() { val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, defaultSessionParams) - val keysBackup = bobSession.getKeysBackupService() + val keysBackup = bobSession.cryptoService().keysBackupService() 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) + val megolmBackupCreationInfo = mTestHelper.doSync { + keysBackup.prepareKeysBackupVersion(null, null, it) + } 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) + mTestHelper.doSync { + keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it) + } // Backup must be enable now assertTrue(keysBackup.isEnabled) stateObserver.stopAndCheckStates(null) - bobSession.close() + mTestHelper.signOutAndClose(bobSession) } /** @@ -209,12 +174,12 @@ class KeysBackupTest : InstrumentedTest { fun backupAfterCreateKeysBackupVersionTest() { val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() - val keysBackup = cryptoTestData.firstSession.getKeysBackupService() + val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() val latch = CountDownLatch(1) - assertEquals(2, cryptoTestData.firstSession.inboundGroupSessionsCount(false)) - assertEquals(0, cryptoTestData.firstSession.inboundGroupSessionsCount(true)) + assertEquals(2, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false)) + assertEquals(0, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true)) val stateObserver = StateObserver(keysBackup, latch, 5) @@ -222,8 +187,8 @@ class KeysBackupTest : InstrumentedTest { mTestHelper.await(latch) - val nbOfKeys = cryptoTestData.firstSession.inboundGroupSessionsCount(false) - val backedUpKeys = cryptoTestData.firstSession.inboundGroupSessionsCount(true) + val nbOfKeys = cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false) + val backedUpKeys = cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true) assertEquals(2, nbOfKeys) assertEquals("All keys must have been marked as backed up", nbOfKeys, backedUpKeys) @@ -238,7 +203,7 @@ class KeysBackupTest : InstrumentedTest { KeysBackupState.ReadyToBackUp ) ) - cryptoTestData.close() + cryptoTestData.cleanUp(mTestHelper) } /** @@ -248,37 +213,36 @@ class KeysBackupTest : InstrumentedTest { fun backupAllGroupSessionsTest() { val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() - val keysBackup = cryptoTestData.firstSession.getKeysBackupService() + val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() val stateObserver = StateObserver(keysBackup) prepareAndCreateKeysBackupData(keysBackup) // Check that backupAllGroupSessions returns valid data - val nbOfKeys = cryptoTestData.firstSession.inboundGroupSessionsCount(false) + val nbOfKeys = cryptoTestData.firstSession.cryptoService().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.doSync { + keysBackup.backupAllGroupSessions(object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + assertEquals(nbOfKeys, total) + lastBackedUpKeysProgress = progress + } + }, it) + } - mTestHelper.await(latch) assertEquals(nbOfKeys, lastBackedUpKeysProgress) - val backedUpKeys = cryptoTestData.firstSession.inboundGroupSessionsCount(true) + val backedUpKeys = cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true) assertEquals("All keys must have been marked as backed up", nbOfKeys, backedUpKeys) stateObserver.stopAndCheckStates(null) - cryptoTestData.close() + cryptoTestData.cleanUp(mTestHelper) } /** @@ -293,7 +257,7 @@ class KeysBackupTest : InstrumentedTest { fun testEncryptAndDecryptKeysBackupData() { val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() - val keysBackup = cryptoTestData.firstSession.getKeysBackupService() as KeysBackup + val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService val stateObserver = StateObserver(keysBackup) @@ -321,7 +285,7 @@ class KeysBackupTest : InstrumentedTest { assertKeysEquals(session.exportKeys(), sessionData) stateObserver.stopAndCheckStates(null) - cryptoTestData.close() + cryptoTestData.cleanUp(mTestHelper) } /** @@ -335,25 +299,19 @@ class KeysBackupTest : InstrumentedTest { 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) + val importRoomKeysResult = mTestHelper.doSync { + testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, + null, + null, + null, + it + ) + } - checkRestoreSuccess(testData, importRoomKeysResult!!.totalNumberOfKeys, importRoomKeysResult!!.successfullyNumberOfImportedKeys) + checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys) - testData.cryptoTestData.close() + testData.cleanUp(mTestHelper) } /** @@ -370,10 +328,12 @@ class KeysBackupTest : InstrumentedTest { */ @Test fun restoreKeysBackupAndKeyShareRequestTest() { + fail("Check with Valere for this test. I think we do not send key share request") + val testData = createKeysBackupScenarioWithPassword(null) // - Check the SDK sent key share requests - val cryptoStore2 = (testData.aliceSession2.getKeysBackupService() as KeysBackup).store + val cryptoStore2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store val unsentRequest = cryptoStore2 .getOutgoingRoomKeyRequestByState(setOf(OutgoingRoomKeyRequest.RequestState.UNSENT)) val sentRequest = cryptoStore2 @@ -383,23 +343,17 @@ class KeysBackupTest : InstrumentedTest { 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) + val importRoomKeysResult = mTestHelper.doSync { + testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, + null, + null, + null, + it + ) + } - checkRestoreSuccess(testData, importRoomKeysResult!!.totalNumberOfKeys, importRoomKeysResult!!.successfullyNumberOfImportedKeys) + checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys) // - There must be no more pending key share requests val unsentRequestAfterRestoration = cryptoStore2 @@ -410,7 +364,7 @@ class KeysBackupTest : InstrumentedTest { // Request is either sent or unsent assertTrue(unsentRequestAfterRestoration == null && sentRequestAfterRestoration == null) - testData.cryptoTestData.close() + testData.cleanUp(mTestHelper) } /** @@ -429,62 +383,47 @@ class KeysBackupTest : InstrumentedTest { // - And log Alice on a new device val testData = createKeysBackupScenarioWithPassword(null) - val stateObserver = StateObserver(testData.aliceSession2.getKeysBackupService()) + val stateObserver = StateObserver(testData.aliceSession2.cryptoService().keysBackupService()) // - 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) + assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) + assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().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) + mTestHelper.doSync { + testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersion( + testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + true, + it + ) + } // 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) + assertEquals(testData.prepareKeysBackupDataResult.version, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion?.version) + assertTrue(testData.aliceSession2.cryptoService().keysBackupService().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) + val keysVersionResult = mTestHelper.doSync { + testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it) + } // - 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) + val keysBackupVersionTrust = mTestHelper.doSync { + testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it) + } // - It must be trusted and must have 2 signatures now - assertTrue(keysBackupVersionTrust!!.usable) - assertEquals(2, keysBackupVersionTrust!!.signatures.size) + assertTrue(keysBackupVersionTrust.usable) + assertEquals(2, keysBackupVersionTrust.signatures.size) stateObserver.stopAndCheckStates(null) - testData.cryptoTestData.close() + testData.cleanUp(mTestHelper) } /** @@ -503,62 +442,47 @@ class KeysBackupTest : InstrumentedTest { // - And log Alice on a new device val testData = createKeysBackupScenarioWithPassword(null) - val stateObserver = StateObserver(testData.aliceSession2.getKeysBackupService()) + val stateObserver = StateObserver(testData.aliceSession2.cryptoService().keysBackupService()) // - 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) + assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) + assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().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) + mTestHelper.doSync { + testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithRecoveryKey( + testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, + it + ) + } // 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) + assertEquals(testData.prepareKeysBackupDataResult.version, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion?.version) + assertTrue(testData.aliceSession2.cryptoService().keysBackupService().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) + val keysVersionResult = mTestHelper.doSync { + testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it) + } // - 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) + val keysBackupVersionTrust = mTestHelper.doSync { + testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it) + } // - It must be trusted and must have 2 signatures now - assertTrue(keysBackupVersionTrust!!.usable) - assertEquals(2, keysBackupVersionTrust!!.signatures.size) + assertTrue(keysBackupVersionTrust.usable) + assertEquals(2, keysBackupVersionTrust.signatures.size) stateObserver.stopAndCheckStates(null) - testData.cryptoTestData.close() + testData.cleanUp(mTestHelper) } /** @@ -575,29 +499,29 @@ class KeysBackupTest : InstrumentedTest { // - And log Alice on a new device val testData = createKeysBackupScenarioWithPassword(null) - val stateObserver = StateObserver(testData.aliceSession2.getKeysBackupService()) + val stateObserver = StateObserver(testData.aliceSession2.cryptoService().keysBackupService()) // - 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) + assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) + assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().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!!, + testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithRecoveryKey( + testData.aliceSession2.cryptoService().keysBackupService().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) + assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) + assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state) stateObserver.stopAndCheckStates(null) - testData.cryptoTestData.close() + testData.cleanUp(mTestHelper) } /** @@ -618,62 +542,47 @@ class KeysBackupTest : InstrumentedTest { // - And log Alice on a new device val testData = createKeysBackupScenarioWithPassword(password) - val stateObserver = StateObserver(testData.aliceSession2.getKeysBackupService()) + val stateObserver = StateObserver(testData.aliceSession2.cryptoService().keysBackupService()) // - 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) + assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) + assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().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) + mTestHelper.doSync { + testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithPassphrase( + testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + password, + it + ) + } // 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) + assertEquals(testData.prepareKeysBackupDataResult.version, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion?.version) + assertTrue(testData.aliceSession2.cryptoService().keysBackupService().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) + val keysVersionResult = mTestHelper.doSync { + testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it) + } // - 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) + val keysBackupVersionTrust = mTestHelper.doSync { + testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it) + } // - It must be trusted and must have 2 signatures now - assertTrue(keysBackupVersionTrust!!.usable) - assertEquals(2, keysBackupVersionTrust!!.signatures.size) + assertTrue(keysBackupVersionTrust.usable) + assertEquals(2, keysBackupVersionTrust.signatures.size) stateObserver.stopAndCheckStates(null) - testData.cryptoTestData.close() + testData.cleanUp(mTestHelper) } /** @@ -693,29 +602,29 @@ class KeysBackupTest : InstrumentedTest { // - And log Alice on a new device val testData = createKeysBackupScenarioWithPassword(password) - val stateObserver = StateObserver(testData.aliceSession2.getKeysBackupService()) + val stateObserver = StateObserver(testData.aliceSession2.cryptoService().keysBackupService()) // - 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) + assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) + assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().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!!, + testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithPassphrase( + testData.aliceSession2.cryptoService().keysBackupService().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) + assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) + assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state) stateObserver.stopAndCheckStates(null) - testData.cryptoTestData.close() + testData.cleanUp(mTestHelper) } /** @@ -731,7 +640,7 @@ class KeysBackupTest : InstrumentedTest { // - 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!!, + testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", null, null, @@ -748,7 +657,7 @@ class KeysBackupTest : InstrumentedTest { // onSuccess may not have been called assertNull(importRoomKeysResult) - testData.cryptoTestData.close() + testData.cleanUp(mTestHelper) } /** @@ -764,27 +673,21 @@ class KeysBackupTest : InstrumentedTest { 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) + val importRoomKeysResult = mTestHelper.doSync { + testData.aliceSession2.cryptoService().keysBackupService().restoreKeyBackupWithPassword(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + password, + null, + null, + object : StepProgressListener { + override fun onStepProgress(step: StepProgressListener.Step) { + steps.add(step) + } + }, + it + ) + } // Check steps assertEquals(105, steps.size) @@ -807,9 +710,9 @@ class KeysBackupTest : InstrumentedTest { assertEquals(50, (steps[103] as StepProgressListener.Step.ImportingKey).progress) assertEquals(100, (steps[104] as StepProgressListener.Step.ImportingKey).progress) - checkRestoreSuccess(testData, importRoomKeysResult!!.totalNumberOfKeys, importRoomKeysResult!!.successfullyNumberOfImportedKeys) + checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys) - testData.cryptoTestData.close() + testData.cleanUp(mTestHelper) } /** @@ -828,7 +731,7 @@ class KeysBackupTest : InstrumentedTest { // - 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!!, + testData.aliceSession2.cryptoService().keysBackupService().restoreKeyBackupWithPassword(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, wrongPassword, null, null, @@ -845,7 +748,7 @@ class KeysBackupTest : InstrumentedTest { // onSuccess may not have been called assertNull(importRoomKeysResult) - testData.cryptoTestData.close() + testData.cleanUp(mTestHelper) } /** @@ -861,25 +764,19 @@ class KeysBackupTest : InstrumentedTest { 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) + val importRoomKeysResult = mTestHelper.doSync { + testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, + null, + null, + null, + it + ) + } - checkRestoreSuccess(testData, importRoomKeysResult!!.totalNumberOfKeys, importRoomKeysResult!!.successfullyNumberOfImportedKeys) + checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys) - testData.cryptoTestData.close() + testData.cleanUp(mTestHelper) } /** @@ -895,7 +792,7 @@ class KeysBackupTest : InstrumentedTest { // - 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!!, + testData.aliceSession2.cryptoService().keysBackupService().restoreKeyBackupWithPassword(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, "password", null, null, @@ -912,7 +809,7 @@ class KeysBackupTest : InstrumentedTest { // onSuccess may not have been called assertNull(importRoomKeysResult) - testData.cryptoTestData.close() + testData.cleanUp(mTestHelper) } /** @@ -924,7 +821,7 @@ class KeysBackupTest : InstrumentedTest { // - Create a backup version val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() - val keysBackup = cryptoTestData.firstSession.getKeysBackupService() + val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() val stateObserver = StateObserver(keysBackup) @@ -932,46 +829,27 @@ class KeysBackupTest : InstrumentedTest { 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) + val keysVersionResult = mTestHelper.doSync { + keysBackup.getCurrentVersion(it) + } // - 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) + val keysBackupVersionTrust = mTestHelper.doSync { + keysBackup.getKeysBackupTrust(keysVersionResult!!, it) + } assertNotNull(keysBackupVersionTrust) - assertTrue(keysBackupVersionTrust!!.usable) - assertEquals(1, keysBackupVersionTrust!!.signatures.size) + assertTrue(keysBackupVersionTrust.usable) + assertEquals(1, keysBackupVersionTrust.signatures.size) - val signature = keysBackupVersionTrust!!.signatures[0] + val signature = keysBackupVersionTrust.signatures[0] assertTrue(signature.valid) assertNotNull(signature.device) - assertEquals(cryptoTestData.firstSession.getMyDevice().deviceId, signature.deviceId) + assertEquals(cryptoTestData.firstSession.cryptoService().getMyDevice().deviceId, signature.deviceId) assertEquals(signature.device!!.deviceId, cryptoTestData.firstSession.sessionParams.credentials.deviceId) stateObserver.stopAndCheckStates(null) - cryptoTestData.close() + cryptoTestData.cleanUp(mTestHelper) } /** @@ -983,10 +861,11 @@ class KeysBackupTest : InstrumentedTest { */ @Test fun testCheckAndStartKeysBackupWhenRestartingAMatrixSession() { + fail("This test still fail. To investigate") // - Create a backup version val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() - val keysBackup = cryptoTestData.firstSession.getKeysBackupService() + val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() val stateObserver = StateObserver(keysBackup) @@ -1000,9 +879,9 @@ class KeysBackupTest : InstrumentedTest { // - Log Alice on a new device val aliceSession2 = mTestHelper.logIntoAccount(cryptoTestData.firstSession.myUserId, defaultSessionParamsWithInitialSync) - cryptoTestData.close() + cryptoTestData.cleanUp(mTestHelper) - val keysBackup2 = aliceSession2.getKeysBackupService() + val keysBackup2 = aliceSession2.cryptoService().keysBackupService() val stateObserver2 = StateObserver(keysBackup2) @@ -1012,12 +891,12 @@ class KeysBackupTest : InstrumentedTest { keysBackup2.addListener(object : KeysBackupStateListener { override fun onStateChange(newState: KeysBackupState) { // Check the backup completes - if (keysBackup.state == KeysBackupState.ReadyToBackUp) { + if (newState == KeysBackupState.ReadyToBackUp) { count++ if (count == 2) { // Remove itself from the list of listeners - keysBackup.removeListener(this) + keysBackup2.removeListener(this) latch.countDown() } @@ -1030,7 +909,7 @@ class KeysBackupTest : InstrumentedTest { stateObserver.stopAndCheckStates(null) stateObserver2.stopAndCheckStates(null) - aliceSession2.close() + mTestHelper.signOutAndClose(aliceSession2) } /** @@ -1046,7 +925,7 @@ class KeysBackupTest : InstrumentedTest { // - Create a backup version val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() - val keysBackup = cryptoTestData.firstSession.getKeysBackupService() + val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() val stateObserver = StateObserver(keysBackup) @@ -1079,21 +958,17 @@ class KeysBackupTest : InstrumentedTest { 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) + mTestHelper.doSync { + (keysBackup as DefaultKeysBackupService).createFakeKeysBackupVersion(megolmBackupCreationInfo, it) + } // Reset the store backup status for keys - (cryptoTestData.firstSession.getKeysBackupService() as KeysBackup).store.resetBackupMarkers() + (cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService).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)) + keysBackup.backupAllGroupSessions(null, TestMatrixCallback(latch2, false)) mTestHelper.await(latch2) // -> That must fail and her backup state must be WrongBackUpVersion @@ -1101,7 +976,7 @@ class KeysBackupTest : InstrumentedTest { assertFalse(keysBackup.isEnabled) stateObserver.stopAndCheckStates(null) - cryptoTestData.close() + cryptoTestData.cleanUp(mTestHelper) } /** @@ -1121,7 +996,7 @@ class KeysBackupTest : InstrumentedTest { // - Create a backup version val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() - val keysBackup = cryptoTestData.firstSession.getKeysBackupService() + val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() val stateObserver = StateObserver(keysBackup) @@ -1129,46 +1004,39 @@ class KeysBackupTest : InstrumentedTest { 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) + mTestHelper.doSync { + keysBackup.backupAllGroupSessions(null, it) + } 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) + aliceSession2.cryptoService().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 keysBackup2 = aliceSession2.cryptoService().keysBackupService() 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) - } - }) + keysBackup2.backupAllGroupSessions( + null, + object : TestMatrixCallback(latch2, false) { + override fun onSuccess(data: Unit) { + isSuccessful = true + super.onSuccess(data) + } + }) mTestHelper.await(latch2) assertFalse(isSuccessful) @@ -1178,7 +1046,7 @@ class KeysBackupTest : InstrumentedTest { assertFalse(keysBackup2.isEnabled) // - Validate the old device from the new one - aliceSession2.setDeviceVerification(DeviceTrustLevel(false, true), aliceSession2.myUserId, oldDeviceId) + aliceSession2.cryptoService().setDeviceVerification(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession2.myUserId, oldDeviceId) // -> Backup should automatically enable on the new device val latch4 = CountDownLatch(1) @@ -1196,19 +1064,19 @@ class KeysBackupTest : InstrumentedTest { mTestHelper.await(latch4) // -> It must use the same backup version - assertEquals(oldKeyBackupVersion, aliceSession2.getKeysBackupService().currentBackupVersion) + assertEquals(oldKeyBackupVersion, aliceSession2.cryptoService().keysBackupService().currentBackupVersion) - val latch5 = CountDownLatch(1) - aliceSession2.getKeysBackupService().backupAllGroupSessions(null, TestMatrixCallback(latch5)) - mTestHelper.await(latch5) + mTestHelper.doSync { + aliceSession2.cryptoService().keysBackupService().backupAllGroupSessions(null, it) + } // -> It must success - assertTrue(aliceSession2.getKeysBackupService().isEnabled) + assertTrue(aliceSession2.cryptoService().keysBackupService().isEnabled) stateObserver.stopAndCheckStates(null) stateObserver2.stopAndCheckStates(null) - aliceSession2.close() - cryptoTestData.close() + mTestHelper.signOutAndClose(aliceSession2) + cryptoTestData.cleanUp(mTestHelper) } /** @@ -1220,7 +1088,7 @@ class KeysBackupTest : InstrumentedTest { // - Create a backup version val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() - val keysBackup = cryptoTestData.firstSession.getKeysBackupService() + val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() val stateObserver = StateObserver(keysBackup) @@ -1230,18 +1098,14 @@ class KeysBackupTest : InstrumentedTest { assertTrue(keysBackup.isEnabled) - val latch = CountDownLatch(1) - // Delete the backup - keysBackup.deleteBackup(keyBackupCreationInfo.version, TestMatrixCallback(latch)) - - mTestHelper.await(latch) + mTestHelper.doSync { keysBackup.deleteBackup(keyBackupCreationInfo.version, it) } // Backup is now disabled assertFalse(keysBackup.isEnabled) stateObserver.stopAndCheckStates(null) - cryptoTestData.close() + cryptoTestData.cleanUp(mTestHelper) } /* ========================================================================================== @@ -1254,17 +1118,17 @@ class KeysBackupTest : InstrumentedTest { */ private fun waitForKeysBackupToBeInState(session: Session, state: KeysBackupState) { // If already in the wanted state, return - if (session.getKeysBackupService().state == state) { + if (session.cryptoService().keysBackupService().state == state) { return } // Else observe state changes val latch = CountDownLatch(1) - session.getKeysBackupService().addListener(object : KeysBackupStateListener { + session.cryptoService().keysBackupService().addListener(object : KeysBackupStateListener { override fun onStateChange(newState: KeysBackupState) { if (newState == state) { - session.getKeysBackupService().removeListener(this) + session.cryptoService().keysBackupService().removeListener(this) latch.countDown() } } @@ -1280,49 +1144,26 @@ class KeysBackupTest : InstrumentedTest { 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) + val megolmBackupCreationInfo = mTestHelper.doSync { + keysBackup.prepareKeysBackupVersion(password, null, it) + } 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) + val keysVersion = mTestHelper.doSync { + keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it) + } - version = data.version - - super.onSuccess(data) - } - }) - mTestHelper.await(latch2) + assertNotNull(keysVersion.version) // Backup must be enable now assertTrue(keysBackup.isEnabled) - assertNotNull(version) stateObserver.stopAndCheckStates(null) - return PrepareKeysBackupDataResult(megolmBackupCreationInfo!!, version!!) + return PrepareKeysBackupDataResult(megolmBackupCreationInfo, keysVersion.version!!) } private fun assertKeysEquals(keys1: MegolmSessionData?, keys2: MegolmSessionData?) { @@ -1347,7 +1188,12 @@ class KeysBackupTest : InstrumentedTest { private data class KeysBackupScenarioData(val cryptoTestData: CryptoTestData, val aliceKeys: List, val prepareKeysBackupDataResult: PrepareKeysBackupDataResult, - val aliceSession2: Session) + val aliceSession2: Session) { + fun cleanUp(testHelper: CommonTestHelper) { + cryptoTestData.cleanUp(testHelper) + testHelper.signOutAndClose(aliceSession2) + } + } /** * Common initial condition @@ -1359,8 +1205,8 @@ class KeysBackupTest : InstrumentedTest { private fun createKeysBackupScenarioWithPassword(password: String?): KeysBackupScenarioData { val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() - val cryptoStore = (cryptoTestData.firstSession.getKeysBackupService() as KeysBackup).store - val keysBackup = cryptoTestData.firstSession.getKeysBackupService() + val cryptoStore = (cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService).store + val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() val stateObserver = StateObserver(keysBackup) @@ -1369,32 +1215,27 @@ class KeysBackupTest : InstrumentedTest { // - 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) + mTestHelper.doSync { + keysBackup.backupAllGroupSessions(object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + lastProgress = progress + lastTotal = total + } + }, it) + } 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)) + assertEquals(0, aliceSession2.cryptoService().inboundGroupSessionsCount(false)) // Wait for backup state to be NotTrusted waitForKeysBackupToBeInState(aliceSession2, KeysBackupState.NotTrusted) @@ -1421,11 +1262,11 @@ class KeysBackupTest : InstrumentedTest { assertEquals(total, imported) // - The new device must have the same count of megolm keys - assertEquals(testData.aliceKeys.size, testData.aliceSession2.inboundGroupSessionsCount(false)) + assertEquals(testData.aliceKeys.size, testData.aliceSession2.cryptoService().inboundGroupSessionsCount(false)) // - Alice must have the same keys on both devices for (aliceKey1 in testData.aliceKeys) { - val aliceKey2 = (testData.aliceSession2.getKeysBackupService() as KeysBackup).store + val aliceKey2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).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/ssss/QuadSTests.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/ssss/QuadSTests.kt new file mode 100644 index 0000000000..1a0723c725 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/ssss/QuadSTests.kt @@ -0,0 +1,363 @@ +/* + * Copyright (c) 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.crypto.ssss + +import androidx.lifecycle.Observer +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.session.Session +import im.vector.matrix.android.api.session.securestorage.EncryptedSecretContent +import im.vector.matrix.android.api.session.securestorage.KeySigner +import im.vector.matrix.android.api.session.securestorage.RawBytesKeySpec +import im.vector.matrix.android.api.session.securestorage.SecretStorageKeyContent +import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService +import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo +import im.vector.matrix.android.api.util.Optional +import im.vector.matrix.android.common.CommonTestHelper +import im.vector.matrix.android.common.SessionTestParams +import im.vector.matrix.android.common.TestConstants +import im.vector.matrix.android.common.TestMatrixCallback +import im.vector.matrix.android.internal.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2 +import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding +import im.vector.matrix.android.internal.crypto.secrets.DefaultSharedSecretStorageService +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.amshove.kluent.shouldBe +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import java.util.concurrent.CountDownLatch + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class QuadSTests : InstrumentedTest { + + private val mTestHelper = CommonTestHelper(context()) + + private val emptyKeySigner = object : KeySigner { + override fun sign(canonicalJson: String): Map>? { + return null + } + } + + @Test + fun test_Generate4SKey() { + val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + + val quadS = aliceSession.sharedSecretStorageService + + val TEST_KEY_ID = "my.test.Key" + + mTestHelper.doSync { + quadS.generateKey(TEST_KEY_ID, "Test Key", emptyKeySigner, it) + } + + // Assert Account data is updated + val accountDataLock = CountDownLatch(1) + var accountData: UserAccountDataEvent? = null + + val liveAccountData = runBlocking(Dispatchers.Main) { + aliceSession.getLiveAccountDataEvent("${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID") + } + val accountDataObserver = Observer?> { t -> + if (t?.getOrNull()?.type == "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID") { + accountData = t.getOrNull() + accountDataLock.countDown() + } + } + GlobalScope.launch(Dispatchers.Main) { liveAccountData.observeForever(accountDataObserver) } + + mTestHelper.await(accountDataLock) + + assertNotNull("Key should be stored in account data", accountData) + val parsed = SecretStorageKeyContent.fromJson(accountData!!.content) + assertNotNull("Key Content cannot be parsed", parsed) + assertEquals("Unexpected Algorithm", SSSS_ALGORITHM_AES_HMAC_SHA2, parsed!!.algorithm) + assertEquals("Unexpected key name", "Test Key", parsed.name) + assertNull("Key was not generated from passphrase", parsed.passphrase) + + // Set as default key + quadS.setDefaultKey(TEST_KEY_ID, object : MatrixCallback {}) + + var defaultKeyAccountData: UserAccountDataEvent? = null + val defaultDataLock = CountDownLatch(1) + + val liveDefAccountData = runBlocking(Dispatchers.Main) { + aliceSession.getLiveAccountDataEvent(DefaultSharedSecretStorageService.DEFAULT_KEY_ID) + } + val accountDefDataObserver = Observer?> { t -> + if (t?.getOrNull()?.type == DefaultSharedSecretStorageService.DEFAULT_KEY_ID) { + defaultKeyAccountData = t.getOrNull()!! + defaultDataLock.countDown() + } + } + GlobalScope.launch(Dispatchers.Main) { liveDefAccountData.observeForever(accountDefDataObserver) } + + mTestHelper.await(defaultDataLock) + + assertNotNull(defaultKeyAccountData?.content) + assertEquals("Unexpected default key ${defaultKeyAccountData?.content}", TEST_KEY_ID, defaultKeyAccountData?.content?.get("key")) + + mTestHelper.signOutAndClose(aliceSession) + } + + @Test + fun test_StoreSecret() { + val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + val keyId = "My.Key" + val info = generatedSecret(aliceSession, keyId, true) + + val keySpec = RawBytesKeySpec.fromRecoveryKey(info.recoveryKey) + + // Store a secret + val clearSecret = "42".toByteArray().toBase64NoPadding() + mTestHelper.doSync { + aliceSession.sharedSecretStorageService.storeSecret( + "secret.of.life", + clearSecret, + listOf(SharedSecretStorageService.KeyRef(null, keySpec)), // default key + it + ) + } + + val secretAccountData = assertAccountData(aliceSession, "secret.of.life") + + val encryptedContent = secretAccountData.content.get("encrypted") as? Map<*, *> + assertNotNull("Element should be encrypted", encryptedContent) + assertNotNull("Secret should be encrypted with default key", encryptedContent?.get(keyId)) + + val secret = EncryptedSecretContent.fromJson(encryptedContent?.get(keyId)) + assertNotNull(secret?.ciphertext) + assertNotNull(secret?.mac) + assertNotNull(secret?.initializationVector) + + // Try to decrypt?? + + val decryptedSecret = mTestHelper.doSync { + aliceSession.sharedSecretStorageService.getSecret( + "secret.of.life", + null, // default key + keySpec!!, + it + ) + } + + assertEquals("Secret mismatch", clearSecret, decryptedSecret) + mTestHelper.signOutAndClose(aliceSession) + } + + @Test + fun test_SetDefaultLocalEcho() { + val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + + val quadS = aliceSession.sharedSecretStorageService + + val TEST_KEY_ID = "my.test.Key" + + mTestHelper.doSync { + quadS.generateKey(TEST_KEY_ID, "Test Key", emptyKeySigner, it) + } + + // Test that we don't need to wait for an account data sync to access directly the keyid from DB + mTestHelper.doSync { + quadS.setDefaultKey(TEST_KEY_ID, it) + } + + mTestHelper.signOutAndClose(aliceSession) + } + + @Test + fun test_StoreSecretWithMultipleKey() { + val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + val keyId1 = "Key.1" + val key1Info = generatedSecret(aliceSession, keyId1, true) + val keyId2 = "Key2" + val key2Info = generatedSecret(aliceSession, keyId2, true) + + val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit" + + mTestHelper.doSync { + aliceSession.sharedSecretStorageService.storeSecret( + "my.secret", + mySecretText.toByteArray().toBase64NoPadding(), + listOf( + SharedSecretStorageService.KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)), + SharedSecretStorageService.KeyRef(keyId2, RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)) + ), + it + ) + } + + val accountDataEvent = aliceSession.getAccountDataEvent("my.secret") + val encryptedContent = accountDataEvent?.content?.get("encrypted") as? Map<*, *> + + assertEquals("Content should contains two encryptions", 2, encryptedContent?.keys?.size ?: 0) + + assertNotNull(encryptedContent?.get(keyId1)) + assertNotNull(encryptedContent?.get(keyId2)) + + // Assert that can decrypt with both keys + mTestHelper.doSync { + aliceSession.sharedSecretStorageService.getSecret("my.secret", + keyId1, + RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)!!, + it + ) + } + + mTestHelper.doSync { + aliceSession.sharedSecretStorageService.getSecret("my.secret", + keyId2, + RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)!!, + it + ) + } + + mTestHelper.signOutAndClose(aliceSession) + } + + @Test + fun test_GetSecretWithBadPassphrase() { + val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + val keyId1 = "Key.1" + val passphrase = "The good pass phrase" + val key1Info = generatedSecretFromPassphrase(aliceSession, passphrase, keyId1, true) + + val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit" + + mTestHelper.doSync { + aliceSession.sharedSecretStorageService.storeSecret( + "my.secret", + mySecretText.toByteArray().toBase64NoPadding(), + listOf(SharedSecretStorageService.KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey))), + it + ) + } + + val decryptCountDownLatch = CountDownLatch(1) + var error = false + aliceSession.sharedSecretStorageService.getSecret("my.secret", + keyId1, + RawBytesKeySpec.fromPassphrase( + "A bad passphrase", + key1Info.content?.passphrase?.salt ?: "", + key1Info.content?.passphrase?.iterations ?: 0, + null), + object : MatrixCallback { + override fun onSuccess(data: String) { + decryptCountDownLatch.countDown() + } + + override fun onFailure(failure: Throwable) { + error = true + decryptCountDownLatch.countDown() + } + } + ) + + mTestHelper.await(decryptCountDownLatch) + + error shouldBe true + + // Now try with correct key + mTestHelper.doSync { + aliceSession.sharedSecretStorageService.getSecret("my.secret", + keyId1, + RawBytesKeySpec.fromPassphrase( + passphrase, + key1Info.content?.passphrase?.salt ?: "", + key1Info.content?.passphrase?.iterations ?: 0, + null), + it + ) + } + + mTestHelper.signOutAndClose(aliceSession) + } + + private fun assertAccountData(session: Session, type: String): UserAccountDataEvent { + val accountDataLock = CountDownLatch(1) + var accountData: UserAccountDataEvent? = null + + val liveAccountData = runBlocking(Dispatchers.Main) { + session.getLiveAccountDataEvent(type) + } + val accountDataObserver = Observer?> { t -> + if (t?.getOrNull()?.type == type) { + accountData = t.getOrNull() + accountDataLock.countDown() + } + } + GlobalScope.launch(Dispatchers.Main) { liveAccountData.observeForever(accountDataObserver) } + mTestHelper.await(accountDataLock) + + assertNotNull("Account Data type:$type should be found", accountData) + + return accountData!! + } + + private fun generatedSecret(session: Session, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo { + val quadS = session.sharedSecretStorageService + + val creationInfo = mTestHelper.doSync { + quadS.generateKey(keyId, keyId, emptyKeySigner, it) + } + + assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId") + + if (asDefault) { + mTestHelper.doSync { + quadS.setDefaultKey(keyId, it) + } + assertAccountData(session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID) + } + + return creationInfo + } + + private fun generatedSecretFromPassphrase(session: Session, passphrase: String, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo { + val quadS = session.sharedSecretStorageService + + val creationInfo = mTestHelper.doSync { + quadS.generateKeyWithPassphrase( + keyId, + keyId, + passphrase, + emptyKeySigner, + null, + it) + } + + assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId") + if (asDefault) { + val setDefaultLatch = CountDownLatch(1) + quadS.setDefaultKey(keyId, TestMatrixCallback(setDefaultLatch)) + mTestHelper.await(setDefaultLatch) + assertAccountData(session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID) + } + + return creationInfo + } +} 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 index 79670bb21e..db48d7653c 100644 --- 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 @@ -19,14 +19,14 @@ 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.CancelCode -import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction -import im.vector.matrix.android.api.session.crypto.sas.OutgoingSasVerificationTransaction -import im.vector.matrix.android.api.session.crypto.sas.SasMode -import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod -import im.vector.matrix.android.api.session.crypto.sas.VerificationService -import im.vector.matrix.android.api.session.crypto.sas.VerificationTransaction -import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState +import im.vector.matrix.android.api.session.crypto.verification.CancelCode +import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction +import im.vector.matrix.android.api.session.crypto.verification.OutgoingSasVerificationTransaction +import im.vector.matrix.android.api.session.crypto.verification.SasMode +import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod +import im.vector.matrix.android.api.session.crypto.verification.VerificationService +import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction +import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState 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 @@ -62,8 +62,8 @@ class SASTest : InstrumentedTest { val aliceSession = cryptoTestData.firstSession val bobSession = cryptoTestData.secondSession - val aliceVerificationService = aliceSession.getVerificationService() - val bobVerificationService = bobSession!!.getVerificationService() + val aliceVerificationService = aliceSession.cryptoService().verificationService() + val bobVerificationService = bobSession!!.cryptoService().verificationService() val bobTxCreatedLatch = CountDownLatch(1) val bobListener = object : VerificationService.Listener { @@ -75,7 +75,7 @@ class SASTest : InstrumentedTest { val txID = aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobSession.myUserId, - bobSession.getMyDevice().deviceId, + bobSession.cryptoService().getMyDevice().deviceId, null) assertNotNull("Alice should have a started transaction", txID) @@ -132,7 +132,7 @@ class SASTest : InstrumentedTest { assertNull(bobVerificationService.getExistingTransaction(aliceSession.myUserId, txID)) assertNull(aliceVerificationService.getExistingTransaction(bobSession.myUserId, txID)) - cryptoTestData.close() + cryptoTestData.cleanUp(mTestHelper) } @Test @@ -157,7 +157,7 @@ class SASTest : InstrumentedTest { } } } - bobSession.getVerificationService().addListener(bobListener) + bobSession.cryptoService().verificationService().addListener(bobListener) // TODO bobSession!!.dataHandler.addListener(object : MXEventListener() { // TODO override fun onToDeviceEvent(event: Event?) { @@ -172,7 +172,7 @@ class SASTest : InstrumentedTest { val aliceSession = cryptoTestData.firstSession val aliceUserID = aliceSession.myUserId - val aliceDevice = aliceSession.getMyDevice().deviceId + val aliceDevice = aliceSession.cryptoService().getMyDevice().deviceId val aliceListener = object : VerificationService.Listener { override fun transactionUpdated(tx: VerificationTransaction) { @@ -181,7 +181,7 @@ class SASTest : InstrumentedTest { } } } - aliceSession.getVerificationService().addListener(aliceListener) + aliceSession.cryptoService().verificationService().addListener(aliceListener) fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, protocols = protocols) @@ -189,7 +189,7 @@ class SASTest : InstrumentedTest { assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod, cancelReason) - cryptoTestData.close() + cryptoTestData.cleanUp(mTestHelper) } @Test @@ -218,7 +218,7 @@ class SASTest : InstrumentedTest { val aliceSession = cryptoTestData.firstSession val aliceUserID = aliceSession.myUserId - val aliceDevice = aliceSession.getMyDevice().deviceId + val aliceDevice = aliceSession.cryptoService().getMyDevice().deviceId fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, mac = mac) @@ -227,7 +227,7 @@ class SASTest : InstrumentedTest { val cancelReq = canceledToDeviceEvent!!.content.toModel()!! assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code) - cryptoTestData.close() + cryptoTestData.cleanUp(mTestHelper) } @Test @@ -256,7 +256,7 @@ class SASTest : InstrumentedTest { val aliceSession = cryptoTestData.firstSession val aliceUserID = aliceSession.myUserId - val aliceDevice = aliceSession.getMyDevice().deviceId + val aliceDevice = aliceSession.cryptoService().getMyDevice().deviceId fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, codes = codes) @@ -265,7 +265,7 @@ class SASTest : InstrumentedTest { val cancelReq = canceledToDeviceEvent!!.content.toModel()!! assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code) - cryptoTestData.close() + cryptoTestData.cleanUp(mTestHelper) } private fun fakeBobStart(bobSession: Session, @@ -277,7 +277,7 @@ class SASTest : InstrumentedTest { mac: List = SASDefaultVerificationTransaction.KNOWN_MACS, codes: List = SASDefaultVerificationTransaction.KNOWN_SHORT_CODES) { val startMessage = KeyVerificationStart( - fromDevice = bobSession.getMyDevice().deviceId, + fromDevice = bobSession.cryptoService().getMyDevice().deviceId, method = VerificationMethod.SAS.toValue(), transactionID = tid, keyAgreementProtocols = protocols, @@ -307,7 +307,7 @@ class SASTest : InstrumentedTest { val aliceSession = cryptoTestData.firstSession val bobSession = cryptoTestData.secondSession - val aliceVerificationService = aliceSession.getVerificationService() + val aliceVerificationService = aliceSession.cryptoService().verificationService() val aliceCreatedLatch = CountDownLatch(2) val aliceCancelledLatch = CountDownLatch(2) @@ -327,14 +327,14 @@ class SASTest : InstrumentedTest { aliceVerificationService.addListener(aliceListener) val bobUserId = bobSession!!.myUserId - val bobDeviceId = bobSession.getMyDevice().deviceId + val bobDeviceId = bobSession.cryptoService().getMyDevice().deviceId aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null) aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null) mTestHelper.await(aliceCreatedLatch) mTestHelper.await(aliceCancelledLatch) - cryptoTestData.close() + cryptoTestData.cleanUp(mTestHelper) } /** @@ -347,8 +347,8 @@ class SASTest : InstrumentedTest { val aliceSession = cryptoTestData.firstSession val bobSession = cryptoTestData.secondSession - val aliceVerificationService = aliceSession.getVerificationService() - val bobVerificationService = bobSession!!.getVerificationService() + val aliceVerificationService = aliceSession.cryptoService().verificationService() + val bobVerificationService = bobSession!!.cryptoService().verificationService() var accepted: KeyVerificationAccept? = null var startReq: KeyVerificationStart? = null @@ -377,7 +377,7 @@ class SASTest : InstrumentedTest { bobVerificationService.addListener(bobListener) val bobUserId = bobSession.myUserId - val bobDeviceId = bobSession.getMyDevice().deviceId + val bobDeviceId = bobSession.cryptoService().getMyDevice().deviceId aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null) mTestHelper.await(aliceAcceptedLatch) @@ -393,7 +393,7 @@ class SASTest : InstrumentedTest { assertTrue("all agreed Short Code should be known by alice", startReq!!.shortAuthenticationStrings!!.contains(it)) } - cryptoTestData.close() + cryptoTestData.cleanUp(mTestHelper) } @Test @@ -403,8 +403,8 @@ class SASTest : InstrumentedTest { val aliceSession = cryptoTestData.firstSession val bobSession = cryptoTestData.secondSession - val aliceVerificationService = aliceSession.getVerificationService() - val bobVerificationService = bobSession!!.getVerificationService() + val aliceVerificationService = aliceSession.cryptoService().verificationService() + val bobVerificationService = bobSession!!.cryptoService().verificationService() val aliceSASLatch = CountDownLatch(1) val aliceListener = object : VerificationService.Listener { @@ -438,7 +438,7 @@ class SASTest : InstrumentedTest { bobVerificationService.addListener(bobListener) val bobUserId = bobSession.myUserId - val bobDeviceId = bobSession.getMyDevice().deviceId + val bobDeviceId = bobSession.cryptoService().getMyDevice().deviceId val verificationSAS = aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null) mTestHelper.await(aliceSASLatch) mTestHelper.await(bobSASLatch) @@ -449,7 +449,7 @@ class SASTest : InstrumentedTest { assertEquals("Should have same SAS", aliceTx.getShortCodeRepresentation(SasMode.DECIMAL), bobTx.getShortCodeRepresentation(SasMode.DECIMAL)) - cryptoTestData.close() + cryptoTestData.cleanUp(mTestHelper) } @Test @@ -459,8 +459,8 @@ class SASTest : InstrumentedTest { val aliceSession = cryptoTestData.firstSession val bobSession = cryptoTestData.secondSession - val aliceVerificationService = aliceSession.getVerificationService() - val bobVerificationService = bobSession!!.getVerificationService() + val aliceVerificationService = aliceSession.cryptoService().verificationService() + val bobVerificationService = bobSession!!.cryptoService().verificationService() val aliceSASLatch = CountDownLatch(1) val aliceListener = object : VerificationService.Listener { @@ -500,20 +500,20 @@ class SASTest : InstrumentedTest { bobVerificationService.addListener(bobListener) val bobUserId = bobSession.myUserId - val bobDeviceId = bobSession.getMyDevice().deviceId + val bobDeviceId = bobSession.cryptoService().getMyDevice().deviceId aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null) mTestHelper.await(aliceSASLatch) mTestHelper.await(bobSASLatch) // Assert that devices are verified - val bobDeviceInfoFromAlicePOV: CryptoDeviceInfo? = aliceSession.getDeviceInfo(bobUserId, bobDeviceId) - val aliceDeviceInfoFromBobPOV: CryptoDeviceInfo? = bobSession.getDeviceInfo(aliceSession.myUserId, aliceSession.getMyDevice().deviceId) + val bobDeviceInfoFromAlicePOV: CryptoDeviceInfo? = aliceSession.cryptoService().getDeviceInfo(bobUserId, bobDeviceId) + val aliceDeviceInfoFromBobPOV: CryptoDeviceInfo? = bobSession.cryptoService().getDeviceInfo(aliceSession.myUserId, aliceSession.cryptoService().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() + cryptoTestData.cleanUp(mTestHelper) } } diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/qrcode/HexParser.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/qrcode/HexParser.kt new file mode 100644 index 0000000000..387b627713 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/qrcode/HexParser.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 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.crypto.verification.qrcode + +fun hexToByteArray(hex: String): ByteArray { + // Remove all spaces + return hex.replace(" ", "") + .let { + if (it.length % 2 != 0) "0$it" else it + } + .let { + ByteArray(it.length / 2) + .apply { + for (i in this.indices) { + val index = i * 2 + val v = it.substring(index, index + 2).toInt(16) + this[i] = v.toByte() + } + } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeTest.kt new file mode 100644 index 0000000000..d19fad4b59 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeTest.kt @@ -0,0 +1,249 @@ +/* + * Copyright (c) 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.crypto.verification.qrcode + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import im.vector.matrix.android.InstrumentedTest +import org.amshove.kluent.shouldBeNull +import org.amshove.kluent.shouldEqual +import org.amshove.kluent.shouldEqualTo +import org.amshove.kluent.shouldNotBeNull +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class QrCodeTest : InstrumentedTest { + + private val qrCode1 = QrCodeData.VerifyingAnotherUser( + transactionId = "MaTransaction", + userMasterCrossSigningPublicKey = "ktEwcUP6su1xh+GuE+CYkQ3H6W/DIl+ybHFdaEOrolU", + otherUserMasterCrossSigningPublicKey = "TXluZKTZLvSRWOTPlOqLq534bA+/K4zLFKSu9cGLQaU", + sharedSecret = "MTIzNDU2Nzg" + ) + + private val value1 = "MATRIX\u0002\u0000\u0000\u000DMaTransaction\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢UMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008BA¥12345678" + + private val qrCode2 = QrCodeData.SelfVerifyingMasterKeyTrusted( + transactionId = "MaTransaction", + userMasterCrossSigningPublicKey = "ktEwcUP6su1xh+GuE+CYkQ3H6W/DIl+ybHFdaEOrolU", + otherDeviceKey = "TXluZKTZLvSRWOTPlOqLq534bA+/K4zLFKSu9cGLQaU", + sharedSecret = "MTIzNDU2Nzg" + ) + + private val value2 = "MATRIX\u0002\u0001\u0000\u000DMaTransaction\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢UMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008BA¥12345678" + + private val qrCode3 = QrCodeData.SelfVerifyingMasterKeyNotTrusted( + transactionId = "MaTransaction", + deviceKey = "TXluZKTZLvSRWOTPlOqLq534bA+/K4zLFKSu9cGLQaU", + userMasterCrossSigningPublicKey = "ktEwcUP6su1xh+GuE+CYkQ3H6W/DIl+ybHFdaEOrolU", + sharedSecret = "MTIzNDU2Nzg" + ) + + private val value3 = "MATRIX\u0002\u0002\u0000\u000DMaTransactionMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008BA¥\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢U12345678" + + private val sharedSecretByteArray = "12345678".toByteArray(Charsets.ISO_8859_1) + + private val tlx_byteArray = hexToByteArray("4d 79 6e 64 a4 d9 2e f4 91 58 e4 cf 94 ea 8b ab 9d f8 6c 0f bf 2b 8c cb 14 a4 ae f5 c1 8b 41 a5") + + private val kte_byteArray = hexToByteArray("92 d1 30 71 43 fa b2 ed 71 87 e1 ae 13 e0 98 91 0d c7 e9 6f c3 22 5f b2 6c 71 5d 68 43 ab a2 55") + + @Test + fun testEncoding1() { + qrCode1.toEncodedString() shouldEqual value1 + } + + @Test + fun testEncoding2() { + qrCode2.toEncodedString() shouldEqual value2 + } + + @Test + fun testEncoding3() { + qrCode3.toEncodedString() shouldEqual value3 + } + + @Test + fun testSymmetry1() { + qrCode1.toEncodedString().toQrCodeData() shouldEqual qrCode1 + } + + @Test + fun testSymmetry2() { + qrCode2.toEncodedString().toQrCodeData() shouldEqual qrCode2 + } + + @Test + fun testSymmetry3() { + qrCode3.toEncodedString().toQrCodeData() shouldEqual qrCode3 + } + + @Test + fun testCase1() { + val url = qrCode1.toEncodedString() + + val byteArray = url.toByteArray(Charsets.ISO_8859_1) + checkHeader(byteArray) + + // Mode + byteArray[7] shouldEqualTo 0 + + checkSizeAndTransaction(byteArray) + + compareArray(byteArray.copyOfRange(23, 23 + 32), kte_byteArray) + compareArray(byteArray.copyOfRange(23 + 32, 23 + 64), tlx_byteArray) + + compareArray(byteArray.copyOfRange(23 + 64, byteArray.size), sharedSecretByteArray) + } + + @Test + fun testCase2() { + val url = qrCode2.toEncodedString() + + val byteArray = url.toByteArray(Charsets.ISO_8859_1) + checkHeader(byteArray) + + // Mode + byteArray[7] shouldEqualTo 1 + + checkSizeAndTransaction(byteArray) + compareArray(byteArray.copyOfRange(23, 23 + 32), kte_byteArray) + compareArray(byteArray.copyOfRange(23 + 32, 23 + 64), tlx_byteArray) + + compareArray(byteArray.copyOfRange(23 + 64, byteArray.size), sharedSecretByteArray) + } + + @Test + fun testCase3() { + val url = qrCode3.toEncodedString() + + val byteArray = url.toByteArray(Charsets.ISO_8859_1) + checkHeader(byteArray) + + // Mode + byteArray[7] shouldEqualTo 2 + + checkSizeAndTransaction(byteArray) + compareArray(byteArray.copyOfRange(23, 23 + 32), tlx_byteArray) + compareArray(byteArray.copyOfRange(23 + 32, 23 + 64), kte_byteArray) + + compareArray(byteArray.copyOfRange(23 + 64, byteArray.size), sharedSecretByteArray) + } + + @Test + fun testLongTransactionId() { + // Size on two bytes (2_000 = 0x07D0) + val longTransactionId = "PatternId_".repeat(200) + + val qrCode = qrCode1.copy(transactionId = longTransactionId) + + val result = qrCode.toEncodedString() + val expected = value1.replace("\u0000\u000DMaTransaction", "\u0007\u00D0$longTransactionId") + + result shouldEqual expected + + // Reverse operation + expected.toQrCodeData() shouldEqual qrCode + } + + @Test + fun testAnyTransactionId() { + for (qty in 0 until 0x1FFF step 200) { + val longTransactionId = "a".repeat(qty) + + val qrCode = qrCode1.copy(transactionId = longTransactionId) + + // Symmetric operation + qrCode.toEncodedString().toQrCodeData() shouldEqual qrCode + } + } + + // Error cases + @Test + fun testErrorHeader() { + value1.replace("MATRIX", "MOTRIX").toQrCodeData().shouldBeNull() + value1.replace("MATRIX", "MATRI").toQrCodeData().shouldBeNull() + value1.replace("MATRIX", "").toQrCodeData().shouldBeNull() + } + + @Test + fun testErrorVersion() { + value1.replace("MATRIX\u0002", "MATRIX\u0000").toQrCodeData().shouldBeNull() + value1.replace("MATRIX\u0002", "MATRIX\u0001").toQrCodeData().shouldBeNull() + value1.replace("MATRIX\u0002", "MATRIX\u0003").toQrCodeData().shouldBeNull() + value1.replace("MATRIX\u0002", "MATRIX").toQrCodeData().shouldBeNull() + } + + @Test + fun testErrorSecretTooShort() { + value1.replace("12345678", "1234567").toQrCodeData().shouldBeNull() + } + + @Test + fun testErrorNoTransactionNoKeyNoSecret() { + // But keep transaction length + "MATRIX\u0002\u0000\u0000\u000D".toQrCodeData().shouldBeNull() + } + + @Test + fun testErrorNoKeyNoSecret() { + "MATRIX\u0002\u0000\u0000\u000DMaTransaction".toQrCodeData().shouldBeNull() + } + + @Test + fun testErrorTransactionLengthTooShort() { + // In this case, the secret will be longer, so this is not an error, but it will lead to keys mismatch + value1.replace("\u000DMaTransaction", "\u000CMaTransaction").toQrCodeData().shouldNotBeNull() + } + + @Test + fun testErrorTransactionLengthTooBig() { + value1.replace("\u000DMaTransaction", "\u000EMaTransaction").toQrCodeData().shouldBeNull() + } + + private fun compareArray(actual: ByteArray, expected: ByteArray) { + actual.size shouldEqual expected.size + + for (i in actual.indices) { + actual[i] shouldEqualTo expected[i] + } + } + + private fun checkHeader(byteArray: ByteArray) { + // MATRIX + byteArray[0] shouldEqualTo 'M'.toByte() + byteArray[1] shouldEqualTo 'A'.toByte() + byteArray[2] shouldEqualTo 'T'.toByte() + byteArray[3] shouldEqualTo 'R'.toByte() + byteArray[4] shouldEqualTo 'I'.toByte() + byteArray[5] shouldEqualTo 'X'.toByte() + + // Version + byteArray[6] shouldEqualTo 2 + } + + private fun checkSizeAndTransaction(byteArray: ByteArray) { + // Size + byteArray[8] shouldEqualTo 0 + byteArray[9] shouldEqualTo 13 + + // Transaction + byteArray.copyOfRange(10, 10 + "MaTransaction".length).toString(Charsets.ISO_8859_1) shouldEqual "MaTransaction" + } +} diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/qrcode/SharedSecretTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/qrcode/SharedSecretTest.kt index 7a07c16d14..4ab79be18b 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/qrcode/SharedSecretTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/qrcode/SharedSecretTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 New Vector Ltd + * Copyright (c) 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. @@ -32,14 +32,14 @@ class SharedSecretTest : InstrumentedTest { @Test fun testSharedSecretLengthCase() { repeat(100) { - generateSharedSecret().length shouldBe 43 + generateSharedSecretV2().length shouldBe 11 } } @Test fun testSharedDiffCase() { - val sharedSecret1 = generateSharedSecret() - val sharedSecret2 = generateSharedSecret() + val sharedSecret1 = generateSharedSecretV2() + val sharedSecret2 = generateSharedSecretV2() sharedSecret1 shouldNotBeEqualTo sharedSecret2 } diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/qrcode/VerificationTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/qrcode/VerificationTest.kt index 61ea0f35b4..e5ffc2ae01 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/qrcode/VerificationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/qrcode/VerificationTest.kt @@ -18,8 +18,8 @@ package im.vector.matrix.android.internal.crypto.verification.qrcode import androidx.test.ext.junit.runners.AndroidJUnit4 import im.vector.matrix.android.InstrumentedTest -import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod -import im.vector.matrix.android.api.session.crypto.sas.VerificationService +import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod +import im.vector.matrix.android.api.session.crypto.verification.VerificationService import im.vector.matrix.android.common.CommonTestHelper import im.vector.matrix.android.common.CryptoTestHelper import im.vector.matrix.android.common.TestConstants @@ -156,7 +156,7 @@ class VerificationTest : InstrumentedTest { val bobSession = cryptoTestData.secondSession!! mTestHelper.doSync { callback -> - aliceSession.getCrossSigningService() + aliceSession.cryptoService().crossSigningService() .initializeCrossSigning(UserPasswordAuth( user = aliceSession.myUserId, password = TestConstants.PASSWORD @@ -164,15 +164,15 @@ class VerificationTest : InstrumentedTest { } mTestHelper.doSync { callback -> - bobSession.getCrossSigningService() + bobSession.cryptoService().crossSigningService() .initializeCrossSigning(UserPasswordAuth( user = bobSession.myUserId, password = TestConstants.PASSWORD ), callback) } - val aliceVerificationService = aliceSession.getVerificationService() - val bobVerificationService = bobSession.getVerificationService() + val aliceVerificationService = aliceSession.cryptoService().verificationService() + val bobVerificationService = bobSession.cryptoService().verificationService() var aliceReadyPendingVerificationRequest: PendingVerificationRequest? = null var bobReadyPendingVerificationRequest: PendingVerificationRequest? = null @@ -227,6 +227,6 @@ class VerificationTest : InstrumentedTest { pr.otherCanScanQrCode() shouldBe expectedResultForBob.otherCanScanQrCode } - cryptoTestData.close() + cryptoTestData.cleanUp(mTestHelper) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixPatterns.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixPatterns.kt index e30d139cd3..bba925b89f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixPatterns.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixPatterns.kt @@ -122,9 +122,9 @@ object MatrixPatterns { */ fun isEventId(str: String?): Boolean { return str != null - && (str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER - || str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3 - || str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4) + && (str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER + || str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3 + || str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4) } /** @@ -144,14 +144,6 @@ object MatrixPatterns { * @return null if not found or if matrixId is null */ fun extractServerNameFromId(matrixId: String?): String? { - if (matrixId == null) { - return null - } - - val index = matrixId.indexOf(":") - - return if (index == -1) { - null - } else matrixId.substring(index + 1) + return matrixId?.substringAfter(":", missingDelimiterValue = "")?.takeIf { it.isNotEmpty() } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnown.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnown.kt index 6285e866cc..bdad4702b7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnown.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnown.kt @@ -46,13 +46,13 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class WellKnown( @Json(name = "m.homeserver") - var homeServer: WellKnownBaseConfig? = null, + val homeServer: WellKnownBaseConfig? = null, @Json(name = "m.identity_server") - var identityServer: WellKnownBaseConfig? = null, + val identityServer: WellKnownBaseConfig? = null, @Json(name = "m.integrations") - var integrations: Map? = null + val integrations: Map? = null ) { /** * Returns the list of integration managers proposed diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/crypto/Emojis.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/crypto/Emojis.kt index 943b2c1b10..469721a4c8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/crypto/Emojis.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/crypto/Emojis.kt @@ -16,7 +16,7 @@ package im.vector.matrix.android.api.crypto -import im.vector.matrix.android.api.session.crypto.sas.EmojiRepresentation +import im.vector.matrix.android.api.session.crypto.verification.EmojiRepresentation import im.vector.matrix.android.internal.crypto.verification.getEmojiForCode /** 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 23e8c70386..6ce04f8b30 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 @@ -16,7 +16,6 @@ package im.vector.matrix.android.api.extensions -import im.vector.matrix.android.api.comparators.DatedObjectComparators import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo @@ -33,7 +32,5 @@ fun CryptoDeviceInfo.getFingerprintHumanReadable() = fingerprint() * ========================================================================================== */ fun List.sortByLastSeen(): List { - val list = toMutableList() - list.sortWith(DatedObjectComparators.descComparator) - return list + return this.sortedByDescending { it.lastSeenTs ?: 0 } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/listeners/ProgressListener.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/listeners/ProgressListener.kt index ad47260f86..d672434046 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/listeners/ProgressListener.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/listeners/ProgressListener.kt @@ -21,6 +21,7 @@ package im.vector.matrix.android.api.listeners */ interface ProgressListener { /** + * Will be invoked on the background thread, not in UI thread. * @param progress from 0 to total by contract * @param total */ 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 5bd219247c..c2fa7d2d32 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 @@ -21,6 +21,7 @@ import androidx.lifecycle.LiveData import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.failure.GlobalError import im.vector.matrix.android.api.pushrules.PushRuleService +import im.vector.matrix.android.api.session.accountdata.AccountDataService import im.vector.matrix.android.api.session.cache.CacheService import im.vector.matrix.android.api.session.content.ContentUploadStateTracker import im.vector.matrix.android.api.session.content.ContentUrlResolver @@ -33,6 +34,7 @@ import im.vector.matrix.android.api.session.pushers.PushersService import im.vector.matrix.android.api.session.room.RoomDirectoryService import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.api.session.securestorage.SecureStorageService +import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService import im.vector.matrix.android.api.session.signout.SignOutService import im.vector.matrix.android.api.session.sync.FilterService import im.vector.matrix.android.api.session.sync.SyncState @@ -47,7 +49,6 @@ interface Session : RoomDirectoryService, GroupService, UserService, - CryptoService, CacheService, SignOutService, FilterService, @@ -57,7 +58,8 @@ interface Session : PushersService, InitialSyncProgressService, HomeServerCapabilitiesService, - SecureStorageService { + SecureStorageService, + AccountDataService { /** * The params associated to the session @@ -136,6 +138,11 @@ interface Session : */ fun contentUploadProgressTracker(): ContentUploadStateTracker + /** + * Returns the cryptoService associated with the session + */ + fun cryptoService(): CryptoService + /** * Add a listener to the session. * @param listener the listener to add. @@ -159,4 +166,6 @@ interface Session : */ fun onGlobalError(globalError: GlobalError) } + + val sharedSecretStorageService: SharedSecretStorageService } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/accountdata/AccountDataService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/accountdata/AccountDataService.kt new file mode 100644 index 0000000000..ee13d1f097 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/accountdata/AccountDataService.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 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.accountdata + +import androidx.lifecycle.LiveData +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.events.model.Content +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.api.util.Optional +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent + +interface AccountDataService { + /** + * Retrieve the account data with the provided type or null if not found + */ + fun getAccountDataEvent(type: String): UserAccountDataEvent? + + /** + * Observe the account data with the provided type + */ + fun getLiveAccountDataEvent(type: String): LiveData> + + /** + * Retrieve the account data with the provided types. The return list can have a different size that + * the size of the types set, because some AccountData may not exist. + * If an empty set is provided, all the AccountData are retrieved + */ + fun getAccountDataEvents(types: Set): List + + /** + * Observe the account data with the provided types. If an empty set is provided, all the AccountData are observed + */ + fun getLiveAccountDataEvents(types: Set): LiveData> + + /** + * Update the account data with the provided type and the provided account data content + */ + fun updateAccountData(type: String, content: Content, callback: MatrixCallback? = null): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt index 0d8ef2c52b..48dff4e56d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt @@ -29,6 +29,7 @@ data class ContentAttachmentData( val width: Long? = 0, val exifOrientation: Int = ExifInterface.ORIENTATION_UNDEFINED, val name: String? = null, + val queryUri: String, val path: String, val mimeType: String?, val type: Type 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 46539d9029..1360924270 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 @@ -23,7 +23,7 @@ import im.vector.matrix.android.api.listeners.ProgressListener import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener -import im.vector.matrix.android.api.session.crypto.sas.VerificationService +import im.vector.matrix.android.api.session.crypto.verification.VerificationService 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.internal.crypto.MXEventDecryptionResult @@ -40,6 +40,12 @@ import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody interface CryptoService { + fun verificationService(): VerificationService + + fun crossSigningService(): CrossSigningService + + fun keysBackupService(): KeysBackupService + fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback) fun deleteDevice(deviceId: String, callback: MatrixCallback) @@ -50,12 +56,6 @@ interface CryptoService { fun isCryptoEnabled(): Boolean - fun getVerificationService(): VerificationService - - fun getCrossSigningService(): CrossSigningService - - fun getKeysBackupService(): KeysBackupService - fun isRoomBlacklistUnverifiedDevices(roomId: String?): Boolean fun setWarnOnUnknownDevices(warn: Boolean) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningService.kt index b7bc20a1dc..ff4745ef46 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningService.kt @@ -42,6 +42,10 @@ interface CrossSigningService { fun initializeCrossSigning(authParams: UserPasswordAuth?, callback: MatrixCallback? = null) + fun checkTrustFromPrivateKeys(masterKeyPrivateKey: String?, + uskKeyPrivateKey: String?, + sskPrivateKey: String?) : UserTrustResult + fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? fun getLiveCrossSigningKeys(userId: String): LiveData> @@ -53,11 +57,13 @@ interface CrossSigningService { fun trustUser(otherUserId: String, callback: MatrixCallback) + fun markMyMasterKeyAsTrusted() + /** * Sign one of your devices and upload the signature */ - fun signDevice(deviceId: String, - callback: MatrixCallback) + fun trustDevice(deviceId: String, + callback: MatrixCallback) fun checkDeviceTrust(otherUserId: String, otherDeviceId: String, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningSsssSecretConstants.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningSsssSecretConstants.kt new file mode 100644 index 0000000000..d46a724463 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningSsssSecretConstants.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 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.crypto.crosssigning + +const val MASTER_KEY_SSSS_NAME = "m.cross_signing.master" + +const val USER_SIGNING_KEY_SSSS_NAME = "m.cross_signing.user_signing" + +const val SELF_SIGNING_KEY_SSSS_NAME = "m.cross_signing.self_signing" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/CancelCode.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/CancelCode.kt similarity index 95% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/CancelCode.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/CancelCode.kt index 79448de83f..35961b309f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/CancelCode.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/CancelCode.kt @@ -14,8 +14,7 @@ * limitations under the License. */ -// TODO Rename package -package im.vector.matrix.android.api.session.crypto.sas +package im.vector.matrix.android.api.session.crypto.verification enum class CancelCode(val value: String, val humanReadable: String) { User("m.user", "the user cancelled the verification"), diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/EmojiRepresentation.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/EmojiRepresentation.kt similarity index 92% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/EmojiRepresentation.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/EmojiRepresentation.kt index 031610f0c6..9ee7c92788 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/EmojiRepresentation.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/EmojiRepresentation.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.api.session.crypto.sas +package im.vector.matrix.android.api.session.crypto.verification import androidx.annotation.StringRes diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/IncomingSasVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/IncomingSasVerificationTransaction.kt similarity index 93% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/IncomingSasVerificationTransaction.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/IncomingSasVerificationTransaction.kt index 8e349416dc..e3ba3dc4c0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/IncomingSasVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/IncomingSasVerificationTransaction.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.api.session.crypto.sas +package im.vector.matrix.android.api.session.crypto.verification interface IncomingSasVerificationTransaction : SasVerificationTransaction { val uxState: UxState diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/OutgoingSasVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/OutgoingSasVerificationTransaction.kt similarity index 93% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/OutgoingSasVerificationTransaction.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/OutgoingSasVerificationTransaction.kt index 7ab386295a..67e88f5648 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/OutgoingSasVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/OutgoingSasVerificationTransaction.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.api.session.crypto.sas +package im.vector.matrix.android.api.session.crypto.verification interface OutgoingSasVerificationTransaction : SasVerificationTransaction { val uxState: UxState diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/QrCodeVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/QrCodeVerificationTransaction.kt similarity index 94% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/QrCodeVerificationTransaction.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/QrCodeVerificationTransaction.kt index ef6462f854..516f7f0712 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/QrCodeVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/QrCodeVerificationTransaction.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.api.session.crypto.sas +package im.vector.matrix.android.api.session.crypto.verification interface QrCodeVerificationTransaction : VerificationTransaction { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasMode.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/SasMode.kt similarity index 91% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasMode.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/SasMode.kt index f58485decd..c346a07228 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasMode.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/SasMode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.api.session.crypto.sas +package im.vector.matrix.android.api.session.crypto.verification object SasMode { const val DECIMAL = "decimal" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/SasVerificationTransaction.kt similarity index 94% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationTransaction.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/SasVerificationTransaction.kt index 912e2b65e8..d1778f19a0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/SasVerificationTransaction.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.api.session.crypto.sas +package im.vector.matrix.android.api.session.crypto.verification interface SasVerificationTransaction : VerificationTransaction { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationMethod.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/VerificationMethod.kt similarity index 93% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationMethod.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/VerificationMethod.kt index b8f0f23891..3cd3aee90d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationMethod.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/VerificationMethod.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.api.session.crypto.sas +package im.vector.matrix.android.api.session.crypto.verification /** * Verification methods diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/VerificationService.kt similarity index 98% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationService.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/VerificationService.kt index 1b5f5d3dd6..d65bd7bb6a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/VerificationService.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.api.session.crypto.sas +package im.vector.matrix.android.api.session.crypto.verification import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.events.model.LocalEcho diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/VerificationTransaction.kt similarity index 93% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationTransaction.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/VerificationTransaction.kt index 6ed650b2ad..13aae70b5b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/VerificationTransaction.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.api.session.crypto.sas +package im.vector.matrix.android.api.session.crypto.verification interface VerificationTransaction { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationTxState.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/VerificationTxState.kt similarity index 96% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationTxState.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/VerificationTxState.kt index b30dde2d4d..aaaf227187 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationTxState.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/VerificationTxState.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.api.session.crypto.sas +package im.vector.matrix.android.api.session.crypto.verification sealed class VerificationTxState { // Uninitialized state 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 8878930de0..9a3107a8ca 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 @@ -66,6 +66,9 @@ object EventType { const val ROOM_KEY_REQUEST = "m.room_key_request" const val FORWARDED_ROOM_KEY = "m.forwarded_room_key" + const val REQUEST_SECRET = "m.secret.request" + const val SEND_SECRET = "m.secret.send" + // Interactive key verification const val KEY_VERIFICATION_START = "m.key.verification.start" const val KEY_VERIFICATION_ACCEPT = "m.key.verification.accept" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/LocalEcho.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/LocalEcho.kt index 1dbee475e0..67ae3b2d73 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/LocalEcho.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/LocalEcho.kt @@ -20,7 +20,7 @@ import java.util.UUID object LocalEcho { - private const val PREFIX = "local." + private const val PREFIX = "\$local." fun isLocalEchoId(eventId: String) = eventId.startsWith(PREFIX) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomDirectoryService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomDirectoryService.kt index c0e413f83b..0273c789dd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomDirectoryService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomDirectoryService.kt @@ -35,9 +35,9 @@ interface RoomDirectoryService { callback: MatrixCallback): Cancelable /** - * Join a room by id + * Join a room by id, or room alias */ - fun joinRoom(roomId: String, + fun joinRoom(roomIdOrAlias: String, reason: String? = null, callback: MatrixCallback): Cancelable 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 9fec605bd9..93761dfd26 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 @@ -36,11 +36,11 @@ interface RoomService { /** * Join a room by id - * @param roomId the roomId of the room to join + * @param roomIdOrAlias the roomId or the room alias of the room to join * @param reason optional reason for joining the room * @param viaServers the servers to attempt to join the room through. One of the servers must be participating in the room. */ - fun joinRoom(roomId: String, + fun joinRoom(roomIdOrAlias: String, reason: String? = null, viaServers: List = emptyList(), callback: MatrixCallback): Cancelable diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReferencesAggregatedContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReferencesAggregatedContent.kt index ae6e52a091..31a5694682 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReferencesAggregatedContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReferencesAggregatedContent.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.api.session.room.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.session.room.VerificationState /** * Contains an aggregated summary info of the references. @@ -26,6 +27,6 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class ReferencesAggregatedContent( // Verification status info for m.key.verification.request msgType events - @Json(name = "verif_sum") val verificationSummary: String + @Json(name = "verif_sum") val verificationState: VerificationState // Add more fields for future summary info. ) 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 28c56125f1..6171b2633b 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 @@ -45,7 +45,8 @@ data class RoomSummary constructor( val versioningState: VersioningState = VersioningState.NONE, val readMarkerId: String? = null, val userDrafts: List = emptyList(), - var isEncrypted: Boolean, + val isEncrypted: Boolean, + val inviterId: String? = null, val typingRoomMemberIds: List = emptyList(), val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS, // TODO Plug it 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 b69c189f89..1abbe9ef3a 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 @@ -17,6 +17,7 @@ package im.vector.matrix.android.api.session.room.model.create import android.util.Patterns +import androidx.annotation.CheckResult import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.MatrixPatterns.isUserId @@ -120,37 +121,53 @@ data class CreateRoomParams( @Json(name = "power_level_content_override") val powerLevelContentOverride: PowerLevelsContent? = null ) { - /** - * Set to true means that if cross-signing is enabled and we can get keys for every invited users, - * the encryption will be enabled on the created room - */ @Transient internal var enableEncryptionIfInvitedUsersSupportIt: Boolean = false private set - fun enableEncryptionIfInvitedUsersSupportIt(): CreateRoomParams { - enableEncryptionIfInvitedUsersSupportIt = true + /** + * After calling this method, when the room will be created, if cross-signing is enabled and we can get keys for every invited users, + * the encryption will be enabled on the created room + * @param value true to activate this behavior. + * @return this, to allow chaining methods + */ + fun enableEncryptionIfInvitedUsersSupportIt(value: Boolean = true): CreateRoomParams { + enableEncryptionIfInvitedUsersSupportIt = value return this } /** * Add the crypto algorithm to the room creation parameters. * - * @param algorithm the algorithm + * @param enable true to enable encryption. + * @param algorithm the algorithm, default to [MXCRYPTO_ALGORITHM_MEGOLM], which is actually the only supported algorithm for the moment + * @return a modified copy of the CreateRoomParams object, or this if there is no modification */ - fun enableEncryptionWithAlgorithm(algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM): CreateRoomParams { + @CheckResult + fun enableEncryptionWithAlgorithm(enable: Boolean = true, + algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM): CreateRoomParams { + // Remove the existing value if any. + val newInitialStates = initialStates + ?.filter { it.type != EventType.STATE_ROOM_ENCRYPTION } + return if (algorithm == MXCRYPTO_ALGORITHM_MEGOLM) { - val contentMap = mapOf("algorithm" to algorithm) + if (enable) { + val contentMap = mapOf("algorithm" to algorithm) - val algoEvent = Event( - type = EventType.STATE_ROOM_ENCRYPTION, - stateKey = "", - content = contentMap.toContent() - ) + val algoEvent = Event( + type = EventType.STATE_ROOM_ENCRYPTION, + stateKey = "", + content = contentMap.toContent() + ) - copy( - initialStates = initialStates.orEmpty().filter { it.type != EventType.STATE_ROOM_ENCRYPTION } + algoEvent - ) + copy( + initialStates = newInitialStates.orEmpty() + algoEvent + ) + } else { + return copy( + initialStates = newInitialStates + ) + } } else { Timber.e("Unsupported algorithm: $algorithm") this @@ -161,7 +178,9 @@ data class CreateRoomParams( * Force the history visibility in the room creation parameters. * * @param historyVisibility the expected history visibility, set null to remove any existing value. + * @return a modified copy of the CreateRoomParams object */ + @CheckResult fun setHistoryVisibility(historyVisibility: RoomHistoryVisibility?): CreateRoomParams { // Remove the existing value if any. val newInitialStates = initialStates @@ -187,7 +206,9 @@ data class CreateRoomParams( /** * Mark as a direct message room. + * @return a modified copy of the CreateRoomParams object */ + @CheckResult fun setDirectMessage(): CreateRoomParams { return copy( preset = CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT, @@ -195,20 +216,6 @@ data class CreateRoomParams( ) } - /** - * @return the invite count - */ - private fun getInviteCount(): Int { - return invitedUserIds?.size ?: 0 - } - - /** - * @return the pid invite count - */ - private fun getInvite3PidCount(): Int { - return invite3pids?.size ?: 0 - } - /** * Tells if the created room can be a direct chat one. * @@ -217,7 +224,6 @@ data class CreateRoomParams( fun isDirect(): Boolean { return preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT && isDirect == true - && (1 == getInviteCount() || 1 == getInvite3PidCount()) } /** @@ -232,7 +238,9 @@ data class CreateRoomParams( * ids might be a matrix id or an email address. * * @param ids the participant ids to add. + * @return a modified copy of the CreateRoomParams object */ + @CheckResult fun addParticipantIds(hsConfig: HomeServerConnectionConfig, userId: String, ids: List): CreateRoomParams { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomResponse.kt index c437bcfbf1..da54b344a2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomResponse.kt @@ -21,5 +21,10 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) internal data class CreateRoomResponse( - @Json(name = "room_id") var roomId: String? = null + /** + * Required. The created room's ID. + */ + @Json(name = "room_id") val roomId: String ) + +internal typealias JoinRoomResponse = CreateRoomResponse diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationCancelContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationCancelContent.kt index 651a25e175..9b7ca13df8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationCancelContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationCancelContent.kt @@ -17,7 +17,7 @@ package im.vector.matrix.android.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import im.vector.matrix.android.api.session.crypto.sas.CancelCode +import im.vector.matrix.android.api.session.crypto.verification.CancelCode import im.vector.matrix.android.api.session.events.model.RelationType import im.vector.matrix.android.api.session.events.model.toContent import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationStartContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationStartContent.kt index 3031b213d9..a2a3940199 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationStartContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationStartContent.kt @@ -17,7 +17,7 @@ package im.vector.matrix.android.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import im.vector.matrix.android.api.session.crypto.sas.SasMode +import im.vector.matrix.android.api.session.crypto.verification.SasMode import im.vector.matrix.android.api.session.events.model.toContent import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/roomdirectory/PublicRoom.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/roomdirectory/PublicRoom.kt index 508cbb55b4..efe2debeab 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/roomdirectory/PublicRoom.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/roomdirectory/PublicRoom.kt @@ -23,66 +23,71 @@ import com.squareup.moshi.JsonClass */ @JsonClass(generateAdapter = true) data class PublicRoom( - /** * Aliases of the room. May be empty. */ @Json(name = "aliases") - var aliases: List? = null, + val aliases: List? = null, /** * The canonical alias of the room, if any. */ @Json(name = "canonical_alias") - var canonicalAlias: String? = null, + val canonicalAlias: String? = null, /** * The name of the room, if any. */ @Json(name = "name") - var name: String? = null, + val name: String? = null, /** * Required. The number of members joined to the room. */ @Json(name = "num_joined_members") - var numJoinedMembers: Int = 0, + val numJoinedMembers: Int = 0, /** * Required. The ID of the room. */ @Json(name = "room_id") - var roomId: String, + val roomId: String, /** * The topic of the room, if any. */ @Json(name = "topic") - var topic: String? = null, + val topic: String? = null, /** * Required. Whether the room may be viewed by guest users without joining. */ @Json(name = "world_readable") - var worldReadable: Boolean = false, + val worldReadable: Boolean = false, /** * Required. Whether guest users may join the room and participate in it. If they can, * they will be subject to ordinary power level rules like any other user. */ @Json(name = "guest_can_join") - var guestCanJoin: Boolean = false, + val guestCanJoin: Boolean = false, /** * The URL for the room's avatar, if one is set. */ @Json(name = "avatar_url") - var avatarUrl: String? = null, + val avatarUrl: String? = null, /** * Undocumented item */ @Json(name = "m.federate") - var isFederated: Boolean = false - -) + val isFederated: Boolean = false +) { + /** + * Return the canonical alias, or the first alias from the list of aliases, or null + */ + fun getPrimaryAlias(): String? { + return canonicalAlias ?: aliases?.firstOrNull() + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/roomdirectory/PublicRoomsFilter.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/roomdirectory/PublicRoomsFilter.kt index b4de72e41a..c519d054f2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/roomdirectory/PublicRoomsFilter.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/roomdirectory/PublicRoomsFilter.kt @@ -27,5 +27,5 @@ data class PublicRoomsFilter( * A string to search for in the room metadata, e.g. name, topic, canonical alias etc. (Optional). */ @Json(name = "generic_search_term") - var searchTerm: String? = null + val searchTerm: String? = null ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/roomdirectory/PublicRoomsParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/roomdirectory/PublicRoomsParams.kt index e2af1c3ccb..467968cd2a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/roomdirectory/PublicRoomsParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/roomdirectory/PublicRoomsParams.kt @@ -28,30 +28,30 @@ data class PublicRoomsParams( * Limit the number of results returned. */ @Json(name = "limit") - var limit: Int? = null, + val limit: Int? = null, /** * A pagination token from a previous request, allowing clients to get the next (or previous) batch of rooms. * The direction of pagination is specified solely by which token is supplied, rather than via an explicit flag. */ @Json(name = "since") - var since: String? = null, + val since: String? = null, /** * Filter to apply to the results. */ @Json(name = "filter") - var filter: PublicRoomsFilter? = null, + val filter: PublicRoomsFilter? = null, /** * Whether or not to include all known networks/protocols from application services on the homeserver. Defaults to false. */ @Json(name = "include_all_networks") - var includeAllNetworks: Boolean = false, + val includeAllNetworks: Boolean = false, /** * The specific third party network/protocol to request from the homeserver. Can only be used if include_all_networks is false. */ @Json(name = "third_party_instance_id") - var thirdPartyInstanceId: String? = null + val thirdPartyInstanceId: String? = null ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/roomdirectory/PublicRoomsResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/roomdirectory/PublicRoomsResponse.kt index 3799d097fc..b83fa51491 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/roomdirectory/PublicRoomsResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/roomdirectory/PublicRoomsResponse.kt @@ -27,24 +27,24 @@ data class PublicRoomsResponse( * A pagination token for the response. The absence of this token means there are no more results to fetch and the client should stop paginating. */ @Json(name = "next_batch") - var nextBatch: String? = null, + val nextBatch: String? = null, /** * A pagination token that allows fetching previous results. The absence of this token means there are no results before this batch, * i.e. this is the first batch. */ @Json(name = "prev_batch") - var prevBatch: String? = null, + val prevBatch: String? = null, /** * A paginated chunk of public rooms. */ @Json(name = "chunk") - var chunk: List? = null, + val chunk: List? = null, /** * An estimate on the total number of public rooms, if the server has an estimate. */ @Json(name = "total_room_count_estimate") - var totalRoomCountEstimate: Int? = null + val totalRoomCountEstimate: Int? = null ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/thirdparty/ThirdPartyProtocol.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/thirdparty/ThirdPartyProtocol.kt index b066cff164..b4ed1f1a8e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/thirdparty/ThirdPartyProtocol.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/thirdparty/ThirdPartyProtocol.kt @@ -26,7 +26,7 @@ data class ThirdPartyProtocol( * where higher groupings are ordered first. For example, the name of a network should be searched before the nickname of a user. */ @Json(name = "user_fields") - var userFields: List? = null, + val userFields: List? = null, /** * Required. Fields which may be used to identify a third party location. These should be ordered to suggest the way that @@ -34,15 +34,15 @@ data class ThirdPartyProtocol( * searched before the name of a channel. */ @Json(name = "location_fields") - var locationFields: List? = null, + val locationFields: List? = null, /** * Required. A content URI representing an icon for the third party protocol. * - * FIXDOC: This field was not present in legacy Riot, and it is sometimes sent by the server (no not Required?) + * FIXDOC: This field was not present in legacy Riot, and it is sometimes sent by the server (so not Required?) */ @Json(name = "icon") - var icon: String? = null, + val icon: String? = null, /** * Required. The type definitions for the fields defined in the user_fields and location_fields. Each entry in those arrays MUST have an entry here. @@ -51,12 +51,12 @@ data class ThirdPartyProtocol( * May be an empty object if no fields are defined. */ @Json(name = "field_types") - var fieldTypes: Map? = null, + val fieldTypes: Map? = null, /** * Required. A list of objects representing independent instances of configuration. For example, multiple networks on IRC * if multiple are provided by the same application service. */ @Json(name = "instances") - var instances: List? = null + val instances: List? = null ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/thirdparty/ThirdPartyProtocolInstance.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/thirdparty/ThirdPartyProtocolInstance.kt index 50f92356fb..f5d59f9282 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/thirdparty/ThirdPartyProtocolInstance.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/thirdparty/ThirdPartyProtocolInstance.kt @@ -25,35 +25,35 @@ data class ThirdPartyProtocolInstance( * Required. A human-readable description for the protocol, such as the name. */ @Json(name = "desc") - var desc: String? = null, + val desc: String? = null, /** * An optional content URI representing the protocol. Overrides the one provided at the higher level Protocol object. */ @Json(name = "icon") - var icon: String? = null, + val icon: String? = null, /** * Required. Preset values for fields the client may use to search by. */ @Json(name = "fields") - var fields: Map? = null, + val fields: Map? = null, /** * Required. A unique identifier across all instances. */ @Json(name = "network_id") - var networkId: String? = null, + val networkId: String? = null, /** * FIXDOC Not documented on matrix.org doc */ @Json(name = "instance_id") - var instanceId: String? = null, + val instanceId: String? = null, /** * FIXDOC Not documented on matrix.org doc */ @Json(name = "bot_user_id") - var botUserId: String? = null + val botUserId: String? = null ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/tombstone/RoomTombstoneContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/tombstone/RoomTombstoneContent.kt index 035e76d10f..4dfcdd86f5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/tombstone/RoomTombstoneContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/tombstone/RoomTombstoneContent.kt @@ -23,6 +23,13 @@ import com.squareup.moshi.JsonClass */ @JsonClass(generateAdapter = true) data class RoomTombstoneContent( + /** + * Required. A server-defined message. + */ @Json(name = "body") val body: String? = null, - @Json(name = "replacement_room") val replacementRoom: String? + + /** + * Required. The new room the client should be visiting. + */ + @Json(name = "replacement_room") val replacementRoomId: String? ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt index e6c32193f4..afa3dda496 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt @@ -51,16 +51,26 @@ interface SendService { /** * Method to send a media asynchronously. * @param attachment the media to send + * @param compressBeforeSending set to true to compress images before sending them + * @param roomIds set of roomIds to where the media will be sent. The current roomId will be add to this set if not present. + * It can be useful to send media to multiple room. It's safe to include the current roomId in this set * @return a [Cancelable] */ - fun sendMedia(attachment: ContentAttachmentData): Cancelable + fun sendMedia(attachment: ContentAttachmentData, + compressBeforeSending: Boolean, + roomIds: Set): Cancelable /** * Method to send a list of media asynchronously. * @param attachments the list of media to send + * @param compressBeforeSending set to true to compress images before sending them + * @param roomIds set of roomIds to where the media will be sent. The current roomId will be add to this set if not present. + * It can be useful to send media to multiple room. It's safe to include the current roomId in this set * @return a [Cancelable] */ - fun sendMedias(attachments: List): Cancelable + fun sendMedias(attachments: List, + compressBeforeSending: Boolean, + roomIds: Set): Cancelable /** * Send a poll to the room. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt index 164afb3a60..eb4a9b59e4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt @@ -41,12 +41,12 @@ interface Timeline { fun removeAllListeners() /** - * This should be called before any other method after creating the timeline. It ensures the underlying database is open + * This must be called before any other method after creating the timeline. It ensures the underlying database is open */ fun start() /** - * This should be called when you don't need the timeline. It ensures the underlying database get closed. + * This must be called when you don't need the timeline. It ensures the underlying database get closed. */ fun dispose() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt index 2b23ee40ca..a69127532e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt @@ -27,6 +27,9 @@ interface TimelineService { /** * Instantiate a [Timeline] with an optional initial eventId, to be used with permalink. * You can also configure some settings with the [settings] param. + * + * Important: the returned Timeline has to be started + * * @param eventId the optional initial eventId. * @param settings settings to configure the timeline. * @return the instantiated timeline diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/EncryptedSecretContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/EncryptedSecretContent.kt new file mode 100644 index 0000000000..57fd652735 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/EncryptedSecretContent.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 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.securestorage + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.di.MoshiProvider +import im.vector.matrix.android.internal.session.user.accountdata.AccountDataContent + +/** + * The account_data will have an encrypted property that is a map from key ID to an object. + * The algorithm from the m.secret_storage.key.[key ID] data for the given key defines how the other properties are interpreted, + * though it's expected that most encryption schemes would have ciphertext and mac properties, + * where the ciphertext property is the unpadded base64-encoded ciphertext, and the mac is used to ensure the integrity of the data. + */ +@JsonClass(generateAdapter = true) +data class EncryptedSecretContent( + /** unpadded base64-encoded ciphertext */ + @Json(name = "ciphertext") val ciphertext: String? = null, + @Json(name = "mac") val mac: String? = null, + @Json(name = "ephemeral") val ephemeral: String? = null, + @Json(name = "iv") val initializationVector: String? = null +) : AccountDataContent { + companion object { + /** + * Facility method to convert from object which must be comprised of maps, lists, + * strings, numbers, booleans and nulls. + */ + fun fromJson(obj: Any?): EncryptedSecretContent? { + return MoshiProvider.providesMoshi() + .adapter(EncryptedSecretContent::class.java) + .fromJsonValue(obj) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/IntegrityResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/IntegrityResult.kt new file mode 100644 index 0000000000..70efa56c48 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/IntegrityResult.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 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.securestorage + +sealed class IntegrityResult { + data class Success(val passphraseBased: Boolean) : IntegrityResult() + data class Error(val cause: SharedSecretStorageError) : IntegrityResult() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/KeyInfoResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/KeyInfoResult.kt new file mode 100644 index 0000000000..940f5298ef --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/KeyInfoResult.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 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.securestorage + +sealed class KeyInfoResult { + data class Success(val keyInfo: KeyInfo) : KeyInfoResult() + data class Error(val error: SharedSecretStorageError) : KeyInfoResult() + + fun isSuccess(): Boolean = this is Success +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/KeySigner.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/KeySigner.kt new file mode 100644 index 0000000000..2cd7a74f31 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/KeySigner.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 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.securestorage + +interface KeySigner { + fun sign(canonicalJson: String): Map>? +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SecretStorageKeyContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SecretStorageKeyContent.kt new file mode 100644 index 0000000000..129f4bab9d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SecretStorageKeyContent.kt @@ -0,0 +1,103 @@ +/* + * Copyright (c) 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.securestorage + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.di.MoshiProvider +import im.vector.matrix.android.internal.util.JsonCanonicalizer + +/** + * + * The contents of the account data for the key will include an algorithm property, which indicates the encryption algorithm used, as well as a name property, + * which is a human-readable name. + * The contents will be signed as signed JSON using the user's master cross-signing key. Other properties depend on the encryption algorithm. + * + * + * "content": { + * "algorithm": "m.secret_storage.v1.curve25519-aes-sha2", + * "passphrase": { + * "algorithm": "m.pbkdf2", + * "iterations": 500000, + * "salt": "IrswcMWnYieBALCAOMBw9k93xSzlc2su" + * }, + * "pubkey": "qql1q3IvBbwMU97zLnyh9HYW5x/zqTy5eoK1n+9fm1Y", + * "signatures": { + * "@valere35:matrix.org": { + * "ed25519:nOUQYiH9L8uKp5JajqiQyv+Loa3+lsdil7UBverz/Ko": "QtePmwfUL7+SHYRJT/HaTgF7gUFog1E/wtUCt0qc5aB8N+Sz5iCOvQ0KtaFHQ5SJzsBlYH8k7ejoBc0RcnU7BA" + * } + * } + * } + */ + +data class KeyInfo( + val id: String, + val content: SecretStorageKeyContent +) + +@JsonClass(generateAdapter = true) +data class SecretStorageKeyContent( + /** Currently support m.secret_storage.v1.curve25519-aes-sha2 */ + @Json(name = "algorithm") val algorithm: String? = null, + @Json(name = "name") val name: String? = null, + @Json(name = "passphrase") val passphrase: SsssPassphrase? = null, + @Json(name = "pubkey") val publicKey: String? = null, + @Json(name = "signatures") val signatures: Map>? = null +) { + + private fun signalableJSONDictionary(): Map { + return mutableMapOf().apply { + algorithm + ?.let { this["algorithm"] = it } + name + ?.let { this["name"] = it } + publicKey + ?.let { this["pubkey"] = it } + passphrase + ?.let { ssssPassphrase -> + this["passphrase"] = mapOf( + "algorithm" to ssssPassphrase.algorithm, + "iterations" to ssssPassphrase.iterations, + "salt" to ssssPassphrase.salt + ) + } + } + } + + fun canonicalSignable(): String { + return JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableJSONDictionary()) + } + + companion object { + /** + * Facility method to convert from object which must be comprised of maps, lists, + * strings, numbers, booleans and nulls. + */ + fun fromJson(obj: Any?): SecretStorageKeyContent? { + return MoshiProvider.providesMoshi() + .adapter(SecretStorageKeyContent::class.java) + .fromJsonValue(obj) + } + } +} + +@JsonClass(generateAdapter = true) +data class SsssPassphrase( + @Json(name = "algorithm") val algorithm: String?, + @Json(name = "iterations") val iterations: Int, + @Json(name = "salt") val salt: String? +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SharedSecretStorageError.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SharedSecretStorageError.kt new file mode 100644 index 0000000000..abd12789a5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SharedSecretStorageError.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 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.securestorage + +sealed class SharedSecretStorageError(message: String?) : Throwable(message) { + data class UnknownSecret(val secretName: String) : SharedSecretStorageError("Unknown Secret $secretName") + data class UnknownKey(val keyId: String) : SharedSecretStorageError("Unknown key $keyId") + data class UnknownAlgorithm(val keyId: String) : SharedSecretStorageError("Unknown algorithm $keyId") + data class UnsupportedAlgorithm(val algorithm: String) : SharedSecretStorageError("Unknown algorithm $algorithm") + data class SecretNotEncrypted(val secretName: String) : SharedSecretStorageError("Missing content for secret $secretName") + data class SecretNotEncryptedWithKey(val secretName: String, val keyId: String) + : SharedSecretStorageError("Missing content for secret $secretName with key $keyId") + + object BadKeyFormat : SharedSecretStorageError("Bad Key Format") + object ParsingError : SharedSecretStorageError("parsing Error") + object BadMac : SharedSecretStorageError("Bad mac") + data class OtherError(val reason: Throwable) : SharedSecretStorageError(reason.localizedMessage) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SharedSecretStorageService.kt new file mode 100644 index 0000000000..596d8d3e5d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SharedSecretStorageService.kt @@ -0,0 +1,118 @@ +/* + * 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.securestorage + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.listeners.ProgressListener + +/** + * Some features may require clients to store encrypted data on the server so that it can be shared securely between clients. + * Clients may also wish to securely send such data directly to each other. + * For example, key backups (MSC1219) can store the decryption key for the backups on the server, or cross-signing (MSC1756) can store the signing keys. + * + * https://github.com/matrix-org/matrix-doc/pull/1946 + * + */ + +interface SharedSecretStorageService { + + /** + * Generates a SSSS key for encrypting secrets. + * Use the SsssKeyCreationInfo object returned by the callback to get more information about the created key (recovery key ...) + * + * @param keyId the ID of the key + * @param keyName a human readable name + * @param keySigner Used to add a signature to the key (client should check key signature before storing secret) + * + * @param callback Get key creation info + */ + fun generateKey(keyId: String, + keyName: String, + keySigner: KeySigner?, + callback: MatrixCallback) + + /** + * Generates a SSSS key using the given passphrase. + * Use the SsssKeyCreationInfo object returned by the callback to get more information about the created key (recovery key, salt, iteration ...) + * + * @param keyId the ID of the key + * @param keyName human readable key name + * @param passphrase The passphrase used to generate the key + * @param keySigner Used to add a signature to the key (client should check key signature before retrieving secret) + * @param progressListener The derivation of the passphrase may take long depending on the device, use this to report progress + * + * @param callback Get key creation info + */ + fun generateKeyWithPassphrase(keyId: String, + keyName: String, + passphrase: String, + keySigner: KeySigner, + progressListener: ProgressListener?, + callback: MatrixCallback) + + fun getKey(keyId: String): KeyInfoResult + + /** + * A key can be marked as the "default" key by setting the user's account_data with event type m.secret_storage.default_key + * to an object that has the ID of the key as its key property. + * The default key will be used to encrypt all secrets that the user would expect to be available on all their clients. + * Unless the user specifies otherwise, clients will try to use the default key to decrypt secrets. + */ + fun getDefaultKey(): KeyInfoResult + + fun setDefaultKey(keyId: String, callback: MatrixCallback) + + /** + * Check whether we have a key with a given ID. + * + * @param keyId The ID of the key to check + * @return Whether we have the key. + */ + fun hasKey(keyId: String): Boolean + + /** + * Store an encrypted secret on the server + * Clients MUST ensure that the key is trusted before using it to encrypt secrets. + * + * @param name The name of the secret + * @param secret The secret contents. + * @param keys The list of (ID,privateKey) of the keys to use to encrypt the secret. + */ + fun storeSecret(name: String, secretBase64: String, keys: List, callback: MatrixCallback) + + /** + * Use this call to determine which SSSSKeySpec to use for requesting secret + */ + fun getAlgorithmsForSecret(name: String): List + + /** + * Get an encrypted secret from the shared storage + * + * @param name The name of the secret + * @param keyId The id of the key that should be used to decrypt (null for default key) + * @param secretKey the secret key to use (@see #RawBytesKeySpec) + * + */ + fun getSecret(name: String, keyId: String?, secretKey: SsssKeySpec, callback: MatrixCallback) + + fun checkShouldBeAbleToAccessSecrets(secretNames: List, keyId: String?) : IntegrityResult + + data class KeyRef( + val keyId: String?, + val keySpec: SsssKeySpec? + ) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SsssKeyCreationInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SsssKeyCreationInfo.kt new file mode 100644 index 0000000000..1d5522b8bf --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SsssKeyCreationInfo.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 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.securestorage + +data class SsssKeyCreationInfo( + val keyId: String = "", + var content: SecretStorageKeyContent?, + val recoveryKey: String = "" +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SsssKeySpec.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SsssKeySpec.kt new file mode 100644 index 0000000000..1fe8fbb90d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SsssKeySpec.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 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.securestorage + +import im.vector.matrix.android.api.listeners.ProgressListener +import im.vector.matrix.android.internal.crypto.keysbackup.deriveKey +import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey + +/** Tag class */ +interface SsssKeySpec + +data class RawBytesKeySpec( + val privateKey: ByteArray +) : SsssKeySpec { + + companion object { + + fun fromPassphrase(passphrase: String, salt: String, iterations: Int, progressListener: ProgressListener?): RawBytesKeySpec { + return RawBytesKeySpec( + privateKey = deriveKey( + passphrase, + salt, + iterations, + progressListener + ) + ) + } + + fun fromRecoveryKey(recoveryKey: String): RawBytesKeySpec? { + return extractCurveKeyFromRecoveryKey(recoveryKey)?.let { + RawBytesKeySpec( + privateKey = it + ) + } + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RawBytesKeySpec + + if (!privateKey.contentEquals(other.privateKey)) return false + + return true + } + + override fun hashCode(): Int { + return privateKey.contentHashCode() + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/model/User.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/model/User.kt index f569f5e47e..753c9b609c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/model/User.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/model/User.kt @@ -24,4 +24,9 @@ data class User( val userId: String, val displayName: String? = null, val avatarUrl: String? = null -) +) { + /** + * Return the display name or the user id + */ + fun getBestName() = displayName?.takeIf { it.isNotEmpty() } ?: userId +} 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 5b3ca234ac..d5aa897c7d 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 @@ -22,7 +22,7 @@ import im.vector.matrix.android.api.session.room.model.RoomMemberSummary 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 -import java.util.* +import java.util.Locale sealed class MatrixItem( open val id: String, @@ -143,8 +143,14 @@ sealed class MatrixItem( * ========================================================================================== */ fun User.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) + fun GroupSummary.toMatrixItem() = MatrixItem.GroupItem(groupId, displayName, avatarUrl) + 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) + +// If no name is available, use room alias as Riot-Web does +fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: getPrimaryAlias() ?: "", avatarUrl) + fun RoomMemberSummary.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt index 306a3846bc..2f03c99421 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt @@ -22,10 +22,19 @@ import im.vector.matrix.android.internal.auth.data.LoginFlowResponse import im.vector.matrix.android.internal.auth.data.PasswordLoginParams import im.vector.matrix.android.internal.auth.data.RiotConfig import im.vector.matrix.android.internal.auth.login.ResetPasswordMailConfirmed -import im.vector.matrix.android.internal.auth.registration.* +import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationParams +import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationResponse +import im.vector.matrix.android.internal.auth.registration.RegistrationParams +import im.vector.matrix.android.internal.auth.registration.SuccessResult +import im.vector.matrix.android.internal.auth.registration.ValidationCodeBody import im.vector.matrix.android.internal.network.NetworkConstants import retrofit2.Call -import retrofit2.http.* +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Headers +import retrofit2.http.POST +import retrofit2.http.Path +import retrofit2.http.Url /** * The login REST API. 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 d5dd7e2959..85c2cdbf3d 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 @@ -20,7 +20,13 @@ import android.net.Uri import dagger.Lazy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.AuthenticationService -import im.vector.matrix.android.api.auth.data.* +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.LoginFlowResult +import im.vector.matrix.android.api.auth.data.SessionParams +import im.vector.matrix.android.api.auth.data.Versions +import im.vector.matrix.android.api.auth.data.isLoginAndRegistrationSupportedBySdk +import im.vector.matrix.android.api.auth.data.isSupportedBySdk import im.vector.matrix.android.api.auth.login.LoginWizard import im.vector.matrix.android.api.auth.registration.RegistrationWizard import im.vector.matrix.android.api.failure.Failure diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/PendingSessionData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/PendingSessionData.kt index 0314491d3b..3241ea5917 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/PendingSessionData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/PendingSessionData.kt @@ -19,7 +19,7 @@ package im.vector.matrix.android.internal.auth.db import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.internal.auth.login.ResetPasswordData import im.vector.matrix.android.internal.auth.registration.ThreePidData -import java.util.* +import java.util.UUID /** * This class holds all pending data when creating a session, either by login or by register diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt index 2d3d25e538..7512454052 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt @@ -32,20 +32,20 @@ data class RegistrationFlowResponse( * The list of flows. */ @Json(name = "flows") - var flows: List? = null, + val flows: List? = null, /** * The list of stages the client has completed successfully. */ @Json(name = "completed") - var completedStages: List? = null, + val completedStages: List? = null, /** * The session identifier that the client must pass back to the home server, if one is provided, * in subsequent attempts to authenticate in the same API call. */ @Json(name = "session") - var session: String? = null, + val session: String? = null, /** * The information that the client will need to know in order to use a given type of authentication. @@ -53,7 +53,7 @@ data class RegistrationFlowResponse( * For example, the public key of reCAPTCHA stage could be given here. */ @Json(name = "params") - var params: JsonDict? = null + val params: JsonDict? = null /** * WARNING, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoConstants.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoConstants.kt index a3b0a567fe..f82300548a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoConstants.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoConstants.kt @@ -31,6 +31,13 @@ const val MXCRYPTO_ALGORITHM_MEGOLM = "m.megolm.v1.aes-sha2" */ const val MXCRYPTO_ALGORITHM_MEGOLM_BACKUP = "m.megolm_backup.v1.curve25519-aes-sha2" +/** + * Secured Shared Storage algorithm constant + */ +const val SSSS_ALGORITHM_CURVE25519_AES_SHA2 = "m.secret_storage.v1.curve25519-aes-sha2" +/* Secrets are encrypted using AES-CTR-256 and MACed using HMAC-SHA-256. **/ +const val SSSS_ALGORITHM_AES_HMAC_SHA2 = "m.secret_storage.v1.aes-hmac-sha2" + // TODO Refacto: use this constants everywhere const val ed25519 = "ed25519" const val curve25519 = "curve25519" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt index 26a35bd919..1db774fd2d 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt @@ -49,7 +49,7 @@ import im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmEncryp import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmEncryptionFactory import im.vector.matrix.android.internal.crypto.crosssigning.DefaultCrossSigningService import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel -import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup +import im.vector.matrix.android.internal.crypto.keysbackup.DefaultKeysBackupService import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo @@ -122,7 +122,7 @@ internal class DefaultCryptoService @Inject constructor( // Device list manager private val deviceListManager: DeviceListManager, // The key backup service. - private val keysBackup: KeysBackup, + private val keysBackupService: DefaultKeysBackupService, // private val objectSigner: ObjectSigner, // @@ -301,7 +301,7 @@ internal class DefaultCryptoService @Inject constructor( uploadDeviceKeys() oneTimeKeysUploader.maybeUploadOneTimeKeys() outgoingRoomKeyRequestManager.start() - keysBackup.checkAndStartKeysBackup() + keysBackupService.checkAndStartKeysBackup() if (isInitialSync) { // refresh the devices list for each known room members deviceListManager.invalidateAllDeviceLists() @@ -340,14 +340,14 @@ internal class DefaultCryptoService @Inject constructor( /** * @return the Keys backup Service */ - override fun getKeysBackupService() = keysBackup + override fun keysBackupService() = keysBackupService /** * @return the VerificationService */ - override fun getVerificationService() = verificationService + override fun verificationService() = verificationService - override fun getCrossSigningService() = crossSigningService + override fun crossSigningService() = crossSigningService /** * A sync response has been received @@ -721,7 +721,7 @@ internal class DefaultCryptoService @Inject constructor( Timber.e("## onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}") return } - alg.onRoomKeyEvent(event, keysBackup) + alg.onRoomKeyEvent(event, keysBackupService) } /** @@ -887,7 +887,7 @@ internal class DefaultCryptoService @Inject constructor( throw Exception("Error") } - megolmSessionDataImporter.handle(importedSessions, true, uiHandler, progressListener) + megolmSessionDataImporter.handle(importedSessions, true, progressListener) } }.foldToCallback(callback) } @@ -1021,12 +1021,12 @@ internal class DefaultCryptoService @Inject constructor( return } - val requestBody = RoomKeyRequestBody() - - requestBody.roomId = event.roomId - requestBody.algorithm = wireContent["algorithm"]?.toString() - requestBody.senderKey = wireContent["sender_key"]?.toString() - requestBody.sessionId = wireContent["session_id"]?.toString() + val requestBody = RoomKeyRequestBody( + algorithm = wireContent["algorithm"]?.toString(), + roomId = event.roomId, + senderKey = wireContent["sender_key"]?.toString(), + sessionId = wireContent["session_id"]?.toString() + ) outgoingRoomKeyRequestManager.resendRoomKeyRequest(requestBody) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequest.kt index fe1f69f904..39b4678a27 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequest.kt @@ -25,54 +25,56 @@ import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyShareRequest /** * IncomingRoomKeyRequest class defines the incoming room keys request. */ -open class IncomingRoomKeyRequest { - /** - * The user id - */ - var userId: String? = null +data class IncomingRoomKeyRequest( + /** + * The user id + */ + override val userId: String? = null, - /** - * The device id - */ - var deviceId: String? = null + /** + * The device id + */ + override val deviceId: String? = null, - /** - * The request id - */ - var requestId: String? = null + /** + * The request id + */ + override val requestId: String? = null, - /** - * The request body - */ - var requestBody: RoomKeyRequestBody? = null + /** + * The request body + */ + val requestBody: RoomKeyRequestBody? = null, - /** - * The runnable to call to accept to share the keys - */ - @Transient - var share: Runnable? = null + /** + * The runnable to call to accept to share the keys + */ + @Transient + var share: Runnable? = null, - /** - * The runnable to call to ignore the key share request. - */ - @Transient - var ignore: Runnable? = null - - /** - * Constructor - * - * @param event the event - */ - constructor(event: Event) { - userId = event.senderId - val roomKeyShareRequest = event.getClearContent().toModel()!! - deviceId = roomKeyShareRequest.requestingDeviceId - requestId = roomKeyShareRequest.requestId - requestBody = if (null != roomKeyShareRequest.body) roomKeyShareRequest.body else RoomKeyRequestBody() + /** + * The runnable to call to ignore the key share request. + */ + @Transient + var ignore: Runnable? = null +) : IncomingRoomKeyRequestCommon { + companion object { + /** + * Factory + * + * @param event the event + */ + fun fromEvent(event: Event): IncomingRoomKeyRequest? { + return event.getClearContent() + .toModel() + ?.let { + IncomingRoomKeyRequest( + userId = event.senderId, + deviceId = it.requestingDeviceId, + requestId = it.requestId, + requestBody = it.body ?: RoomKeyRequestBody() + ) + } + } } - - /** - * Constructor for object creation from crypto store - */ - constructor() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestCancellation.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestCancellation.kt index 0b22a0e28b..6779936f3a 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestCancellation.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestCancellation.kt @@ -17,13 +17,44 @@ package im.vector.matrix.android.internal.crypto 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.internal.crypto.model.rest.RoomKeyShareCancellation /** * IncomingRoomKeyRequestCancellation describes the incoming room key cancellation. */ -class IncomingRoomKeyRequestCancellation(event: Event) : IncomingRoomKeyRequest(event) { +data class IncomingRoomKeyRequestCancellation( + /** + * The user id + */ + override val userId: String? = null, - init { - requestBody = null + /** + * The device id + */ + override val deviceId: String? = null, + + /** + * The request id + */ + override val requestId: String? = null +) : IncomingRoomKeyRequestCommon { + companion object { + /** + * Factory + * + * @param event the event + */ + fun fromEvent(event: Event): IncomingRoomKeyRequestCancellation? { + return event.getClearContent() + .toModel() + ?.let { + IncomingRoomKeyRequestCancellation( + userId = event.senderId, + deviceId = it.requestingDeviceId, + requestId = it.requestId + ) + } + } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestCommon.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestCommon.kt new file mode 100644 index 0000000000..a7b1c6b117 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestCommon.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 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.crypto + +interface IncomingRoomKeyRequestCommon { + /** + * The user id + */ + val userId: String? + + /** + * The device id + */ + val deviceId: String? + + /** + * The request id + */ + val requestId: String? +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt index 814d9d5a7c..92a117d64b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt @@ -19,7 +19,6 @@ package im.vector.matrix.android.internal.crypto import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener 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.internal.crypto.model.rest.RoomKeyShare import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.session.SessionScope @@ -51,11 +50,10 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( * @param event the announcement event. */ fun onRoomKeyRequestEvent(event: Event) { - val roomKeyShare = event.getClearContent().toModel() - when (roomKeyShare?.action) { - RoomKeyShare.ACTION_SHARE_REQUEST -> receivedRoomKeyRequests.add(IncomingRoomKeyRequest(event)) - RoomKeyShare.ACTION_SHARE_CANCELLATION -> receivedRoomKeyRequestCancellations.add(IncomingRoomKeyRequestCancellation(event)) - else -> Timber.e("## onRoomKeyRequestEvent() : unsupported action ${roomKeyShare?.action}") + when (val roomKeyShareAction = event.getClearContent()?.get("action") as? String) { + RoomKeyShare.ACTION_SHARE_REQUEST -> IncomingRoomKeyRequest.fromEvent(event)?.let { receivedRoomKeyRequests.add(it) } + RoomKeyShare.ACTION_SHARE_CANCELLATION -> IncomingRoomKeyRequestCancellation.fromEvent(event)?.let { receivedRoomKeyRequestCancellations.add(it) } + else -> Timber.e("## onRoomKeyRequestEvent() : unsupported action $roomKeyShareAction") } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt index 75a4ba5ed7..47ec85ec8c 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt @@ -29,7 +29,12 @@ import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.util.JsonCanonicalizer import im.vector.matrix.android.internal.util.convertFromUTF8 import im.vector.matrix.android.internal.util.convertToUTF8 -import org.matrix.olm.* +import org.matrix.olm.OlmAccount +import org.matrix.olm.OlmException +import org.matrix.olm.OlmMessage +import org.matrix.olm.OlmOutboundGroupSession +import org.matrix.olm.OlmSession +import org.matrix.olm.OlmUtility import timber.log.Timber import java.net.URLEncoder import javax.inject.Inject diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MegolmSessionData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MegolmSessionData.kt index 821ed0a553..d6dae1a865 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MegolmSessionData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MegolmSessionData.kt @@ -28,46 +28,46 @@ data class MegolmSessionData( * The algorithm used. */ @Json(name = "algorithm") - var algorithm: String? = null, + val algorithm: String? = null, /** * Unique id for the session. */ @Json(name = "session_id") - var sessionId: String? = null, + val sessionId: String? = null, /** * Sender's Curve25519 device key. */ @Json(name = "sender_key") - var senderKey: String? = null, + val senderKey: String? = null, /** * Room this session is used in. */ @Json(name = "room_id") - var roomId: String? = null, + val roomId: String? = null, /** * Base64'ed key data. */ @Json(name = "session_key") - var sessionKey: String? = null, + val sessionKey: String? = null, /** * Other keys the sender claims. */ @Json(name = "sender_claimed_keys") - var senderClaimedKeys: Map? = null, + val senderClaimedKeys: Map? = null, // This is a shortcut for sender_claimed_keys.get("ed25519") // Keep it for compatibility reason. @Json(name = "sender_claimed_ed25519_key") - var senderClaimedEd25519Key: String? = null, + val senderClaimedEd25519Key: String? = null, /** * Devices which forwarded this session to us (normally empty). */ @Json(name = "forwarding_curve25519_key_chain") - var forwardingCurve25519KeyChain: List? = null + val forwardingCurve25519KeyChain: List? = null ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt index 5320b84b0e..b59c93ba83 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt @@ -213,10 +213,11 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor( Timber.v("## sendOutgoingRoomKeyRequest() : Requesting keys " + request.requestBody + " from " + request.recipients + " id " + request.requestId) - val requestMessage = RoomKeyShareRequest() - requestMessage.requestingDeviceId = cryptoStore.getDeviceId() - requestMessage.requestId = request.requestId - requestMessage.body = request.requestBody + val requestMessage = RoomKeyShareRequest( + requestingDeviceId = cryptoStore.getDeviceId(), + requestId = request.requestId, + body = request.requestBody + ) sendMessageToDevices(requestMessage, request.recipients, request.requestId, object : MatrixCallback { private fun onDone(state: OutgoingRoomKeyRequest.RequestState) { @@ -253,9 +254,10 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor( + " to " + request.recipients + " cancellation id " + request.cancellationTxnId) - val roomKeyShareCancellation = RoomKeyShareCancellation() - roomKeyShareCancellation.requestingDeviceId = cryptoStore.getDeviceId() - roomKeyShareCancellation.requestId = request.cancellationTxnId + val roomKeyShareCancellation = RoomKeyShareCancellation( + requestingDeviceId = cryptoStore.getDeviceId(), + requestId = request.cancellationTxnId + ) sendMessageToDevices(roomKeyShareCancellation, request.recipients, request.cancellationTxnId, object : MatrixCallback { private fun onDone() { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/MegolmSessionDataImporter.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/MegolmSessionDataImporter.kt index 62b4f1b851..6f41116b90 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/MegolmSessionDataImporter.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/MegolmSessionDataImporter.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.crypto.actions -import android.os.Handler import androidx.annotation.WorkerThread import im.vector.matrix.android.api.listeners.ProgressListener import im.vector.matrix.android.internal.crypto.MXOlmDevice @@ -46,7 +45,6 @@ internal class MegolmSessionDataImporter @Inject constructor(private val olmDevi @WorkerThread fun handle(megolmSessionsData: List, fromBackup: Boolean, - uiHandler: Handler, progressListener: ProgressListener?): ImportRoomKeysResult { val t0 = System.currentTimeMillis() @@ -54,11 +52,7 @@ internal class MegolmSessionDataImporter @Inject constructor(private val olmDevi var lastProgress = 0 var totalNumbersOfImportedKeys = 0 - if (progressListener != null) { - uiHandler.post { - progressListener.onProgress(0, 100) - } - } + progressListener?.onProgress(0, 100) val olmInboundGroupSessionWrappers = olmDevice.importInboundGroupSessions(megolmSessionsData) megolmSessionsData.forEachIndexed { cpt, megolmSessionData -> @@ -72,12 +66,12 @@ internal class MegolmSessionDataImporter @Inject constructor(private val olmDevi totalNumbersOfImportedKeys++ // cancel any outstanding room key requests for this session - val roomKeyRequestBody = RoomKeyRequestBody() - - roomKeyRequestBody.algorithm = megolmSessionData.algorithm - roomKeyRequestBody.roomId = megolmSessionData.roomId - roomKeyRequestBody.senderKey = megolmSessionData.senderKey - roomKeyRequestBody.sessionId = megolmSessionData.sessionId + val roomKeyRequestBody = RoomKeyRequestBody( + algorithm = megolmSessionData.algorithm, + roomId = megolmSessionData.roomId, + senderKey = megolmSessionData.senderKey, + sessionId = megolmSessionData.sessionId + ) outgoingRoomKeyRequestManager.cancelRoomKeyRequest(roomKeyRequestBody) @@ -89,14 +83,12 @@ internal class MegolmSessionDataImporter @Inject constructor(private val olmDevi } if (progressListener != null) { - uiHandler.post { - val progress = 100 * cpt / totalNumbersOfKeys + val progress = 100 * (cpt + 1) / totalNumbersOfKeys - if (lastProgress != progress) { - lastProgress = progress + if (lastProgress != progress) { + lastProgress = progress - progressListener.onProgress(progress, 100) - } + progressListener.onProgress(progress, 100) } } } 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 8dad832617..d6538f041d 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 @@ -17,7 +17,7 @@ package im.vector.matrix.android.internal.crypto.actions import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel -import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup +import im.vector.matrix.android.internal.crypto.keysbackup.DefaultKeysBackupService import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.di.UserId import timber.log.Timber @@ -26,7 +26,7 @@ import javax.inject.Inject internal class SetDeviceVerificationAction @Inject constructor( private val cryptoStore: IMXCryptoStore, @UserId private val userId: String, - private val keysBackup: KeysBackup) { + private val defaultKeysBackupService: DefaultKeysBackupService) { fun handle(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) { val device = cryptoStore.getUserDevice(userId, deviceId) @@ -42,7 +42,7 @@ internal class SetDeviceVerificationAction @Inject constructor( // If one of the user's own devices is being marked as verified / unverified, // check the key backup status, since whether or not we use this depends on // whether it has a signature from a verified device - keysBackup.checkAndStartKeysBackup() + defaultKeysBackupService.checkAndStartKeysBackup() } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXDecrypting.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXDecrypting.kt index f63eaa93b3..e8fb6be6ff 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXDecrypting.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXDecrypting.kt @@ -20,7 +20,7 @@ package im.vector.matrix.android.internal.crypto.algorithms import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult -import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup +import im.vector.matrix.android.internal.crypto.keysbackup.DefaultKeysBackupService /** * An interface for decrypting data @@ -41,7 +41,7 @@ internal interface IMXDecrypting { * * @param event the key event. */ - fun onRoomKeyEvent(event: Event, keysBackup: KeysBackup) {} + fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {} /** * Check if the some messages can be decrypted with a new session diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt index 9121ce3fcb..90100fcc48 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt @@ -30,7 +30,7 @@ import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequestManager import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevicesAction import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter import im.vector.matrix.android.internal.crypto.algorithms.IMXDecrypting -import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup +import im.vector.matrix.android.internal.crypto.keysbackup.DefaultKeysBackupService import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent @@ -163,12 +163,12 @@ internal class MXMegolmDecryption(private val userId: String, recipients.add(senderMap) } - val requestBody = RoomKeyRequestBody() - - requestBody.roomId = event.roomId - requestBody.algorithm = encryptedEventContent.algorithm - requestBody.senderKey = encryptedEventContent.senderKey - requestBody.sessionId = encryptedEventContent.sessionId + val requestBody = RoomKeyRequestBody( + roomId = event.roomId, + algorithm = encryptedEventContent.algorithm, + senderKey = encryptedEventContent.senderKey, + sessionId = encryptedEventContent.sessionId + ) outgoingRoomKeyRequestManager.sendRoomKeyRequest(requestBody, recipients) } @@ -198,7 +198,7 @@ internal class MXMegolmDecryption(private val userId: String, * * @param event the key event. */ - override fun onRoomKeyEvent(event: Event, keysBackup: KeysBackup) { + override fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) { var exportFormat = false val roomKeyContent = event.getClearContent().toModel() ?: return @@ -262,14 +262,14 @@ internal class MXMegolmDecryption(private val userId: String, exportFormat) if (added) { - keysBackup.maybeBackupKeys() + defaultKeysBackupService.maybeBackupKeys() - val content = RoomKeyRequestBody() - - content.algorithm = roomKeyContent.algorithm - content.roomId = roomKeyContent.roomId - content.sessionId = roomKeyContent.sessionId - content.senderKey = senderKey + val content = RoomKeyRequestBody( + algorithm = roomKeyContent.algorithm, + roomId = roomKeyContent.roomId, + sessionId = roomKeyContent.sessionId, + senderKey = senderKey + ) outgoingRoomKeyRequestManager.cancelRoomKeyRequest(content) @@ -290,8 +290,8 @@ internal class MXMegolmDecryption(private val userId: String, override fun hasKeysForKeyRequest(request: IncomingRoomKeyRequest): Boolean { val roomId = request.requestBody?.roomId ?: return false - val senderKey = request.requestBody?.senderKey ?: return false - val sessionId = request.requestBody?.sessionId ?: return false + val senderKey = request.requestBody.senderKey ?: return false + val sessionId = request.requestBody.sessionId ?: return false return olmDevice.hasInboundSessionKeys(roomId, senderKey, sessionId) } @@ -319,15 +319,14 @@ internal class MXMegolmDecryption(private val userId: String, return@mapCatching } Timber.v("## shareKeysWithDevice() : sharing keys for session" + - " ${body?.senderKey}|${body?.sessionId} with device $userId:$deviceId") + " ${body.senderKey}|${body.sessionId} with device $userId:$deviceId") val payloadJson = mutableMapOf("type" to EventType.FORWARDED_ROOM_KEY) - runCatching { olmDevice.getInboundGroupSession(body?.sessionId, body?.senderKey, body?.roomId) } + runCatching { olmDevice.getInboundGroupSession(body.sessionId, body.senderKey, body.roomId) } .fold( { // TODO - payloadJson["content"] = it.exportKeys() - ?: "" + payloadJson["content"] = it.exportKeys() ?: "" }, { // TODO diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt index ee35810763..a2d21c4f89 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -28,7 +28,7 @@ import im.vector.matrix.android.internal.crypto.MXOlmDevice import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevicesAction import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting -import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup +import im.vector.matrix.android.internal.crypto.keysbackup.DefaultKeysBackupService import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.repository.WarnOnUnknownDeviceRepository @@ -42,7 +42,7 @@ internal class MXMegolmEncryption( // The id of the room we will be sending to. private var roomId: String, private val olmDevice: MXOlmDevice, - private val keysBackup: KeysBackup, + private val defaultKeysBackupService: DefaultKeysBackupService, private val cryptoStore: IMXCryptoStore, private val deviceListManager: DeviceListManager, private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, @@ -85,7 +85,7 @@ internal class MXMegolmEncryption( olmDevice.addInboundGroupSession(sessionId!!, olmDevice.getSessionKey(sessionId)!!, roomId, olmDevice.deviceCurve25519Key!!, emptyList(), keysClaimedMap, false) - keysBackup.maybeBackupKeys() + defaultKeysBackupService.maybeBackupKeys() return MXOutboundSessionInfo(sessionId) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt index dadd810a4b..e9fe902f1f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt @@ -21,7 +21,7 @@ import im.vector.matrix.android.internal.crypto.DeviceListManager import im.vector.matrix.android.internal.crypto.MXOlmDevice import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevicesAction import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter -import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup +import im.vector.matrix.android.internal.crypto.keysbackup.DefaultKeysBackupService import im.vector.matrix.android.internal.crypto.repository.WarnOnUnknownDeviceRepository import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask @@ -29,7 +29,7 @@ import javax.inject.Inject internal class MXMegolmEncryptionFactory @Inject constructor( private val olmDevice: MXOlmDevice, - private val keysBackup: KeysBackup, + private val defaultKeysBackupService: DefaultKeysBackupService, private val cryptoStore: IMXCryptoStore, private val deviceListManager: DeviceListManager, private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, @@ -42,7 +42,7 @@ internal class MXMegolmEncryptionFactory @Inject constructor( return MXMegolmEncryption( roomId, olmDevice, - keysBackup, + defaultKeysBackupService, cryptoStore, deviceListManager, ensureOlmSessionsForDevicesAction, 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 4953d53ae0..93dae588aa 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 @@ -16,10 +16,29 @@ */ package im.vector.matrix.android.internal.crypto.api -import im.vector.matrix.android.internal.crypto.model.rest.* +import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceParams +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.KeyChangesResponse +import im.vector.matrix.android.internal.crypto.model.rest.KeysClaimBody +import im.vector.matrix.android.internal.crypto.model.rest.KeysClaimResponse +import im.vector.matrix.android.internal.crypto.model.rest.KeysQueryBody +import im.vector.matrix.android.internal.crypto.model.rest.KeysQueryResponse +import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadBody +import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse +import im.vector.matrix.android.internal.crypto.model.rest.SendToDeviceBody +import im.vector.matrix.android.internal.crypto.model.rest.SignatureUploadResponse +import im.vector.matrix.android.internal.crypto.model.rest.UpdateDeviceInfoBody +import im.vector.matrix.android.internal.crypto.model.rest.UploadSigningKeysBody import im.vector.matrix.android.internal.network.NetworkConstants import retrofit2.Call -import retrofit2.http.* +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.HTTP +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Path +import retrofit2.http.Query internal interface CryptoApi { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/ComputeTrustTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/ComputeTrustTask.kt index 207dc0b928..841de92130 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/ComputeTrustTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/ComputeTrustTask.kt @@ -20,6 +20,8 @@ import im.vector.matrix.android.api.extensions.orFalse import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.task.Task +import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers +import kotlinx.coroutines.withContext import javax.inject.Inject internal interface ComputeTrustTask : Task { @@ -29,14 +31,15 @@ internal interface ComputeTrustTask : Task getUserCrossSigningKeys(userId)?.isTrusted() == true } - return if (allTrustedUserIds.isEmpty()) { + if (allTrustedUserIds.isEmpty()) { RoomEncryptionTrustLevel.Default } else { // If one of the verified user as an untrusted device -> warning diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt index 7fc3c0a549..a29f27ddd6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt @@ -88,7 +88,8 @@ internal class DefaultCrossSigningService @Inject constructor( Timber.i("## CrossSigning - Loading master key success") } else { Timber.w("## CrossSigning - Public master key does not match the private key") - // TODO untrust + pkSigning.releaseSigning() + // TODO untrust? } } privateKeysInfo.user @@ -100,7 +101,8 @@ internal class DefaultCrossSigningService @Inject constructor( Timber.i("## CrossSigning - Loading User Signing key success") } else { Timber.w("## CrossSigning - Public User key does not match the private key") - // TODO untrust + pkSigning.releaseSigning() + // TODO untrust? } } privateKeysInfo.selfSigned @@ -112,7 +114,8 @@ internal class DefaultCrossSigningService @Inject constructor( Timber.i("## CrossSigning - Loading Self Signing key success") } else { Timber.w("## CrossSigning - Public Self Signing key does not match the private key") - // TODO untrust + pkSigning.releaseSigning() + // TODO untrust? } } } @@ -224,16 +227,18 @@ internal class DefaultCrossSigningService @Inject constructor( val myDevice = myDeviceInfoHolder.get().myDevice val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, myDevice.signalableJSONDictionary()) val signedDevice = selfSigningPkOlm.sign(canonicalJson) - val updateSignatures = (myDevice.signatures?.toMutableMap() ?: HashMap()).also { - it[userId] = (it[userId] - ?: HashMap()) + mapOf("ed25519:$sskPublicKey" to signedDevice) - } + val updateSignatures = (myDevice.signatures?.toMutableMap() ?: HashMap()) + .also { + it[userId] = (it[userId] + ?: HashMap()) + mapOf("ed25519:$sskPublicKey" to signedDevice) + } myDevice.copy(signatures = updateSignatures).let { uploadSignatureQueryBuilder.withDeviceInfo(it) } // sign MSK with device key (migration) and upload signatures - olmDevice.signMessage(JsonCanonicalizer.getCanonicalJson(Map::class.java, mskCrossSigningKeyInfo.signalableJSONDictionary()))?.let { sign -> + val message = JsonCanonicalizer.getCanonicalJson(Map::class.java, mskCrossSigningKeyInfo.signalableJSONDictionary()) + olmDevice.signMessage(message)?.let { sign -> val mskUpdatedSignatures = (mskCrossSigningKeyInfo.signatures?.toMutableMap() ?: HashMap()).also { it[userId] = (it[userId] @@ -292,6 +297,80 @@ internal class DefaultCrossSigningService @Inject constructor( cryptoStore.clearOtherUserTrust() } + override fun checkTrustFromPrivateKeys(masterKeyPrivateKey: String?, + uskKeyPrivateKey: String?, + sskPrivateKey: String? + ): UserTrustResult { + val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return UserTrustResult.CrossSigningNotConfigured(userId) + + var masterKeyIsTrusted = false + var userKeyIsTrusted = false + var selfSignedKeyIsTrusted = false + + masterKeyPrivateKey?.fromBase64NoPadding() + ?.let { privateKeySeed -> + val pkSigning = OlmPkSigning() + try { + if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) { + masterPkSigning?.releaseSigning() + masterPkSigning = pkSigning + masterKeyIsTrusted = true + Timber.i("## CrossSigning - Loading master key success") + } else { + pkSigning.releaseSigning() + } + } catch (failure: Throwable) { + pkSigning.releaseSigning() + } + } + + uskKeyPrivateKey?.fromBase64NoPadding() + ?.let { privateKeySeed -> + val pkSigning = OlmPkSigning() + try { + if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) { + userPkSigning?.releaseSigning() + userPkSigning = pkSigning + userKeyIsTrusted = true + Timber.i("## CrossSigning - Loading master key success") + } else { + pkSigning.releaseSigning() + } + } catch (failure: Throwable) { + pkSigning.releaseSigning() + } + } + + sskPrivateKey?.fromBase64NoPadding() + ?.let { privateKeySeed -> + val pkSigning = OlmPkSigning() + try { + if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) { + selfSigningPkSigning?.releaseSigning() + selfSigningPkSigning = pkSigning + selfSignedKeyIsTrusted = true + Timber.i("## CrossSigning - Loading master key success") + } else { + pkSigning.releaseSigning() + } + } catch (failure: Throwable) { + pkSigning.releaseSigning() + } + } + + if (!masterKeyIsTrusted || !userKeyIsTrusted || !selfSignedKeyIsTrusted) { + return UserTrustResult.KeysNotTrusted(mxCrossSigningInfo) + } else { + cryptoStore.markMyMasterKeyAsLocallyTrusted(true) + val checkSelfTrust = checkSelfTrust() + if (checkSelfTrust.isVerified()) { + cryptoStore.storePrivateKeysInfo(masterKeyPrivateKey, uskKeyPrivateKey, sskPrivateKey) + setUserKeysAsTrusted(userId, true) + } + return checkSelfTrust + } + } + /** * * ┏━━━━━━━━┓ ┏━━━━━━━━┓ @@ -374,7 +453,9 @@ internal class DefaultCrossSigningService @Inject constructor( ?.fromBase64NoPadding() var isMaterKeyTrusted = false - if (masterPrivateKey != null) { + if (myMasterKey.trustLevel?.locallyVerified == true) { + isMaterKeyTrusted = true + } else if (masterPrivateKey != null) { // Check if private match public var olmPkSigning: OlmPkSigning? = null try { @@ -507,7 +588,12 @@ internal class DefaultCrossSigningService @Inject constructor( }.executeBy(taskExecutor) } - override fun signDevice(deviceId: String, callback: MatrixCallback) { + override fun markMyMasterKeyAsTrusted() { + cryptoStore.markMyMasterKeyAsLocallyTrusted(true) + checkSelfTrust() + } + + override fun trustDevice(deviceId: String, callback: MatrixCallback) { // This device should be yours val device = cryptoStore.getUserDevice(userId, deviceId) if (device == null) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/ShieldTrustUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/ShieldTrustUpdater.kt index 5bc6e2df0f..c4c49a5940 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/ShieldTrustUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/ShieldTrustUpdater.kt @@ -18,16 +18,19 @@ package im.vector.matrix.android.internal.crypto.crosssigning import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntity import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.di.CryptoDatabase import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.createBackgroundHandler import io.realm.Realm import io.realm.RealmConfiguration import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe +import timber.log.Timber +import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference import javax.inject.Inject @@ -35,7 +38,7 @@ internal class ShieldTrustUpdater @Inject constructor( private val eventBus: EventBus, private val computeTrustTask: ComputeTrustTask, private val taskExecutor: TaskExecutor, - @CryptoDatabase private val cryptoRealmConfiguration: RealmConfiguration, + private val coroutineDispatchers: MatrixCoroutineDispatchers, @SessionDatabase private val sessionRealmConfiguration: RealmConfiguration, private val roomSummaryUpdater: RoomSummaryUpdater ) { @@ -44,51 +47,41 @@ internal class ShieldTrustUpdater @Inject constructor( private val BACKGROUND_HANDLER = createBackgroundHandler("SHIELD_CRYPTO_DB_THREAD") } - private val backgroundCryptoRealm = AtomicReference() private val backgroundSessionRealm = AtomicReference() -// private var cryptoDevicesResult: RealmResults? = null - -// private val cryptoDeviceChangeListener = object : OrderedRealmCollectionChangeListener> { -// override fun onChange(t: RealmResults, changeSet: OrderedCollectionChangeSet) { -// val grouped = t.groupBy { it.userId } -// onCryptoDevicesChange(grouped.keys.mapNotNull { it }) -// } -// } + private val isStarted = AtomicBoolean() fun start() { - eventBus.register(this) - BACKGROUND_HANDLER.post { - val cryptoRealm = Realm.getInstance(cryptoRealmConfiguration) - backgroundCryptoRealm.set(cryptoRealm) -// cryptoDevicesResult = cryptoRealm.where().findAll() -// cryptoDevicesResult?.addChangeListener(cryptoDeviceChangeListener) - - backgroundSessionRealm.set(Realm.getInstance(sessionRealmConfiguration)) + if (isStarted.compareAndSet(false, true)) { + eventBus.register(this) + BACKGROUND_HANDLER.post { + backgroundSessionRealm.set(Realm.getInstance(sessionRealmConfiguration)) + } } } fun stop() { - eventBus.unregister(this) - BACKGROUND_HANDLER.post { - // cryptoDevicesResult?.removeAllChangeListeners() - backgroundCryptoRealm.getAndSet(null).also { - it?.close() - } - backgroundSessionRealm.getAndSet(null).also { - it?.close() + if (isStarted.compareAndSet(true, false)) { + eventBus.unregister(this) + BACKGROUND_HANDLER.post { + backgroundSessionRealm.getAndSet(null).also { + it?.close() + } } } } @Subscribe fun onRoomMemberChange(update: SessionToCryptoRoomMembersUpdate) { - taskExecutor.executorScope.launch { + if (!isStarted.get()) { + return + } + taskExecutor.executorScope.launch(coroutineDispatchers.crypto) { val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(update.userIds)) // We need to send that back to session base BACKGROUND_HANDLER.post { - backgroundSessionRealm.get().executeTransaction { realm -> + backgroundSessionRealm.get()?.executeTransaction { realm -> roomSummaryUpdater.updateShieldTrust(realm, update.roomId, updatedTrust) } } @@ -97,34 +90,47 @@ internal class ShieldTrustUpdater @Inject constructor( @Subscribe fun onTrustUpdate(update: CryptoToSessionUserTrustChange) { + if (!isStarted.get()) { + return + } + onCryptoDevicesChange(update.userIds) } private fun onCryptoDevicesChange(users: List) { BACKGROUND_HANDLER.post { - val impactedRoomsId = backgroundSessionRealm.get().where(RoomMemberSummaryEntity::class.java) - .`in`(RoomMemberSummaryEntityFields.USER_ID, users.toTypedArray()) - .findAll() - .map { it.roomId } - .distinct() + val impactedRoomsId = backgroundSessionRealm.get()?.where(RoomMemberSummaryEntity::class.java) + ?.`in`(RoomMemberSummaryEntityFields.USER_ID, users.toTypedArray()) + ?.findAll() + ?.map { it.roomId } + ?.distinct() val map = HashMap>() - impactedRoomsId.forEach { roomId -> - RoomMemberSummaryEntity.where(backgroundSessionRealm.get(), roomId) - .findAll() - .let { results -> - map[roomId] = results.map { it.userId } - } + impactedRoomsId?.forEach { roomId -> + backgroundSessionRealm.get()?.let { realm -> + RoomMemberSummaryEntity.where(realm, roomId) + .findAll() + .let { results -> + map[roomId] = results.map { it.userId } + } + } } map.forEach { entry -> val roomId = entry.key val userList = entry.value taskExecutor.executorScope.launch { - val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(userList)) - BACKGROUND_HANDLER.post { - backgroundSessionRealm.get().executeTransaction { realm -> - roomSummaryUpdater.updateShieldTrust(realm, roomId, updatedTrust) + withContext(coroutineDispatchers.crypto) { + try { + // Can throw if the crypto database has been closed in between, in this case log and ignore? + val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(userList)) + BACKGROUND_HANDLER.post { + backgroundSessionRealm.get()?.executeTransaction { realm -> + roomSummaryUpdater.updateShieldTrust(realm, roomId, updatedTrust) + } + } + } catch (failure: Throwable) { + Timber.e(failure) } } } 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/DefaultKeysBackupService.kt similarity index 89% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/DefaultKeysBackupService.kt index 7906005046..3ec5a2f979 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/DefaultKeysBackupService.kt @@ -40,8 +40,28 @@ import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersi import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrustSignature import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupAuthData import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo -import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.* -import im.vector.matrix.android.internal.crypto.keysbackup.tasks.* +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.BackupKeysResult +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeyBackupData +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysBackupData +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.keysbackup.model.rest.RoomKeysBackupData +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.UpdateKeysBackupVersionBody +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DeleteBackupTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DeleteRoomSessionDataTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DeleteRoomSessionsDataTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DeleteSessionsDataTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.GetKeysBackupLastVersionTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.GetKeysBackupVersionTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.GetRoomSessionDataTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.GetRoomSessionsDataTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.GetSessionsDataTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.StoreRoomSessionDataTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.StoreRoomSessionsDataTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.StoreSessionsDataTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask import im.vector.matrix.android.internal.crypto.keysbackup.util.computeRecoveryKey import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult @@ -60,6 +80,7 @@ import im.vector.matrix.android.internal.util.JsonCanonicalizer import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.awaitCallback import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.olm.OlmException @@ -72,12 +93,11 @@ import javax.inject.Inject import kotlin.random.Random /** - * A KeysBackup class instance manage incremental backup of e2e keys (megolm keys) + * A DefaultKeysBackupService class instance manage incremental backup of e2e keys (megolm keys) * to the user's homeserver. */ - @SessionScope -internal class KeysBackup @Inject constructor( +internal class DefaultKeysBackupService @Inject constructor( @UserId private val userId: String, private val credentials: Credentials, private val cryptoStore: IMXCryptoStore, @@ -148,9 +168,7 @@ internal class KeysBackup @Inject constructor( runCatching { withContext(coroutineDispatchers.crypto) { val olmPkDecryption = OlmPkDecryption() - val megolmBackupAuthData = MegolmBackupAuthData() - - if (password != null) { + val megolmBackupAuthData = if (password != null) { // Generate a private key from the password val backgroundProgressListener = if (progressListener == null) { null @@ -169,25 +187,30 @@ internal class KeysBackup @Inject constructor( } val generatePrivateKeyResult = generatePrivateKeyWithPassword(password, backgroundProgressListener) - megolmBackupAuthData.publicKey = olmPkDecryption.setPrivateKey(generatePrivateKeyResult.privateKey) - megolmBackupAuthData.privateKeySalt = generatePrivateKeyResult.salt - megolmBackupAuthData.privateKeyIterations = generatePrivateKeyResult.iterations + MegolmBackupAuthData( + publicKey = olmPkDecryption.setPrivateKey(generatePrivateKeyResult.privateKey), + privateKeySalt = generatePrivateKeyResult.salt, + privateKeyIterations = generatePrivateKeyResult.iterations + ) } else { val publicKey = olmPkDecryption.generateKey() - megolmBackupAuthData.publicKey = publicKey + MegolmBackupAuthData( + publicKey = publicKey + ) } val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, megolmBackupAuthData.signalableJSONDictionary()) - megolmBackupAuthData.signatures = objectSigner.signObject(canonicalJson) + val signedMegolmBackupAuthData = megolmBackupAuthData.copy( + signatures = objectSigner.signObject(canonicalJson) + ) - val megolmBackupCreationInfo = MegolmBackupCreationInfo() - megolmBackupCreationInfo.algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP - megolmBackupCreationInfo.authData = megolmBackupAuthData - megolmBackupCreationInfo.recoveryKey = computeRecoveryKey(olmPkDecryption.privateKey()) - - megolmBackupCreationInfo + MegolmBackupCreationInfo( + algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP, + authData = signedMegolmBackupAuthData, + recoveryKey = computeRecoveryKey(olmPkDecryption.privateKey()) + ) } }.foldToCallback(callback) } @@ -195,11 +218,12 @@ internal class KeysBackup @Inject constructor( override fun createKeysBackupVersion(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? + val createKeysBackupVersionBody = CreateKeysBackupVersionBody( + algorithm = keysBackupCreationInfo.algorithm, + authData = MoshiProvider.providesMoshi().adapter(Map::class.java) + .fromJson(keysBackupCreationInfo.authData?.toJsonString() ?: "") as JsonDict? + ) keysBackupStateManager.state = KeysBackupState.Enabling @@ -210,14 +234,14 @@ internal class KeysBackup @Inject constructor( // Reset backup markers. cryptoStore.resetBackupMarkers() - val keyBackupVersion = KeysVersionResult() - keyBackupVersion.algorithm = createKeysBackupVersionBody.algorithm - keyBackupVersion.authData = createKeysBackupVersionBody.authData - keyBackupVersion.version = data.version - - // We can consider that the server does not have keys yet - keyBackupVersion.count = 0 - keyBackupVersion.hash = null + val keyBackupVersion = KeysVersionResult( + algorithm = createKeysBackupVersionBody.algorithm, + authData = createKeysBackupVersionBody.authData, + version = data.version, + // We can consider that the server does not have keys yet + count = 0, + hash = null + ) enableKeysBackup(keyBackupVersion) @@ -387,7 +411,7 @@ internal class KeysBackup @Inject constructor( return keysBackupVersionTrust } - val mySigs = authData.signatures?.get(userId) + val mySigs = authData.signatures[userId] if (mySigs.isNullOrEmpty()) { Timber.v("getKeysBackupTrust: Ignoring key backup because it lacks any signatures from this user") return keysBackupVersionTrust @@ -450,8 +474,7 @@ internal class KeysBackup @Inject constructor( cryptoCoroutineScope.launch(coroutineDispatchers.main) { val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) { // Get current signatures, or create an empty set - val myUserSignatures = authData.signatures?.get(userId)?.toMutableMap() - ?: HashMap() + val myUserSignatures = authData.signatures?.get(userId)?.toMutableMap() ?: HashMap() if (trust) { // Add current device signature @@ -468,24 +491,23 @@ internal class KeysBackup @Inject constructor( } // Create an updated version of KeysVersionResult - val updateKeysBackupVersionBody = UpdateKeysBackupVersionBody(keysBackupVersion.version!!) - - updateKeysBackupVersionBody.algorithm = keysBackupVersion.algorithm - val newMegolmBackupAuthData = authData.copy() val newSignatures = newMegolmBackupAuthData.signatures!!.toMutableMap() newSignatures[userId] = myUserSignatures - newMegolmBackupAuthData.signatures = newSignatures + val newMegolmBackupAuthDataWithNewSignature = newMegolmBackupAuthData.copy( + signatures = newSignatures + ) val moshi = MoshiProvider.providesMoshi() val adapter = moshi.adapter(Map::class.java) @Suppress("UNCHECKED_CAST") - updateKeysBackupVersionBody.authData = adapter.fromJson(newMegolmBackupAuthData.toJsonString()) as Map? - - updateKeysBackupVersionBody + UpdateKeysBackupVersionBody( + algorithm = keysBackupVersion.algorithm, + authData = adapter.fromJson(newMegolmBackupAuthDataWithNewSignature.toJsonString()) as Map?, + version = keysBackupVersion.version!!) } // And send it to the homeserver @@ -494,13 +516,13 @@ internal class KeysBackup @Inject constructor( this.callback = object : MatrixCallback { override fun onSuccess(data: Unit) { // Relaunch the state machine on this updated backup version - val newKeysBackupVersion = KeysVersionResult() - - newKeysBackupVersion.version = keysBackupVersion.version - newKeysBackupVersion.algorithm = keysBackupVersion.algorithm - newKeysBackupVersion.count = keysBackupVersion.count - newKeysBackupVersion.hash = keysBackupVersion.hash - newKeysBackupVersion.authData = updateKeysBackupVersionBody.authData + val newKeysBackupVersion = KeysVersionResult( + algorithm = keysBackupVersion.algorithm, + authData = updateKeysBackupVersionBody.authData, + version = keysBackupVersion.version, + hash = keysBackupVersion.hash, + count = keysBackupVersion.count + ) checkAndStartWithKeysBackupVersion(newKeysBackupVersion) @@ -673,7 +695,7 @@ internal class KeysBackup @Inject constructor( null } - val result = megolmSessionDataImporter.handle(sessionsData, !backUp, uiHandler, progressListener) + val result = megolmSessionDataImporter.handle(sessionsData, !backUp, progressListener) // Do not back up the key if it comes from a backup recovery if (backUp) { @@ -788,7 +810,10 @@ internal class KeysBackup @Inject constructor( // new key is sent val delayInMs = Random.nextLong(KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS) - uiHandler.postDelayed({ backupKeys() }, delayInMs) + cryptoCoroutineScope.launch { + delay(delayInMs) + uiHandler.post { backupKeys() } + } } else -> { Timber.v("maybeBackupKeys: Skip it because state: $state") @@ -1005,7 +1030,7 @@ internal class KeysBackup @Inject constructor( } // Extract the recovery key from the passphrase - val data = retrievePrivateKeyWithPassword(password, authData.privateKeySalt!!, authData.privateKeyIterations!!, progressListener) + val data = retrievePrivateKeyWithPassword(password, authData.privateKeySalt, authData.privateKeyIterations, progressListener) return computeRecoveryKey(data) } @@ -1159,14 +1184,16 @@ internal class KeysBackup @Inject constructor( // Gather data to send to the homeserver // roomId -> sessionId -> MXKeyBackupData - val keysBackupData = KeysBackupData() - keysBackupData.roomIdToRoomKeysBackupData = HashMap() + val keysBackupData = KeysBackupData( + roomIdToRoomKeysBackupData = HashMap() + ) for (olmInboundGroupSessionWrapper in olmInboundGroupSessionWrappers) { val keyBackupData = encryptGroupSession(olmInboundGroupSessionWrapper) if (keysBackupData.roomIdToRoomKeysBackupData[olmInboundGroupSessionWrapper.roomId] == null) { - val roomKeysBackupData = RoomKeysBackupData() - roomKeysBackupData.sessionIdToKeyBackupData = HashMap() + val roomKeysBackupData = RoomKeysBackupData( + sessionIdToKeyBackupData = HashMap() + ) keysBackupData.roomIdToRoomKeysBackupData[olmInboundGroupSessionWrapper.roomId!!] = roomKeysBackupData } @@ -1282,24 +1309,21 @@ internal class KeysBackup @Inject constructor( } // Build backup data for that key - val keyBackupData = KeyBackupData() - try { - keyBackupData.firstMessageIndex = olmInboundGroupSessionWrapper.olmInboundGroupSession!!.firstKnownIndex - } catch (e: OlmException) { - Timber.e(e, "OlmException") - } + return KeyBackupData( + firstMessageIndex = try { + olmInboundGroupSessionWrapper.olmInboundGroupSession!!.firstKnownIndex + } catch (e: OlmException) { + Timber.e(e, "OlmException") + 0L + }, + forwardedCount = olmInboundGroupSessionWrapper.forwardingCurve25519KeyChain!!.size, + isVerified = device?.isVerified == true, - keyBackupData.forwardedCount = olmInboundGroupSessionWrapper.forwardingCurve25519KeyChain!!.size - keyBackupData.isVerified = device?.isVerified == true - - val data = mapOf( - "ciphertext" to encryptedSessionBackupData!!.mCipherText, - "mac" to encryptedSessionBackupData.mMac, - "ephemeral" to encryptedSessionBackupData.mEphemeralKey) - - keyBackupData.sessionData = data - - return keyBackupData + sessionData = mapOf( + "ciphertext" to encryptedSessionBackupData!!.mCipherText, + "mac" to encryptedSessionBackupData.mMac, + "ephemeral" to encryptedSessionBackupData.mEphemeralKey) + ) } @VisibleForTesting @@ -1331,8 +1355,10 @@ internal class KeysBackup @Inject constructor( } if (sessionBackupData != null) { - sessionBackupData.sessionId = sessionId - sessionBackupData.roomId = roomId + sessionBackupData = sessionBackupData.copy( + sessionId = sessionId, + roomId = roomId + ) } } @@ -1351,11 +1377,12 @@ internal class KeysBackup @Inject constructor( @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? + val createKeysBackupVersionBody = CreateKeysBackupVersionBody( + algorithm = keysBackupCreationInfo.algorithm, + authData = MoshiProvider.providesMoshi().adapter(Map::class.java) + .fromJson(keysBackupCreationInfo.authData?.toJsonString() ?: "") as JsonDict? + ) createKeysBackupVersionTask .configureWith(createKeysBackupVersionBody) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupPassword.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupPassword.kt index 344ba61277..2429c1e658 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupPassword.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupPassword.kt @@ -83,10 +83,10 @@ fun retrievePrivateKeyWithPassword(password: String, * @return a private key. */ @WorkerThread -private fun deriveKey(password: String, - salt: String, - iterations: Int, - progressListener: ProgressListener?): ByteArray { +fun deriveKey(password: String, + salt: String, + iterations: Int, + progressListener: ProgressListener?): ByteArray { // Note: copied and adapted from MXMegolmExportEncryption val t0 = System.currentTimeMillis() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/api/RoomKeysApi.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/api/RoomKeysApi.kt index f5d89fb5eb..beaee58a3e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/api/RoomKeysApi.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/api/RoomKeysApi.kt @@ -16,10 +16,23 @@ package im.vector.matrix.android.internal.crypto.keysbackup.api -import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.* +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.BackupKeysResult +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeyBackupData +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysBackupData +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.keysbackup.model.rest.RoomKeysBackupData +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.UpdateKeysBackupVersionBody import im.vector.matrix.android.internal.network.NetworkConstants import retrofit2.Call -import retrofit2.http.* +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Path +import retrofit2.http.Query /** * Ref: https://github.com/uhoreg/matrix-doc/blob/e2e_backup/proposals/1219-storing-megolm-keys-serverside.md diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/MegolmBackupAuthData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/MegolmBackupAuthData.kt index 442b1f081c..48015a98dd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/MegolmBackupAuthData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/MegolmBackupAuthData.kt @@ -30,26 +30,27 @@ data class MegolmBackupAuthData( * The curve25519 public key used to encrypt the backups. */ @Json(name = "public_key") - var publicKey: String = "", + val publicKey: String = "", /** * In case of a backup created from a password, the salt associated with the backup * private key. */ @Json(name = "private_key_salt") - var privateKeySalt: String? = null, + val privateKeySalt: String? = null, /** * In case of a backup created from a password, the number of key derivations. */ @Json(name = "private_key_iterations") - var privateKeyIterations: Int? = null, + val privateKeyIterations: Int? = null, /** * Signatures of the public key. * userId -> (deviceSignKeyId -> signature) */ - var signatures: Map>? = null + @Json(name = "signatures") + val signatures: Map>? = null ) { fun toJsonString(): String { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/MegolmBackupCreationInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/MegolmBackupCreationInfo.kt index a08ba9ba96..b329fa44c9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/MegolmBackupCreationInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/MegolmBackupCreationInfo.kt @@ -19,20 +19,19 @@ package im.vector.matrix.android.internal.crypto.keysbackup.model /** * Data retrieved from Olm library. algorithm and authData will be send to the homeserver, and recoveryKey will be displayed to the user */ -class MegolmBackupCreationInfo { +data class MegolmBackupCreationInfo( + /** + * The algorithm used for storing backups [org.matrix.androidsdk.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP]. + */ + val algorithm: String = "", - /** - * The algorithm used for storing backups [org.matrix.androidsdk.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP]. - */ - var algorithm: String = "" + /** + * Authentication data. + */ + val authData: MegolmBackupAuthData? = null, - /** - * Authentication data. - */ - var authData: MegolmBackupAuthData? = null - - /** - * The Base58 recovery key. - */ - var recoveryKey: String = "" -} + /** + * The Base58 recovery key. + */ + val recoveryKey: String = "" +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/CreateKeysBackupVersionBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/CreateKeysBackupVersionBody.kt index 5efbc6d017..3b267280e5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/CreateKeysBackupVersionBody.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/CreateKeysBackupVersionBody.kt @@ -16,7 +16,21 @@ package im.vector.matrix.android.internal.crypto.keysbackup.model.rest +import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.util.JsonDict @JsonClass(generateAdapter = true) -class CreateKeysBackupVersionBody : KeysAlgorithmAndData() +data class CreateKeysBackupVersionBody( + /** + * The algorithm used for storing backups. Currently, only "m.megolm_backup.v1.curve25519-aes-sha2" is defined + */ + @Json(name = "algorithm") + override val algorithm: String? = null, + + /** + * algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" see [im.vector.matrix.android.internal.crypto.keysbackup.MegolmBackupAuthData] + */ + @Json(name = "auth_data") + override val authData: JsonDict? = null +) : KeysAlgorithmAndData diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeyBackupData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeyBackupData.kt index f172d45ffd..b2d10687aa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeyBackupData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeyBackupData.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.crypto.keysbackup.model.rest import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import im.vector.matrix.android.internal.di.MoshiProvider +import im.vector.matrix.android.internal.network.parsing.ForceToBoolean /** * Backup data for one key. @@ -29,25 +30,27 @@ data class KeyBackupData( * Required. The index of the first message in the session that the key can decrypt. */ @Json(name = "first_message_index") - var firstMessageIndex: Long = 0, + val firstMessageIndex: Long = 0, /** * Required. The number of times this key has been forwarded. */ @Json(name = "forwarded_count") - var forwardedCount: Int = 0, + val forwardedCount: Int = 0, /** * Whether the device backing up the key has verified the device that the key is from. + * Force to boolean because of https://github.com/matrix-org/synapse/issues/6977 */ + @ForceToBoolean @Json(name = "is_verified") - var isVerified: Boolean = false, + val isVerified: Boolean = false, /** * Algorithm-dependent data. */ @Json(name = "session_data") - var sessionData: Map? = null + val sessionData: Map? = null ) { fun toJsonString(): String { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeysAlgorithmAndData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeysAlgorithmAndData.kt index 6fba833589..81ca6586a3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeysAlgorithmAndData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeysAlgorithmAndData.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.crypto.keysbackup.model.rest -import com.squareup.moshi.Json import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupAuthData import im.vector.matrix.android.internal.di.MoshiProvider @@ -38,19 +37,17 @@ import im.vector.matrix.android.internal.di.MoshiProvider * } * */ -open class KeysAlgorithmAndData { +interface KeysAlgorithmAndData { /** * The algorithm used for storing backups. Currently, only "m.megolm_backup.v1.curve25519-aes-sha2" is defined */ - @Json(name = "algorithm") - var algorithm: String? = null + val algorithm: String? /** * algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" see [im.vector.matrix.android.internal.crypto.keysbackup.MegolmBackupAuthData] */ - @Json(name = "auth_data") - var authData: JsonDict? = null + val authData: JsonDict? /** * Facility method to convert authData to a MegolmBackupAuthData object diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeysBackupData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeysBackupData.kt index 2f4165d8ab..240c79fd1e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeysBackupData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeysBackupData.kt @@ -24,9 +24,7 @@ import com.squareup.moshi.JsonClass */ @JsonClass(generateAdapter = true) data class KeysBackupData( - // the keys are the room IDs, and the values are RoomKeysBackupData @Json(name = "rooms") - var roomIdToRoomKeysBackupData: MutableMap = HashMap() - + val roomIdToRoomKeysBackupData: MutableMap = HashMap() ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeysVersionResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeysVersionResult.kt index 4510cdd773..0addd1491e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeysVersionResult.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeysVersionResult.kt @@ -16,16 +16,33 @@ package im.vector.matrix.android.internal.crypto.keysbackup.model.rest +import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.util.JsonDict @JsonClass(generateAdapter = true) data class KeysVersionResult( + /** + * The algorithm used for storing backups. Currently, only "m.megolm_backup.v1.curve25519-aes-sha2" is defined + */ + @Json(name = "algorithm") + override val algorithm: String? = null, + + /** + * algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" see [im.vector.matrix.android.internal.crypto.keysbackup.MegolmBackupAuthData] + */ + @Json(name = "auth_data") + override val authData: JsonDict? = null, + // the backup version - var version: String? = null, + @Json(name = "version") + val version: String? = null, // The hash value which is an opaque string representing stored keys in the backup - var hash: String? = null, + @Json(name = "hash") + val hash: String? = null, // The number of keys stored in the backup. - var count: Int? = null -) : KeysAlgorithmAndData() + @Json(name = "count") + val count: Int? = null +) : KeysAlgorithmAndData diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/RoomKeysBackupData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/RoomKeysBackupData.kt index 5d69f63538..f3c218baca 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/RoomKeysBackupData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/RoomKeysBackupData.kt @@ -24,8 +24,7 @@ import com.squareup.moshi.JsonClass */ @JsonClass(generateAdapter = true) data class RoomKeysBackupData( - // the keys are the session IDs, and the values are KeyBackupData @Json(name = "sessions") - var sessionIdToKeyBackupData: MutableMap = HashMap() + val sessionIdToKeyBackupData: MutableMap = HashMap() ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/UpdateKeysBackupVersionBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/UpdateKeysBackupVersionBody.kt index cb8ba5e26c..9d88af20ef 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/UpdateKeysBackupVersionBody.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/UpdateKeysBackupVersionBody.kt @@ -16,10 +16,25 @@ package im.vector.matrix.android.internal.crypto.keysbackup.model.rest +import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.util.JsonDict @JsonClass(generateAdapter = true) data class UpdateKeysBackupVersionBody( + /** + * The algorithm used for storing backups. Currently, only "m.megolm_backup.v1.curve25519-aes-sha2" is defined + */ + @Json(name = "algorithm") + override val algorithm: String? = null, + + /** + * algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" see [im.vector.matrix.android.internal.crypto.keysbackup.MegolmBackupAuthData] + */ + @Json(name = "auth_data") + override val authData: JsonDict? = null, + // the backup version, mandatory + @Json(name = "version") val version: String -) : KeysAlgorithmAndData() +) : KeysAlgorithmAndData diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoDeviceInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoDeviceInfo.kt index 8f6d64221c..e3e8f3de27 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoDeviceInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoDeviceInfo.kt @@ -28,10 +28,6 @@ data class CryptoDeviceInfo( override val keys: Map? = null, override val signatures: Map>? = null, val unsigned: JsonDict? = null, - - // TODO how to store if this device is verified by a user SSK, or is legacy trusted? - // I need to know if it is trusted via cross signing (Trusted because bob verified it) - var trustLevel: DeviceTrustLevel? = null, var isBlocked: Boolean = false ) : CryptoInfo { @@ -75,19 +71,6 @@ data class CryptoDeviceInfo( keys?.let { map["keys"] = it } return map } -// -// /** -// * @return a dictionary of the parameters -// */ -// fun toDeviceKeys(): DeviceKeys { -// return DeviceKeys( -// userId = userId, -// deviceId = deviceId, -// algorithms = algorithms!!, -// keys = keys!!, -// signatures = signatures!! -// ) -// } } internal fun CryptoDeviceInfo.toRest(): RestDeviceInfo { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXDeviceInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXDeviceInfo.kt index cc9b3bff74..ae53694a0f 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXDeviceInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXDeviceInfo.kt @@ -26,48 +26,47 @@ import java.io.Serializable @JsonClass(generateAdapter = true) data class MXDeviceInfo( - /** * The id of this device. */ @Json(name = "device_id") - var deviceId: String, + val deviceId: String, /** * the user id */ @Json(name = "user_id") - var userId: String, + val userId: String, /** * The list of algorithms supported by this device. */ @Json(name = "algorithms") - var algorithms: List? = null, + val algorithms: List? = null, /** * A map from ":" to "". */ @Json(name = "keys") - var keys: Map? = null, + val keys: Map? = null, /** * The signature of this MXDeviceInfo. * A map from "" to a map from ":" to "" */ @Json(name = "signatures") - var signatures: Map>? = null, + val signatures: Map>? = null, /* * Additional data from the home server. */ @Json(name = "unsigned") - var unsigned: JsonDict? = null, + val unsigned: JsonDict? = null, /** * Verification state of this device. */ - var verified: Int = DEVICE_VERIFICATION_UNKNOWN + val verified: Int = DEVICE_VERIFICATION_UNKNOWN ) : Serializable { /** * Tells if the device is unknown @@ -137,11 +136,11 @@ data class MXDeviceInfo( map["user_id"] = userId if (null != algorithms) { - map["algorithms"] = algorithms!! + map["algorithms"] = algorithms } if (null != keys) { - map["keys"] = keys!! + map["keys"] = keys } return map diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/OlmInboundGroupSessionWrapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/OlmInboundGroupSessionWrapper.kt index 361b8bc205..cf1a3b237a 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/OlmInboundGroupSessionWrapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/OlmInboundGroupSessionWrapper.kt @@ -116,16 +116,16 @@ class OlmInboundGroupSessionWrapper : Serializable { return null } - MegolmSessionData().also { - it.senderClaimedEd25519Key = keysClaimed?.get("ed25519") - it.forwardingCurve25519KeyChain = ArrayList(forwardingCurve25519KeyChain!!) - it.senderKey = senderKey - it.senderClaimedKeys = keysClaimed - it.roomId = roomId - it.sessionId = olmInboundGroupSession!!.sessionIdentifier() - it.sessionKey = olmInboundGroupSession!!.export(olmInboundGroupSession!!.firstKnownIndex) - it.algorithm = MXCRYPTO_ALGORITHM_MEGOLM - } + MegolmSessionData( + senderClaimedEd25519Key = keysClaimed?.get("ed25519"), + forwardingCurve25519KeyChain = ArrayList(forwardingCurve25519KeyChain!!), + senderKey = senderKey, + senderClaimedKeys = keysClaimed, + roomId = roomId, + sessionId = olmInboundGroupSession!!.sessionIdentifier(), + sessionKey = olmInboundGroupSession!!.export(olmInboundGroupSession!!.firstKnownIndex), + algorithm = MXCRYPTO_ALGORITHM_MEGOLM + ) } catch (e: Exception) { Timber.e(e, "## export() : senderKey $senderKey failed") null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/EncryptionEventContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/EncryptionEventContent.kt index 6de50f84c2..05e97da68d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/EncryptionEventContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/EncryptionEventContent.kt @@ -23,22 +23,21 @@ import com.squareup.moshi.JsonClass */ @JsonClass(generateAdapter = true) data class EncryptionEventContent( - /** * Required. The encryption algorithm to be used to encrypt messages sent in this room. Must be 'm.megolm.v1.aes-sha2'. */ @Json(name = "algorithm") - var algorithm: String, + val algorithm: String, /** * How long the session should be used before changing it. 604800000 (a week) is the recommended default. */ @Json(name = "rotation_period_ms") - var rotationPeriodMs: Long? = null, + val rotationPeriodMs: Long? = null, /** * How many messages should be sent before changing the session. 100 is the recommended default. */ @Json(name = "rotation_period_msgs") - var rotationPeriodMsgs: Long? = null + val rotationPeriodMsgs: Long? = null ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/NewDeviceContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/NewDeviceContent.kt index a6777a4f12..62fe4293e7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/NewDeviceContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/NewDeviceContent.kt @@ -20,12 +20,11 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class NewDeviceContent( - // the device id @Json(name = "device_id") - var deviceId: String? = null, + val deviceId: String? = null, // the room ids list @Json(name = "rooms") - var rooms: List? = null + val rooms: List? = null ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/OlmEventContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/OlmEventContent.kt index 7ac0b075be..2b2b49120a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/OlmEventContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/OlmEventContent.kt @@ -27,11 +27,11 @@ data class OlmEventContent( * */ @Json(name = "ciphertext") - var ciphertext: Map? = null, + val ciphertext: Map? = null, /** * the sender key */ @Json(name = "sender_key") - var senderKey: String? = null + val senderKey: String? = null ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DeviceInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DeviceInfo.kt index 1289ef3d92..b058fac082 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DeviceInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DeviceInfo.kt @@ -30,31 +30,31 @@ data class DeviceInfo( * The owner user id (not documented and useless but the homeserver sent it. You should not need it) */ @Json(name = "user_id") - var user_id: String? = null, + val user_id: String? = null, /** * The device id */ @Json(name = "device_id") - var deviceId: String? = null, + val deviceId: String? = null, /** * The device display name */ @Json(name = "display_name") - var displayName: String? = null, + val displayName: String? = null, /** * The last time this device has been seen. */ @Json(name = "last_seen_ts") - var lastSeenTs: Long? = null, + val lastSeenTs: Long? = null, /** * The last ip address */ @Json(name = "last_seen_ip") - var lastSeenIp: String? = null + val lastSeenIp: String? = null ) : DatedObject { override val date: Long diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DevicesListResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DevicesListResponse.kt index 9b50b486dc..2bf3d06299 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DevicesListResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DevicesListResponse.kt @@ -24,5 +24,5 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class DevicesListResponse( @Json(name = "devices") - var devices: List? = null + val devices: List? = null ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedFileInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedFileInfo.kt index 5e09b20c91..93e8b4d211 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedFileInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedFileInfo.kt @@ -27,38 +27,38 @@ data class EncryptedFileInfo( * Required. The URL to the file. */ @Json(name = "url") - var url: String? = null, + val url: String? = null, /** * Not documented */ @Json(name = "mimetype") - var mimetype: String? = null, + val mimetype: String? = null, /** * Required. A JSON Web Key object. */ @Json(name = "key") - var key: EncryptedFileKey? = null, + val key: EncryptedFileKey? = null, /** * Required. The Initialisation Vector used by AES-CTR, encoded as unpadded base64. */ @Json(name = "iv") - var iv: String? = null, + val iv: String? = null, /** * Required. A map from an algorithm name to a hash of the ciphertext, encoded as unpadded base64. * Clients should support the SHA-256 hash, which uses the key "sha256". */ @Json(name = "hashes") - var hashes: Map? = null, + val hashes: Map? = null, /** * Required. Version of the encrypted attachments protocol. Must be "v2". */ @Json(name = "v") - var v: String? = null + val v: String? = null ) { /** * Check what the spec tells us diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedFileKey.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedFileKey.kt index 799819ceee..fa5885de49 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedFileKey.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedFileKey.kt @@ -24,31 +24,31 @@ data class EncryptedFileKey( * Required. Algorithm. Must be "A256CTR". */ @Json(name = "alg") - var alg: String? = null, + val alg: String? = null, /** * Required. Extractable. Must be true. This is a W3C extension. */ @Json(name = "ext") - var ext: Boolean? = null, + val ext: Boolean? = null, /** * Required. Key operations. Must at least contain "encrypt" and "decrypt". */ @Json(name = "key_ops") - var key_ops: List? = null, + val key_ops: List? = null, /** * Required. Key type. Must be "oct". */ @Json(name = "kty") - var kty: String? = null, + val kty: String? = null, /** * Required. The key, encoded as urlsafe unpadded base64. */ @Json(name = "k") - var k: String? = null + val k: String? = null ) { /** * Check what the spec tells us @@ -62,7 +62,7 @@ data class EncryptedFileKey( return false } - if (key_ops?.contains("encrypt") != true || key_ops?.contains("decrypt") != true) { + if (key_ops?.contains("encrypt") != true || !key_ops.contains("decrypt")) { return false } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedMessage.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedMessage.kt index c546cd04c4..e3ada0c0ab 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedMessage.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedMessage.kt @@ -21,11 +21,12 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class EncryptedMessage( - var algorithm: String? = null, + @Json(name = "algorithm") + val algorithm: String? = null, @Json(name = "sender_key") - var senderKey: String? = null, + val senderKey: String? = null, @Json(name = "ciphertext") - var cipherText: Map? = null + val cipherText: Map? = null ) : SendToDeviceObject diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyChangesResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyChangesResponse.kt index 12d27a023f..3af7d7c8c5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyChangesResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyChangesResponse.kt @@ -25,9 +25,9 @@ import com.squareup.moshi.JsonClass internal data class KeyChangesResponse( // list of user ids which have new devices @Json(name = "changed") - var changed: List? = null, + val changed: List? = null, // List of user ids who are no more tracked. @Json(name = "left") - var left: List? = null + val left: List? = null ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationCancel.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationCancel.kt index d1ae09cc37..dcf08531c8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationCancel.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationCancel.kt @@ -17,7 +17,7 @@ package im.vector.matrix.android.internal.crypto.model.rest import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import im.vector.matrix.android.api.session.crypto.sas.CancelCode +import im.vector.matrix.android.api.session.crypto.verification.CancelCode import im.vector.matrix.android.internal.crypto.verification.VerificationInfoCancel /** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationDone.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationDone.kt index c0a72d29db..bdce77b31d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationDone.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationDone.kt @@ -24,7 +24,7 @@ import im.vector.matrix.android.internal.crypto.verification.VerificationInfoDon */ @JsonClass(generateAdapter = true) internal data class KeyVerificationDone( - @Json(name = "transaction_id") override var transactionID: String? = null + @Json(name = "transaction_id") override val transactionID: String? = null ) : SendToDeviceObject, VerificationInfoDone { override fun toSendToDeviceObject() = this diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationKey.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationKey.kt index 9a190e1e15..4f90f8db8c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationKey.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationKey.kt @@ -17,8 +17,8 @@ package im.vector.matrix.android.internal.crypto.model.rest import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import im.vector.matrix.android.internal.crypto.verification.VerificationInfoKeyFactory import im.vector.matrix.android.internal.crypto.verification.VerificationInfoKey +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoKeyFactory /** * Sent by both devices to send their ephemeral Curve25519 public key to the other device. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationRequest.kt index 5bd09658b5..fcddb5c3d4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationRequest.kt @@ -27,7 +27,7 @@ internal data class KeyVerificationRequest( @Json(name = "from_device") override val fromDevice: String?, @Json(name = "methods") override val methods: List, @Json(name = "timestamp") override val timestamp: Long?, - @Json(name = "transaction_id") override var transactionID: String? = null + @Json(name = "transaction_id") override val transactionID: String? = null ) : SendToDeviceObject, VerificationInfoRequest { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationStart.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationStart.kt index 9e4b7b773e..c6a64fc2c8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationStart.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationStart.kt @@ -17,7 +17,7 @@ package im.vector.matrix.android.internal.crypto.model.rest import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import im.vector.matrix.android.api.session.crypto.sas.SasMode +import im.vector.matrix.android.api.session.crypto.verification.SasMode import im.vector.matrix.android.internal.crypto.verification.SASDefaultVerificationTransaction import im.vector.matrix.android.internal.crypto.verification.VerificationInfoStart import im.vector.matrix.android.internal.util.JsonCanonicalizer diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysClaimBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysClaimBody.kt index 38f6615dad..26ee1ebe38 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysClaimBody.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysClaimBody.kt @@ -24,16 +24,15 @@ import com.squareup.moshi.JsonClass */ @JsonClass(generateAdapter = true) internal data class KeysClaimBody( - /** * The time (in milliseconds) to wait when downloading keys from remote servers. 10 seconds is the recommended default. */ @Json(name = "timeout") - var timeout: Int? = null, + val timeout: Int? = null, /** * Required. The keys to be claimed. A map from user ID, to a map from device ID to algorithm name. */ @Json(name = "one_time_keys") - var oneTimeKeys: Map> + val oneTimeKeys: Map> ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysClaimResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysClaimResponse.kt index 59567ba77a..3483873fbb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysClaimResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysClaimResponse.kt @@ -24,11 +24,10 @@ import com.squareup.moshi.JsonClass */ @JsonClass(generateAdapter = true) internal data class KeysClaimResponse( - /** * The requested keys ordered by device by user. * TODO Type does not match spec, should be Map */ @Json(name = "one_time_keys") - var oneTimeKeys: Map>>>? = null + val oneTimeKeys: Map>>>? = null ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryBody.kt index 3dca696fcd..da2dd781dd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryBody.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryBody.kt @@ -25,12 +25,11 @@ import com.squareup.moshi.JsonClass */ @JsonClass(generateAdapter = true) internal data class KeysQueryBody( - /** * The time (in milliseconds) to wait when downloading keys from remote servers. 10 seconds is the recommended default. */ @Json(name = "timeout") - var timeout: Int? = null, + val timeout: Int? = null, /** * Required. The keys to be downloaded. @@ -45,6 +44,5 @@ internal data class KeysQueryBody( * by the notification in that sync. */ @Json(name = "token") - var token: String? = null - + val token: String? = null ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysUploadResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysUploadResponse.kt index 38360fa1cd..cd71749acf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysUploadResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysUploadResponse.kt @@ -23,13 +23,11 @@ import com.squareup.moshi.JsonClass */ @JsonClass(generateAdapter = true) internal data class KeysUploadResponse( - /** * The count per algorithm as returned by the home server: a map (algorithm to count). */ @Json(name = "one_time_key_counts") - var oneTimeKeyCounts: Map? = null - + val oneTimeKeyCounts: Map? = null ) { /** * Helper methods to extract information from 'oneTimeKeyCounts' diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyRequestBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyRequestBody.kt index 06f70ee25b..3eb6600e5e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyRequestBody.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyRequestBody.kt @@ -25,14 +25,14 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class RoomKeyRequestBody( @Json(name = "algorithm") - var algorithm: String? = null, + val algorithm: String? = null, @Json(name = "room_id") - var roomId: String? = null, + val roomId: String? = null, @Json(name = "sender_key") - var senderKey: String? = null, + val senderKey: String? = null, @Json(name = "session_id") - var sessionId: String? = null + val sessionId: String? = null ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShare.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShare.kt index de2345e002..4ea95d84ae 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShare.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShare.kt @@ -15,21 +15,17 @@ */ package im.vector.matrix.android.internal.crypto.model.rest -import com.squareup.moshi.Json - /** - * Parent class representing an room key action request + * Interface representing an room key action request * Note: this class cannot be abstract because of [org.matrix.androidsdk.core.JsonUtils.toRoomKeyShare] */ -internal open class RoomKeyShare : SendToDeviceObject { +internal interface RoomKeyShare : SendToDeviceObject { - var action: String? = null + val action: String? - @Json(name = "requesting_device_id") - var requestingDeviceId: String? = null + val requestingDeviceId: String? - @Json(name = "request_id") - var requestId: String? = null + val requestId: String? companion object { const val ACTION_SHARE_REQUEST = "request" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareCancellation.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareCancellation.kt index fcfbfccbac..b394993338 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareCancellation.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareCancellation.kt @@ -15,14 +15,20 @@ */ package im.vector.matrix.android.internal.crypto.model.rest +import com.squareup.moshi.Json import com.squareup.moshi.JsonClass /** - * Class representing an room key request cancellation content + * Class representing a room key request cancellation content */ @JsonClass(generateAdapter = true) -internal class RoomKeyShareCancellation : RoomKeyShare() { - init { - action = ACTION_SHARE_CANCELLATION - } -} +internal data class RoomKeyShareCancellation( + @Json(name = "action") + override val action: String? = RoomKeyShare.ACTION_SHARE_CANCELLATION, + + @Json(name = "requesting_device_id") + override val requestingDeviceId: String? = null, + + @Json(name = "request_id") + override val requestId: String? = null +) : RoomKeyShare diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareRequest.kt index 3b9d210812..d92bc03aab 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareRequest.kt @@ -16,16 +16,23 @@ */ package im.vector.matrix.android.internal.crypto.model.rest +import com.squareup.moshi.Json import com.squareup.moshi.JsonClass /** - * Class representing an room key request content + * Class representing a room key request content */ @JsonClass(generateAdapter = true) -internal class RoomKeyShareRequest : RoomKeyShare() { - var body: RoomKeyRequestBody? = null +internal data class RoomKeyShareRequest( + @Json(name = "action") + override val action: String? = RoomKeyShare.ACTION_SHARE_REQUEST, - init { - action = ACTION_SHARE_REQUEST - } -} + @Json(name = "requesting_device_id") + override val requestingDeviceId: String? = null, + + @Json(name = "request_id") + override val requestId: String? = null, + + @Json(name = "body") + val body: RoomKeyRequestBody? = null +) : RoomKeyShare diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UpdateDeviceInfoBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UpdateDeviceInfoBody.kt index f2ea24a960..8ae373ba8c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UpdateDeviceInfoBody.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UpdateDeviceInfoBody.kt @@ -25,5 +25,5 @@ internal data class UpdateDeviceInfoBody( * The new display name for this device. If not given, the display name is unchanged. */ @Json(name = "display_name") - var displayName: String? = null + val displayName: String? = null ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/VerificationMethodValues.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/VerificationMethodValues.kt index 643ac5a495..98613e147a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/VerificationMethodValues.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/VerificationMethodValues.kt @@ -16,7 +16,7 @@ package im.vector.matrix.android.internal.crypto.model.rest -import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod +import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod internal const val VERIFICATION_METHOD_SAS = "m.sas.v1" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt new file mode 100644 index 0000000000..9627492dc7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt @@ -0,0 +1,432 @@ +/* + * Copyright (c) 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.crypto.secrets + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.extensions.orFalse +import im.vector.matrix.android.api.listeners.ProgressListener +import im.vector.matrix.android.api.session.accountdata.AccountDataService +import im.vector.matrix.android.api.session.events.model.toContent +import im.vector.matrix.android.api.session.securestorage.EncryptedSecretContent +import im.vector.matrix.android.api.session.securestorage.IntegrityResult +import im.vector.matrix.android.api.session.securestorage.KeyInfo +import im.vector.matrix.android.api.session.securestorage.KeyInfoResult +import im.vector.matrix.android.api.session.securestorage.KeySigner +import im.vector.matrix.android.api.session.securestorage.RawBytesKeySpec +import im.vector.matrix.android.api.session.securestorage.SecretStorageKeyContent +import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageError +import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService +import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo +import im.vector.matrix.android.api.session.securestorage.SsssKeySpec +import im.vector.matrix.android.api.session.securestorage.SsssPassphrase +import im.vector.matrix.android.internal.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2 +import im.vector.matrix.android.internal.crypto.SSSS_ALGORITHM_CURVE25519_AES_SHA2 +import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64NoPadding +import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding +import im.vector.matrix.android.internal.crypto.keysbackup.generatePrivateKeyWithPassword +import im.vector.matrix.android.internal.crypto.keysbackup.util.computeRecoveryKey +import im.vector.matrix.android.internal.crypto.tools.HkdfSha256 +import im.vector.matrix.android.internal.crypto.tools.withOlmDecryption +import im.vector.matrix.android.internal.extensions.foldToCallback +import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.matrix.olm.OlmPkMessage +import java.security.SecureRandom +import javax.crypto.Cipher +import javax.crypto.Mac +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec +import javax.inject.Inject +import kotlin.experimental.and + +internal class DefaultSharedSecretStorageService @Inject constructor( + private val accountDataService: AccountDataService, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val cryptoCoroutineScope: CoroutineScope +) : SharedSecretStorageService { + + override fun generateKey(keyId: String, + keyName: String, + keySigner: KeySigner?, + callback: MatrixCallback) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + val key = try { + ByteArray(32).also { + SecureRandom().nextBytes(it) + } + } catch (failure: Throwable) { + callback.onFailure(failure) + return@launch + } + + val storageKeyContent = SecretStorageKeyContent( + name = keyName, + algorithm = SSSS_ALGORITHM_AES_HMAC_SHA2, + passphrase = null + ) + + val signedContent = keySigner?.sign(storageKeyContent.canonicalSignable())?.let { + storageKeyContent.copy( + signatures = it + ) + } ?: storageKeyContent + + accountDataService.updateAccountData( + "$KEY_ID_BASE.$keyId", + signedContent.toContent(), + object : MatrixCallback { + override fun onFailure(failure: Throwable) { + callback.onFailure(failure) + } + + override fun onSuccess(data: Unit) { + callback.onSuccess(SsssKeyCreationInfo( + keyId = keyId, + content = storageKeyContent, + recoveryKey = computeRecoveryKey(key) + )) + } + } + ) + } + } + + override fun generateKeyWithPassphrase(keyId: String, + keyName: String, + passphrase: String, + keySigner: KeySigner, + progressListener: ProgressListener?, + callback: MatrixCallback) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + val privatePart = generatePrivateKeyWithPassword(passphrase, progressListener) + + val storageKeyContent = SecretStorageKeyContent( + algorithm = SSSS_ALGORITHM_AES_HMAC_SHA2, + passphrase = SsssPassphrase(algorithm = "m.pbkdf2", iterations = privatePart.iterations, salt = privatePart.salt) + ) + + val signedContent = keySigner.sign(storageKeyContent.canonicalSignable())?.let { + storageKeyContent.copy( + signatures = it + ) + } ?: storageKeyContent + + accountDataService.updateAccountData( + "$KEY_ID_BASE.$keyId", + signedContent.toContent(), + object : MatrixCallback { + override fun onFailure(failure: Throwable) { + callback.onFailure(failure) + } + + override fun onSuccess(data: Unit) { + callback.onSuccess(SsssKeyCreationInfo( + keyId = keyId, + content = storageKeyContent, + recoveryKey = computeRecoveryKey(privatePart.privateKey) + )) + } + } + ) + } + } + + override fun hasKey(keyId: String): Boolean { + return accountDataService.getAccountDataEvent("$KEY_ID_BASE.$keyId") != null + } + + override fun getKey(keyId: String): KeyInfoResult { + val accountData = accountDataService.getAccountDataEvent("$KEY_ID_BASE.$keyId") + ?: return KeyInfoResult.Error(SharedSecretStorageError.UnknownKey(keyId)) + return SecretStorageKeyContent.fromJson(accountData.content)?.let { + KeyInfoResult.Success( + KeyInfo(id = keyId, content = it) + ) + } ?: KeyInfoResult.Error(SharedSecretStorageError.UnknownAlgorithm(keyId)) + } + + override fun setDefaultKey(keyId: String, callback: MatrixCallback) { + val existingKey = getKey(keyId) + if (existingKey is KeyInfoResult.Success) { + accountDataService.updateAccountData(DEFAULT_KEY_ID, + mapOf("key" to keyId), + callback + ) + } else { + callback.onFailure(SharedSecretStorageError.UnknownKey(keyId)) + } + } + + override fun getDefaultKey(): KeyInfoResult { + val accountData = accountDataService.getAccountDataEvent(DEFAULT_KEY_ID) + ?: return KeyInfoResult.Error(SharedSecretStorageError.UnknownKey(DEFAULT_KEY_ID)) + val keyId = accountData.content["key"] as? String + ?: return KeyInfoResult.Error(SharedSecretStorageError.UnknownKey(DEFAULT_KEY_ID)) + return getKey(keyId) + } + + override fun storeSecret(name: String, secretBase64: String, keys: List, callback: MatrixCallback) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + val encryptedContents = HashMap() + try { + keys.forEach { + val keyId = it.keyId + // encrypt the content + when (val key = keyId?.let { getKey(keyId) } ?: getDefaultKey()) { + is KeyInfoResult.Success -> { + if (key.keyInfo.content.algorithm == SSSS_ALGORITHM_AES_HMAC_SHA2) { + encryptAesHmacSha2(it.keySpec!!, name, secretBase64).let { + encryptedContents[key.keyInfo.id] = it + } + } else { + // Unknown algorithm + callback.onFailure(SharedSecretStorageError.UnknownAlgorithm(key.keyInfo.content.algorithm ?: "")) + return@launch + } + } + is KeyInfoResult.Error -> { + callback.onFailure(key.error) + return@launch + } + } + } + + accountDataService.updateAccountData( + type = name, + content = mapOf( + "encrypted" to encryptedContents + ), + callback = callback + ) + } catch (failure: Throwable) { + callback.onFailure(failure) + } + } + } + + /** + * Encryption algorithm m.secret_storage.v1.aes-hmac-sha2 + * Secrets are encrypted using AES-CTR-256 and MACed using HMAC-SHA-256. The data is encrypted and MACed as follows: + * + * Given the secret storage key, generate 64 bytes by performing an HKDF with SHA-256 as the hash, a salt of 32 bytes + * of 0, and with the secret name as the info. + * + * The first 32 bytes are used as the AES key, and the next 32 bytes are used as the MAC key + * + * Generate 16 random bytes, set bit 63 to 0 (in order to work around differences in AES-CTR implementations), and use + * this as the AES initialization vector. + * This becomes the iv property, encoded using base64. + * + * Encrypt the data using AES-CTR-256 using the AES key generated above. + * + * This encrypted data, encoded using base64, becomes the ciphertext property. + * + * Pass the raw encrypted data (prior to base64 encoding) through HMAC-SHA-256 using the MAC key generated above. + * The resulting MAC is base64-encoded and becomes the mac property. + * (We use AES-CTR to match file encryption and key exports.) + */ + @Throws + private fun encryptAesHmacSha2(secretKey: SsssKeySpec, secretName: String, clearDataBase64: String): EncryptedSecretContent { + secretKey as RawBytesKeySpec + val pseudoRandomKey = HkdfSha256.deriveSecret( + secretKey.privateKey, + ByteArray(32) { 0.toByte() }, + secretName.toByteArray(), + 64) + + // The first 32 bytes are used as the AES key, and the next 32 bytes are used as the MAC key + val aesKey = pseudoRandomKey.copyOfRange(0, 32) + val macKey = pseudoRandomKey.copyOfRange(32, 64) + + val secureRandom = SecureRandom() + val iv = ByteArray(16) + secureRandom.nextBytes(iv) + + // clear bit 63 of the salt to stop us hitting the 64-bit counter boundary + // (which would mean we wouldn't be able to decrypt on Android). The loss + // of a single bit of salt is a price we have to pay. + iv[9] = iv[9] and 0x7f + + val cipher = Cipher.getInstance("AES/CTR/NoPadding") + + val secretKeySpec = SecretKeySpec(aesKey, "AES") + val ivParameterSpec = IvParameterSpec(iv) + cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec) + // secret are not that big, just do Final + val cipherBytes = cipher.doFinal(clearDataBase64.fromBase64NoPadding()) + require(cipherBytes.isNotEmpty()) + + val macKeySpec = SecretKeySpec(macKey, "HmacSHA256") + val mac = Mac.getInstance("HmacSHA256") + mac.init(macKeySpec) + val digest = mac.doFinal(cipherBytes) + + return EncryptedSecretContent( + ciphertext = cipherBytes.toBase64NoPadding(), + initializationVector = iv.toBase64NoPadding(), + mac = digest.toBase64NoPadding() + ) + } + + private fun decryptAesHmacSha2(secretKey: SsssKeySpec, secretName: String, cipherContent: EncryptedSecretContent): String { + secretKey as RawBytesKeySpec + val pseudoRandomKey = HkdfSha256.deriveSecret( + secretKey.privateKey, + ByteArray(32) { 0.toByte() }, + secretName.toByteArray(), + 64) + + // The first 32 bytes are used as the AES key, and the next 32 bytes are used as the MAC key + val aesKey = pseudoRandomKey.copyOfRange(0, 32) + val macKey = pseudoRandomKey.copyOfRange(32, 64) + + val iv = cipherContent.initializationVector?.fromBase64NoPadding() ?: ByteArray(16) + + val cipherRawBytes = cipherContent.ciphertext!!.fromBase64NoPadding() + + val cipher = Cipher.getInstance("AES/CTR/NoPadding") + + val secretKeySpec = SecretKeySpec(aesKey, "AES") + val ivParameterSpec = IvParameterSpec(iv) + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec) + // secret are not that big, just do Final + val decryptedSecret = cipher.doFinal(cipherRawBytes) + + require(decryptedSecret.isNotEmpty()) + + // Check Signature + val macKeySpec = SecretKeySpec(macKey, "HmacSHA256") + val mac = Mac.getInstance("HmacSHA256").apply { init(macKeySpec) } + val digest = mac.doFinal(cipherRawBytes) + + if (!cipherContent.mac?.fromBase64NoPadding()?.contentEquals(digest).orFalse()) { + throw SharedSecretStorageError.BadMac + } else { + // we are good + return decryptedSecret.toBase64NoPadding() + } + } + + override fun getAlgorithmsForSecret(name: String): List { + val accountData = accountDataService.getAccountDataEvent(name) + ?: return listOf(KeyInfoResult.Error(SharedSecretStorageError.UnknownSecret(name))) + val encryptedContent = accountData.content[ENCRYPTED] as? Map<*, *> + ?: return listOf(KeyInfoResult.Error(SharedSecretStorageError.SecretNotEncrypted(name))) + + val results = ArrayList() + encryptedContent.keys.forEach { + (it as? String)?.let { keyId -> + results.add(getKey(keyId)) + } + } + return results + } + + override fun getSecret(name: String, keyId: String?, secretKey: SsssKeySpec, callback: MatrixCallback) { + val accountData = accountDataService.getAccountDataEvent(name) ?: return Unit.also { + callback.onFailure(SharedSecretStorageError.UnknownSecret(name)) + } + val encryptedContent = accountData.content[ENCRYPTED] as? Map<*, *> ?: return Unit.also { + callback.onFailure(SharedSecretStorageError.SecretNotEncrypted(name)) + } + val key = keyId?.let { getKey(it) } as? KeyInfoResult.Success ?: getDefaultKey() as? KeyInfoResult.Success ?: return Unit.also { + callback.onFailure(SharedSecretStorageError.UnknownKey(name)) + } + + val encryptedForKey = encryptedContent[key.keyInfo.id] ?: return Unit.also { + callback.onFailure(SharedSecretStorageError.SecretNotEncryptedWithKey(name, key.keyInfo.id)) + } + + val secretContent = EncryptedSecretContent.fromJson(encryptedForKey) + ?: return Unit.also { + callback.onFailure(SharedSecretStorageError.ParsingError) + } + + val algorithm = key.keyInfo.content + if (SSSS_ALGORITHM_CURVE25519_AES_SHA2 == algorithm.algorithm) { + val keySpec = secretKey as? RawBytesKeySpec ?: return Unit.also { + callback.onFailure(SharedSecretStorageError.BadKeyFormat) + } + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + kotlin.runCatching { + // decrypt from recovery key + withOlmDecryption { olmPkDecryption -> + olmPkDecryption.setPrivateKey(keySpec.privateKey) + olmPkDecryption.decrypt(OlmPkMessage() + .apply { + mCipherText = secretContent.ciphertext + mEphemeralKey = secretContent.ephemeral + mMac = secretContent.mac + } + ) + } + }.foldToCallback(callback) + } + } else if (SSSS_ALGORITHM_AES_HMAC_SHA2 == algorithm.algorithm) { + val keySpec = secretKey as? RawBytesKeySpec ?: return Unit.also { + callback.onFailure(SharedSecretStorageError.BadKeyFormat) + } + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + kotlin.runCatching { + decryptAesHmacSha2(keySpec, name, secretContent) + }.foldToCallback(callback) + } + } else { + callback.onFailure(SharedSecretStorageError.UnsupportedAlgorithm(algorithm.algorithm ?: "")) + } + } + + companion object { + const val KEY_ID_BASE = "m.secret_storage.key" + const val ENCRYPTED = "encrypted" + const val DEFAULT_KEY_ID = "m.secret_storage.default_key" + } + + override fun checkShouldBeAbleToAccessSecrets(secretNames: List, keyId: String?): IntegrityResult { + if (secretNames.isEmpty()) { + return IntegrityResult.Error(SharedSecretStorageError.UnknownSecret("none")) + } + + val keyInfoResult = if (keyId == null) { + getDefaultKey() + } else { + getKey(keyId) + } + + val keyInfo = (keyInfoResult as? KeyInfoResult.Success)?.keyInfo + ?: return IntegrityResult.Error(SharedSecretStorageError.UnknownKey(keyId ?: "")) + + if (keyInfo.content.algorithm != SSSS_ALGORITHM_AES_HMAC_SHA2 + || keyInfo.content.algorithm != SSSS_ALGORITHM_CURVE25519_AES_SHA2) { + // Unsupported algorithm + return IntegrityResult.Error( + SharedSecretStorageError.UnsupportedAlgorithm(keyInfo.content.algorithm ?: "") + ) + } + + secretNames.forEach { secretName -> + val secretEvent = accountDataService.getAccountDataEvent(secretName) + ?: return IntegrityResult.Error(SharedSecretStorageError.UnknownSecret(secretName)) + if ((secretEvent.content["encrypted"] as? Map<*, *>)?.get(keyInfo.id) == null) { + return IntegrityResult.Error(SharedSecretStorageError.SecretNotEncryptedWithKey(secretName, keyInfo.id)) + } + } + + return IntegrityResult.Success(keyInfo.content.passphrase != null) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt index 3a12df2cd7..e89f4a49ed 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt @@ -21,6 +21,7 @@ import androidx.lifecycle.LiveData import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest +import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequestCommon import im.vector.matrix.android.internal.crypto.NewSessionListener import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey @@ -382,7 +383,7 @@ internal interface IMXCryptoStore { * * @param incomingRoomKeyRequest the incoming key request */ - fun deleteIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequest) + fun deleteIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequestCommon) /** * Search an IncomingRoomKeyRequest @@ -412,6 +413,8 @@ internal interface IMXCryptoStore { fun getLiveCrossSigningInfo(userId: String) : LiveData> fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?) + fun markMyMasterKeyAsLocallyTrusted(trusted: Boolean) + fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) fun getCrossSigningPrivateKeys() : PrivateKeysInfo? diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt index 00a496cae4..a93203fc21 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt @@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningIn import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.toOptional import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest +import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequestCommon import im.vector.matrix.android.internal.crypto.NewSessionListener import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel @@ -888,7 +889,7 @@ internal class RealmCryptoStore @Inject constructor( } } - override fun deleteIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequest) { + override fun deleteIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequestCommon) { doRealmTransaction(realmConfiguration) { it.where() .equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, incomingRoomKeyRequest.userId) @@ -1093,6 +1094,23 @@ internal class RealmCryptoStore @Inject constructor( } } + override fun markMyMasterKeyAsLocallyTrusted(trusted: Boolean) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where().findFirst()?.userId?.let { myUserId -> + CrossSigningInfoEntity.get(realm, myUserId)?.getMasterKey()?.let { xInfoEntity -> + val level = xInfoEntity.trustLevelEntity + if (level == null) { + val newLevel = realm.createObject(TrustLevelEntity::class.java) + newLevel.locallyVerified = trusted + xInfoEntity.trustLevelEntity = newLevel + } else { + level.locallyVerified = trusted + } + } + } + } + } + private fun addOrUpdateCrossSigningInfo(realm: Realm, userId: String, info: MXCrossSigningInfo?): CrossSigningInfoEntity? { var existing = CrossSigningInfoEntity.get(realm, userId) if (info == null) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreModule.kt index 9d7b823efb..1053cc5f43 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreModule.kt @@ -16,7 +16,18 @@ package im.vector.matrix.android.internal.crypto.store.db -import im.vector.matrix.android.internal.crypto.store.db.model.* +import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntity +import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMetadataEntity +import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntity +import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntity +import im.vector.matrix.android.internal.crypto.store.db.model.IncomingRoomKeyRequestEntity +import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity +import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity +import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity +import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity +import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingRoomKeyRequestEntity +import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntity +import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity import io.realm.annotations.RealmModule /** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/IncomingRoomKeyRequestEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/IncomingRoomKeyRequestEntity.kt index 9b1d116aa9..38cece99ac 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/IncomingRoomKeyRequestEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/IncomingRoomKeyRequestEntity.kt @@ -32,17 +32,17 @@ internal open class IncomingRoomKeyRequestEntity( ) : RealmObject() { fun toIncomingRoomKeyRequest(): IncomingRoomKeyRequest { - return IncomingRoomKeyRequest().also { - it.requestId = requestId - it.userId = userId - it.deviceId = deviceId - it.requestBody = RoomKeyRequestBody().apply { - algorithm = requestBodyAlgorithm - roomId = requestBodyRoomId - senderKey = requestBodySenderKey - sessionId = requestBodySessionId - } - } + return IncomingRoomKeyRequest( + requestId = requestId, + userId = userId, + deviceId = deviceId, + requestBody = RoomKeyRequestBody( + algorithm = requestBodyAlgorithm, + roomId = requestBodyRoomId, + senderKey = requestBodySenderKey, + sessionId = requestBodySessionId + ) + ) } fun putRequestBody(requestBody: RoomKeyRequestBody?) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingRoomKeyRequestEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingRoomKeyRequestEntity.kt index 3130bd2f89..86fc177f2b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingRoomKeyRequestEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingRoomKeyRequestEntity.kt @@ -43,12 +43,12 @@ internal open class OutgoingRoomKeyRequestEntity( fun toOutgoingRoomKeyRequest(): OutgoingRoomKeyRequest { val cancellationTxnId = this.cancellationTxnId return OutgoingRoomKeyRequest( - RoomKeyRequestBody().apply { - algorithm = requestBodyAlgorithm - roomId = requestBodyRoomId - senderKey = requestBodySenderKey - sessionId = requestBodySessionId - }, + RoomKeyRequestBody( + algorithm = requestBodyAlgorithm, + roomId = requestBodyRoomId, + senderKey = requestBodySenderKey, + sessionId = requestBodySessionId + ), getRecipients()!!, requestId!!, OutgoingRoomKeyRequest.RequestState.from(state) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DownloadKeysForUsersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DownloadKeysForUsersTask.kt index 2e11bb1b3e..94fe3c1e8d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DownloadKeysForUsersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DownloadKeysForUsersTask.kt @@ -29,7 +29,8 @@ internal interface DownloadKeysForUsersTask : Task?, // the up-to token - val token: String?) + val token: String? + ) } internal class DefaultDownloadKeysForUsers @Inject constructor( @@ -41,13 +42,10 @@ internal class DefaultDownloadKeysForUsers @Inject constructor( val downloadQuery = params.userIds?.associateWith { emptyMap() }.orEmpty() val body = KeysQueryBody( - deviceKeys = downloadQuery + deviceKeys = downloadQuery, + token = params.token?.takeIf { it.isNotEmpty() } ) - if (!params.token.isNullOrEmpty()) { - body.token = params.token - } - return executeRequest(eventBus) { apiCall = cryptoApi.downloadKeysForUsers(body) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt index d6118867ea..51c5015a1d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt @@ -18,18 +18,24 @@ package im.vector.matrix.android.internal.crypto.tasks import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.crypto.MXCryptoError -import im.vector.matrix.android.api.session.crypto.sas.VerificationService +import im.vector.matrix.android.api.session.crypto.verification.VerificationService 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.toModel -import im.vector.matrix.android.api.session.room.model.message.* +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent +import im.vector.matrix.android.api.session.room.model.message.MessageType +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationReadyContent +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationStartContent import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationService import im.vector.matrix.android.internal.di.DeviceId import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.task.Task import timber.log.Timber -import java.util.* +import java.util.ArrayList +import java.util.UUID import javax.inject.Inject internal interface RoomVerificationUpdateTask : Task { @@ -83,11 +89,14 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor( } Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}") + // Relates to is not encrypted + val relatesToEventId = event.content.toModel()?.relatesTo?.eventId + if (event.senderId == userId) { // If it's send from me, we need to keep track of Requests or Start // done from another device of mine - if (EventType.MESSAGE == event.type) { + if (EventType.MESSAGE == event.getClearType()) { val msgType = event.getClearContent().toModel()?.msgType if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) { event.getClearContent().toModel()?.let { @@ -98,26 +107,26 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor( } } } - } else if (EventType.KEY_VERIFICATION_START == event.type) { + } else if (EventType.KEY_VERIFICATION_START == event.getClearType()) { event.getClearContent().toModel()?.let { if (it.fromDevice != deviceId) { // The verification is started from another device - Timber.v("## SAS Verification live observer: Transaction started by other device tid:${it.transactionID} ") - it.transactionID?.let { txId -> transactionsHandledByOtherDevice.add(txId) } + Timber.v("## SAS Verification live observer: Transaction started by other device tid:$relatesToEventId ") + relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) } params.verificationService.onRoomRequestHandledByOtherDevice(event) } } - } else if (EventType.KEY_VERIFICATION_READY == event.type) { + } else if (EventType.KEY_VERIFICATION_READY == event.getClearType()) { event.getClearContent().toModel()?.let { if (it.fromDevice != deviceId) { // The verification is started from another device - Timber.v("## SAS Verification live observer: Transaction started by other device tid:${it.transactionID} ") - it.transactionID?.let { txId -> transactionsHandledByOtherDevice.add(txId) } + Timber.v("## SAS Verification live observer: Transaction started by other device tid:$relatesToEventId ") + relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) } params.verificationService.onRoomRequestHandledByOtherDevice(event) } } - } else if (EventType.KEY_VERIFICATION_CANCEL == event.type || EventType.KEY_VERIFICATION_DONE == event.type) { - event.getClearContent().toModel()?.relatesTo?.eventId?.let { + } else if (EventType.KEY_VERIFICATION_CANCEL == event.getClearType() || EventType.KEY_VERIFICATION_DONE == event.getClearType()) { + relatesToEventId?.let { transactionsHandledByOtherDevice.remove(it) params.verificationService.onRoomRequestHandledByOtherDevice(event) } @@ -127,10 +136,9 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor( return@forEach } - val relatesTo = event.getClearContent().toModel()?.relatesTo?.eventId - if (relatesTo != null && transactionsHandledByOtherDevice.contains(relatesTo)) { + if (relatesToEventId != null && transactionsHandledByOtherDevice.contains(relatesToEventId)) { // Ignore this event, it is directed to another of my devices - Timber.v("## SAS Verification live observer: Ignore Transaction handled by other device tid:$relatesTo ") + Timber.v("## SAS Verification live observer: Ignore Transaction handled by other device tid:$relatesToEventId ") return@forEach } when (event.getClearType()) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tools/HkdfSha256.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tools/HkdfSha256.kt new file mode 100644 index 0000000000..4a24e054ac --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tools/HkdfSha256.kt @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright (C) 2015 Square, Inc. + * + * 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.tools + +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec +import kotlin.math.ceil + +/** + * HMAC-based Extract-and-Expand Key Derivation Function (HkdfSha256) + * [RFC-5869] https://tools.ietf.org/html/rfc5869 + */ +object HkdfSha256 { + + public fun deriveSecret(inputKeyMaterial: ByteArray, salt: ByteArray?, info: ByteArray, outputLength: Int): ByteArray { + return expand(extract(salt, inputKeyMaterial), info, outputLength) + } + + /** + * HkdfSha256-Extract(salt, IKM) -> PRK + * + * @param salt optional salt value (a non-secret random value); + * if not provided, it is set to a string of HashLen (size in octets) zeros. + * @param ikm input keying material + */ + private fun extract(salt: ByteArray?, ikm: ByteArray): ByteArray { + val mac = initMac(salt ?: ByteArray(HASH_LEN) { 0.toByte() }) + return mac.doFinal(ikm) + } + + /** + * HkdfSha256-Expand(PRK, info, L) -> OKM + * + * @param prk a pseudorandom key of at least HashLen bytes (usually, the output from the extract step) + * @param info optional context and application specific information (can be empty) + * @param outputLength length of output keying material in bytes (<= 255*HashLen) + * @return OKM output keying material + */ + private fun expand(prk: ByteArray, info: ByteArray = ByteArray(0), outputLength: Int): ByteArray { + require(outputLength <= 255 * HASH_LEN) { "outputLength must be less than or equal to 255*HashLen" } + + /* + The output OKM is calculated as follows: + Notation | -> When the message is composed of several elements we use concatenation (denoted |) in the second argument; + + + N = ceil(L/HashLen) + T = T(1) | T(2) | T(3) | ... | T(N) + OKM = first L octets of T + + where: + T(0) = empty string (zero length) + T(1) = HMAC-Hash(PRK, T(0) | info | 0x01) + T(2) = HMAC-Hash(PRK, T(1) | info | 0x02) + T(3) = HMAC-Hash(PRK, T(2) | info | 0x03) + ... + */ + val n = ceil(outputLength.toDouble() / HASH_LEN.toDouble()).toInt() + + var stepHash = ByteArray(0) // T(0) empty string (zero length) + + val generatedBytes = ByteArrayOutputStream() // ByteBuffer.allocate(Math.multiplyExact(n, HASH_LEN)) + val mac = initMac(prk) + for (roundNum in 1..n) { + mac.reset() + val t = ByteBuffer.allocate(stepHash.size + info.size + 1).apply { + put(stepHash) + put(info) + put(roundNum.toByte()) + } + stepHash = mac.doFinal(t.array()) + generatedBytes.write(stepHash) + } + + return generatedBytes.toByteArray().sliceArray(0 until outputLength) + } + + private fun initMac(secret: ByteArray): Mac { + val mac = Mac.getInstance(HASH_ALG) + mac.init(SecretKeySpec(secret, HASH_ALG)) + return mac + } + + private const val HASH_LEN = 32 + private const val HASH_ALG = "HmacSHA256" +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tools/Tools.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tools/Tools.kt new file mode 100644 index 0000000000..260e6165ba --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tools/Tools.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 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.crypto.tools + +import org.matrix.olm.OlmPkDecryption +import org.matrix.olm.OlmPkEncryption + +fun withOlmEncryption(block: (OlmPkEncryption) -> T): T { + val olmPkEncryption = OlmPkEncryption() + try { + return block(olmPkEncryption) + } finally { + olmPkEncryption.releaseEncryption() + } +} + +fun withOlmDecryption(block: (OlmPkDecryption) -> T): T { + val olmPkDecryption = OlmPkDecryption() + try { + return block(olmPkDecryption) + } finally { + olmPkDecryption.releaseDecryption() + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt index efaa67d05f..81c3b96d76 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt @@ -18,10 +18,10 @@ package im.vector.matrix.android.internal.crypto.verification import android.util.Base64 import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService -import im.vector.matrix.android.api.session.crypto.sas.CancelCode -import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction -import im.vector.matrix.android.api.session.crypto.sas.SasMode -import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState +import im.vector.matrix.android.api.session.crypto.verification.CancelCode +import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction +import im.vector.matrix.android.api.session.crypto.verification.SasMode +import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt index 4befb6aff3..0ae0d8d612 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt @@ -16,9 +16,9 @@ package im.vector.matrix.android.internal.crypto.verification import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService -import im.vector.matrix.android.api.session.crypto.sas.CancelCode -import im.vector.matrix.android.api.session.crypto.sas.OutgoingSasVerificationTransaction -import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState +import im.vector.matrix.android.api.session.crypto.verification.CancelCode +import im.vector.matrix.android.api.session.crypto.verification.OutgoingSasVerificationTransaction +import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt index 00ac4a6986..15ad63a3e9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt @@ -22,14 +22,14 @@ import dagger.Lazy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService -import im.vector.matrix.android.api.session.crypto.sas.CancelCode -import im.vector.matrix.android.api.session.crypto.sas.QrCodeVerificationTransaction -import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction -import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod -import im.vector.matrix.android.api.session.crypto.sas.VerificationService -import im.vector.matrix.android.api.session.crypto.sas.VerificationTransaction -import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState -import im.vector.matrix.android.api.session.crypto.sas.safeValueOf +import im.vector.matrix.android.api.session.crypto.verification.CancelCode +import im.vector.matrix.android.api.session.crypto.verification.QrCodeVerificationTransaction +import im.vector.matrix.android.api.session.crypto.verification.SasVerificationTransaction +import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod +import im.vector.matrix.android.api.session.crypto.verification.VerificationService +import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction +import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState +import im.vector.matrix.android.api.session.crypto.verification.safeValueOf 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.LocalEcho @@ -66,7 +66,7 @@ import im.vector.matrix.android.internal.crypto.model.rest.toValue import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.verification.qrcode.DefaultQrCodeVerificationTransaction import im.vector.matrix.android.internal.crypto.verification.qrcode.QrCodeData -import im.vector.matrix.android.internal.crypto.verification.qrcode.generateSharedSecret +import im.vector.matrix.android.internal.crypto.verification.qrcode.generateSharedSecretV2 import im.vector.matrix.android.internal.di.DeviceId import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.session.SessionScope @@ -255,7 +255,7 @@ internal class DefaultVerificationService @Inject constructor( } fun onRoomRequestHandledByOtherDevice(event: Event) { - val requestInfo = event.getClearContent().toModel() + val requestInfo = event.content.toModel() ?: return val requestId = requestInfo.relatesTo?.eventId ?: return getExistingVerificationRequestInRoom(event.roomId ?: "", requestId)?.let { @@ -465,7 +465,11 @@ internal class DefaultVerificationService @Inject constructor( Timber.v("## SAS onStartRequestReceived - request accepted ${startReq.transactionID!!}") // If there is a corresponding request, we can auto accept // as we are the one requesting in first place (or we accepted the request) - val autoAccept = getExistingVerificationRequest(otherUserId)?.any { it.transactionId == startReq.transactionID } + // I need to check if the pending request was related to this device also + val autoAccept = getExistingVerificationRequest(otherUserId)?.any { + it.transactionId == startReq.transactionID + && (it.requestInfo?.fromDevice == this.deviceId || it.readyInfo?.fromDevice == this.deviceId) + } ?: false val tx = DefaultIncomingSASDefaultVerificationTransaction( // this, @@ -796,17 +800,17 @@ internal class DefaultVerificationService @Inject constructor( return when { userId != otherUserId -> - createQrCodeDataForDistinctUser(requestId, otherUserId, otherDeviceId) + createQrCodeDataForDistinctUser(requestId, otherUserId) crossSigningService.isCrossSigningVerified() -> // This is a self verification and I am the old device (Osborne2) createQrCodeDataForVerifiedDevice(requestId, otherDeviceId) else -> // This is a self verification and I am the new device (Dynabook) - createQrCodeDataForUnVerifiedDevice(requestId, otherDeviceId) + createQrCodeDataForUnVerifiedDevice(requestId) } } - private fun createQrCodeDataForDistinctUser(requestId: String, otherUserId: String, otherDeviceId: String?): QrCodeData? { + private fun createQrCodeDataForDistinctUser(requestId: String, otherUserId: String): QrCodeData.VerifyingAnotherUser? { val myMasterKey = crossSigningService.getMyCrossSigningKeys() ?.masterKey() ?.unpaddedBase64PublicKey @@ -823,39 +827,16 @@ internal class DefaultVerificationService @Inject constructor( return null } - val myDeviceId = deviceId - ?: run { - Timber.w("## Unable to get my deviceId") - return null - } - - val myDeviceKey = myDeviceInfoHolder.get().myDevice.fingerprint() - ?: run { - Timber.w("## Unable to get my fingerprint") - return null - } - - val otherDeviceKey = otherDeviceId - ?.let { - cryptoStore.getUserDevice(userId, otherDeviceId)?.fingerprint() - } - - return QrCodeData( - userId = userId, - requestId = requestId, - action = QrCodeData.ACTION_VERIFY, - keys = hashMapOf( - myMasterKey to myMasterKey, - myDeviceId to myDeviceKey - ), - sharedSecret = generateSharedSecret(), - otherUserKey = otherUserMasterKey, - otherDeviceKey = otherDeviceKey + return QrCodeData.VerifyingAnotherUser( + transactionId = requestId, + userMasterCrossSigningPublicKey = myMasterKey, + otherUserMasterCrossSigningPublicKey = otherUserMasterKey, + sharedSecret = generateSharedSecretV2() ) } // Create a QR code to display on the old device (Osborne2) - private fun createQrCodeDataForVerifiedDevice(requestId: String, otherDeviceId: String?): QrCodeData? { + private fun createQrCodeDataForVerifiedDevice(requestId: String, otherDeviceId: String?): QrCodeData.SelfVerifyingMasterKeyTrusted? { val myMasterKey = crossSigningService.getMyCrossSigningKeys() ?.masterKey() ?.unpaddedBase64PublicKey @@ -873,34 +854,16 @@ internal class DefaultVerificationService @Inject constructor( return null } - val myDeviceId = deviceId - ?: run { - Timber.w("## Unable to get my deviceId") - return null - } - - val myDeviceKey = myDeviceInfoHolder.get().myDevice.fingerprint() - ?: run { - Timber.w("## Unable to get my fingerprint") - return null - } - - return QrCodeData( - userId = userId, - requestId = requestId, - action = QrCodeData.ACTION_VERIFY, - keys = hashMapOf( - myMasterKey to myMasterKey, - myDeviceId to myDeviceKey - ), - sharedSecret = generateSharedSecret(), - otherUserKey = null, - otherDeviceKey = otherDeviceKey + return QrCodeData.SelfVerifyingMasterKeyTrusted( + transactionId = requestId, + userMasterCrossSigningPublicKey = myMasterKey, + otherDeviceKey = otherDeviceKey, + sharedSecret = generateSharedSecretV2() ) } // Create a QR code to display on the new device (Dynabook) - private fun createQrCodeDataForUnVerifiedDevice(requestId: String, otherDeviceId: String?): QrCodeData? { + private fun createQrCodeDataForUnVerifiedDevice(requestId: String): QrCodeData.SelfVerifyingMasterKeyNotTrusted? { val myMasterKey = crossSigningService.getMyCrossSigningKeys() ?.masterKey() ?.unpaddedBase64PublicKey @@ -909,34 +872,17 @@ internal class DefaultVerificationService @Inject constructor( return null } - val myDeviceId = deviceId - ?: run { - Timber.w("## Unable to get my deviceId") - return null - } - val myDeviceKey = myDeviceInfoHolder.get().myDevice.fingerprint() ?: run { Timber.w("## Unable to get my fingerprint") return null } - val otherDeviceKey = otherDeviceId - ?.let { - cryptoStore.getUserDevice(userId, otherDeviceId)?.fingerprint() - } - - return QrCodeData( - userId = userId, - requestId = requestId, - action = QrCodeData.ACTION_VERIFY, - keys = hashMapOf( - // Note: no master key here - myDeviceId to myDeviceKey - ), - sharedSecret = generateSharedSecret(), - otherUserKey = myMasterKey, - otherDeviceKey = otherDeviceKey + return QrCodeData.SelfVerifyingMasterKeyNotTrusted( + transactionId = requestId, + deviceKey = myDeviceKey, + userMasterCrossSigningPublicKey = myMasterKey, + sharedSecret = generateSharedSecretV2() ) } @@ -1141,8 +1087,12 @@ internal class DefaultVerificationService @Inject constructor( } .distinct() - transport.sendVerificationRequest(methodValues, localID, otherUserId, null, targetDevices) { _, _ -> + transport.sendVerificationRequest(methodValues, localID, otherUserId, null, targetDevices) { _, info -> // Nothing special to do in to device mode + updatePendingRequest(verificationRequest.copy( + // localId stays different + requestInfo = info + )) } requestsForUser.add(verificationRequest) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationTransaction.kt index 6f06b93b88..93e0b9f2b9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationTransaction.kt @@ -15,7 +15,7 @@ */ package im.vector.matrix.android.internal.crypto.verification -import im.vector.matrix.android.api.session.crypto.sas.VerificationTransaction +import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction /** * Generic interactive key verification transaction diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt index fe5f9dadb9..4e85fb936e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt @@ -16,7 +16,7 @@ package im.vector.matrix.android.internal.crypto.verification import im.vector.matrix.android.api.extensions.orFalse -import im.vector.matrix.android.api.session.crypto.sas.CancelCode +import im.vector.matrix.android.api.session.crypto.verification.CancelCode import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SCAN import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SHOW import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_SAS diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASDefaultVerificationTransaction.kt index 22f543f267..7856e571eb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASDefaultVerificationTransaction.kt @@ -18,11 +18,11 @@ package im.vector.matrix.android.internal.crypto.verification import android.os.Build import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService -import im.vector.matrix.android.api.session.crypto.sas.CancelCode -import im.vector.matrix.android.api.session.crypto.sas.EmojiRepresentation -import im.vector.matrix.android.api.session.crypto.sas.SasMode -import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction -import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState +import im.vector.matrix.android.api.session.crypto.verification.CancelCode +import im.vector.matrix.android.api.session.crypto.verification.EmojiRepresentation +import im.vector.matrix.android.api.session.crypto.verification.SasMode +import im.vector.matrix.android.api.session.crypto.verification.SasVerificationTransaction +import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel @@ -312,7 +312,7 @@ internal abstract class SASDefaultVerificationTransaction( if (otherUserId == userId) { // If me it's reasonable to sign and upload the device signature // Notice that i might not have the private keys, so may not be able to do it - crossSigningService.signDevice(otherDeviceId!!, object : MatrixCallback { + crossSigningService.trustDevice(otherDeviceId!!, object : MatrixCallback { override fun onFailure(failure: Throwable) { Timber.w(failure, "## SAS Verification: Failed to sign new device $otherDeviceId") } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationEmoji.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationEmoji.kt index 0e814083dd..eb9acd045a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationEmoji.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationEmoji.kt @@ -16,7 +16,7 @@ package im.vector.matrix.android.internal.crypto.verification import im.vector.matrix.android.R -import im.vector.matrix.android.api.session.crypto.sas.EmojiRepresentation +import im.vector.matrix.android.api.session.crypto.verification.EmojiRepresentation internal fun getEmojiForCode(code: Int): EmojiRepresentation { return when (code % 64) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransport.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransport.kt index ee0e66959d..1c60dfb768 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransport.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransport.kt @@ -15,8 +15,8 @@ */ package im.vector.matrix.android.internal.crypto.verification -import im.vector.matrix.android.api.session.crypto.sas.CancelCode -import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState +import im.vector.matrix.android.api.session.crypto.verification.CancelCode +import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState /** * Verification can be performed using toDevice events or via DM. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt index 11093ca3ba..ef7b7c5ae1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt @@ -23,8 +23,8 @@ import androidx.work.Operation import androidx.work.WorkInfo import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.R -import im.vector.matrix.android.api.session.crypto.sas.CancelCode -import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState +import im.vector.matrix.android.api.session.crypto.verification.CancelCode +import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState 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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportToDevice.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportToDevice.kt index 1dae8fba68..a0bddfb3ad 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportToDevice.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportToDevice.kt @@ -16,8 +16,8 @@ package im.vector.matrix.android.internal.crypto.verification import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.session.crypto.sas.CancelCode -import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState +import im.vector.matrix.android.api.session.crypto.verification.CancelCode +import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt index d1b72f54c6..9c2a40a4e8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt @@ -18,9 +18,9 @@ package im.vector.matrix.android.internal.crypto.verification.qrcode import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService -import im.vector.matrix.android.api.session.crypto.sas.CancelCode -import im.vector.matrix.android.api.session.crypto.sas.QrCodeVerificationTransaction -import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState +import im.vector.matrix.android.api.session.crypto.verification.CancelCode +import im.vector.matrix.android.api.session.crypto.verification.QrCodeVerificationTransaction +import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel @@ -28,7 +28,7 @@ import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationTransaction import im.vector.matrix.android.internal.crypto.verification.VerificationInfo import im.vector.matrix.android.internal.crypto.verification.VerificationInfoStart -import im.vector.matrix.android.internal.util.withoutPrefix +import im.vector.matrix.android.internal.util.exhaustive import timber.log.Timber internal class DefaultQrCodeVerificationTransaction( @@ -46,7 +46,7 @@ internal class DefaultQrCodeVerificationTransaction( ) : DefaultVerificationTransaction(transactionId, otherUserId, otherDeviceId, isIncoming), QrCodeVerificationTransaction { override val qrCodeText: String? - get() = qrCodeData?.toUrl() + get() = qrCodeData?.toEncodedString() override var state: VerificationTxState = VerificationTxState.None set(newState) { @@ -69,89 +69,76 @@ internal class DefaultQrCodeVerificationTransaction( } // Perform some checks - if (otherQrCodeData.action != QrCodeData.ACTION_VERIFY) { - Timber.d("## Verification QR: Invalid action ${otherQrCodeData.action}") - cancel(CancelCode.QrCodeInvalid) - return - } - - if (otherQrCodeData.userId != otherUserId) { - Timber.d("## Verification QR: Mismatched user ${otherQrCodeData.userId}") - cancel(CancelCode.MismatchedUser) - return - } - - if (otherQrCodeData.requestId != transactionId) { - Timber.d("## Verification QR: Invalid transaction actual ${otherQrCodeData.requestId} expected:$transactionId") + if (otherQrCodeData.transactionId != transactionId) { + Timber.d("## Verification QR: Invalid transaction actual ${otherQrCodeData.transactionId} expected:$transactionId") cancel(CancelCode.QrCodeInvalid) return } // check master key - if (otherQrCodeData.userId != userId - && otherQrCodeData.otherUserKey == null) { - // Verification with other user, other_user_key is mandatory in this case - Timber.d("## Verification QR: Invalid, missing other_user_key") - cancel(CancelCode.QrCodeInvalid) - return - } - - if (otherQrCodeData.otherUserKey != null - && otherQrCodeData.otherUserKey != crossSigningService.getUserCrossSigningKeys(userId)?.masterKey()?.unpaddedBase64PublicKey) { - Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.otherUserKey}") - cancel(CancelCode.MismatchedKeys) - return - } - - // Check device key if available - if (otherQrCodeData.otherDeviceKey != null - && otherQrCodeData.otherDeviceKey != cryptoStore.getUserDevice(userId, deviceId)?.fingerprint()) { - Timber.d("## Verification QR: Invalid other device key") - cancel(CancelCode.MismatchedKeys) - return - } + when (otherQrCodeData) { + is QrCodeData.VerifyingAnotherUser -> { + if (otherQrCodeData.otherUserMasterCrossSigningPublicKey + != crossSigningService.getUserCrossSigningKeys(userId)?.masterKey()?.unpaddedBase64PublicKey) { + Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.otherUserMasterCrossSigningPublicKey}") + cancel(CancelCode.MismatchedKeys) + return + } else Unit + } + is QrCodeData.SelfVerifyingMasterKeyTrusted -> { + if (otherQrCodeData.userMasterCrossSigningPublicKey + != crossSigningService.getUserCrossSigningKeys(userId)?.masterKey()?.unpaddedBase64PublicKey) { + Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.userMasterCrossSigningPublicKey}") + cancel(CancelCode.MismatchedKeys) + return + } else Unit + } + is QrCodeData.SelfVerifyingMasterKeyNotTrusted -> { + if (otherQrCodeData.userMasterCrossSigningPublicKey + != crossSigningService.getUserCrossSigningKeys(userId)?.masterKey()?.unpaddedBase64PublicKey) { + Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.userMasterCrossSigningPublicKey}") + cancel(CancelCode.MismatchedKeys) + return + } else Unit + } + }.exhaustive val toVerifyDeviceIds = mutableListOf() var canTrustOtherUserMasterKey = false - val otherDevices = cryptoStore.getUserDevices(otherUserId) - otherQrCodeData.keys.keys.forEach { key -> - Timber.w("## Verification QR: Checking key $key") - - when (val keyNoPrefix = key.withoutPrefix("ed25519:")) { - otherQrCodeData.keys[key] -> { - // Maybe master key? - if (otherQrCodeData.keys[key] == crossSigningService.getUserCrossSigningKeys(otherUserId)?.masterKey()?.unpaddedBase64PublicKey) { - canTrustOtherUserMasterKey = true - } else { - cancel(CancelCode.MismatchedKeys) - return - } - } - else -> { - when (val otherDevice = otherDevices?.get(keyNoPrefix)) { - null -> { - // Unknown device, ignore - } - else -> { - when (otherDevice.fingerprint()) { - null -> { - // Ignore - } - otherQrCodeData.keys[key] -> { - // Store the deviceId to verify after - toVerifyDeviceIds.add(key) - } - else -> { - cancel(CancelCode.MismatchedKeys) - return - } - } - } - } + // Check device key if available + when (otherQrCodeData) { + is QrCodeData.VerifyingAnotherUser -> { + if (otherQrCodeData.userMasterCrossSigningPublicKey + != crossSigningService.getUserCrossSigningKeys(otherUserId)?.masterKey()?.unpaddedBase64PublicKey) { + Timber.d("## Verification QR: Invalid user master key ${otherQrCodeData.userMasterCrossSigningPublicKey}") + cancel(CancelCode.MismatchedKeys) + return + } else { + canTrustOtherUserMasterKey = true + Unit } } - } + is QrCodeData.SelfVerifyingMasterKeyTrusted -> { + if (otherQrCodeData.otherDeviceKey + != cryptoStore.getUserDevice(userId, deviceId)?.fingerprint()) { + Timber.d("## Verification QR: Invalid other device key ${otherQrCodeData.otherDeviceKey}") + cancel(CancelCode.MismatchedKeys) + return + } else Unit + } + is QrCodeData.SelfVerifyingMasterKeyNotTrusted -> { + if (otherQrCodeData.deviceKey + != cryptoStore.getUserDevice(otherUserId, otherDeviceId ?: "")?.fingerprint()) { + Timber.d("## Verification QR: Invalid device key ${otherQrCodeData.deviceKey}") + cancel(CancelCode.MismatchedKeys) + return + } else { + toVerifyDeviceIds.add(otherQrCodeData.deviceKey) + Unit + } + } + }.exhaustive if (!canTrustOtherUserMasterKey && toVerifyDeviceIds.isEmpty()) { // Nothing to verify @@ -164,13 +151,6 @@ internal class DefaultQrCodeVerificationTransaction( // qrCodeData.sharedSecret will be used to send the start request start(otherQrCodeData.sharedSecret) - val safeOtherDeviceId = otherDeviceId - if (!otherQrCodeData.otherDeviceKey.isNullOrBlank() - && safeOtherDeviceId != null) { - // Locally verify the device - toVerifyDeviceIds.add(safeOtherDeviceId) - } - // Trust the other user trust(canTrustOtherUserMasterKey, toVerifyDeviceIds.distinct()) } @@ -242,20 +222,25 @@ internal class DefaultQrCodeVerificationTransaction( private fun trust(canTrustOtherUserMasterKey: Boolean, toVerifyDeviceIds: List) { // If not me sign his MSK and upload the signature - if (otherUserId != userId && canTrustOtherUserMasterKey) { - // we should trust this master key - // And check verification MSK -> SSK? - crossSigningService.trustUser(otherUserId, object : MatrixCallback { - override fun onFailure(failure: Throwable) { - Timber.e(failure, "## QR Verification: Failed to trust User $otherUserId") - } - }) + if (canTrustOtherUserMasterKey) { + if (otherUserId != userId) { + // we should trust this master key + // And check verification MSK -> SSK? + crossSigningService.trustUser(otherUserId, object : MatrixCallback { + override fun onFailure(failure: Throwable) { + Timber.e(failure, "## QR Verification: Failed to trust User $otherUserId") + } + }) + } else { + // Mark my keys as trusted locally + crossSigningService.markMyMasterKeyAsTrusted() + } } if (otherUserId == userId) { // If me it's reasonable to sign and upload the device signature // Notice that i might not have the private keys, so may not be able to do it - crossSigningService.signDevice(otherDeviceId!!, object : MatrixCallback { + crossSigningService.trustDevice(otherDeviceId!!, object : MatrixCallback { override fun onFailure(failure: Throwable) { Timber.w(failure, "## QR Verification: Failed to sign new device $otherDeviceId") } @@ -264,8 +249,8 @@ internal class DefaultQrCodeVerificationTransaction( // TODO what if the otherDevice is not in this list? and should we toVerifyDeviceIds.forEach { - setDeviceVerified(otherUserId, it) - } + setDeviceVerified(otherUserId, it) + } transport.done(transactionId) state = VerificationTxState.Verified } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/Extensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/Extensions.kt index d539152135..da926a0e10 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/Extensions.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/Extensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 New Vector Ltd + * Copyright (c) 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. @@ -16,116 +16,112 @@ package im.vector.matrix.android.internal.crypto.verification.qrcode -import im.vector.matrix.android.api.MatrixPatterns -import im.vector.matrix.android.api.permalinks.PermalinkFactory -import java.net.URLDecoder -import java.net.URLEncoder +import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64NoPadding +import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding +import im.vector.matrix.android.internal.extensions.toUnsignedInt -private const val ENCODING = "utf-8" +// MATRIX +private val prefix = "MATRIX".toByteArray(Charsets.ISO_8859_1) -/** - * Generate an URL to generate a QR code of the form: - *
- * https://matrix.to/#/?
- *     request=
- *     &action=verify
- *     &key_=...
- *     &secret=
- *     &other_user_key=
- *     &other_device_key=
- *
- * Example:
- * https://matrix.to/#/@user:matrix.org?
- *     request=%24pBeIfm7REDACTEDSQJbgqvi-yYiwmPB8_H_W_O974
- *     &action=verify
- *     &key_VJEDVKUYTQ=DL7LWIw7Qp%2B4AREDACTEDOwy2BjygumSWAGfzaWY
- *     &key_fsh%2FfQ08N3xvh4ySXsINB%2BJ2hREDACTEDVcVOG4qqo=fsh%2FfQ08N3xvh4ySXsINB%2BJ2hREDACTEDVcVOG4qqo
- *     &secret=AjQqw51Fp6UBuPolZ2FAD5WnXc22ZhJG6iGslrVvIdw%3D
- *     &other_user_key=WqSVLkBCS%2Fi5NqRREDACTEDRPxBIuqK8Usl6Y3big
- *     &other_device_key=WqSVLkBREDACTEDBsfszdvsdBEvefqsdcsfBvsfcsFb
- * 
- */ -fun QrCodeData.toUrl(): String { - return buildString { - append(PermalinkFactory.createPermalink(userId)) - append("?request=") - append(URLEncoder.encode(requestId, ENCODING)) - append("&action=") - append(URLEncoder.encode(action, ENCODING)) +fun QrCodeData.toEncodedString(): String { + var result = ByteArray(0) - for ((keyId, key) in keys) { - append("&key_${URLEncoder.encode(keyId, ENCODING)}=") - append(URLEncoder.encode(key, ENCODING)) - } - - append("&secret=") - append(URLEncoder.encode(sharedSecret, ENCODING)) - - if (!otherUserKey.isNullOrBlank()) { - append("&other_user_key=") - append(URLEncoder.encode(otherUserKey, ENCODING)) - } - if (!otherDeviceKey.isNullOrBlank()) { - append("&other_device_key=") - append(URLEncoder.encode(otherDeviceKey, ENCODING)) - } + // MATRIX + for (i in prefix.indices) { + result += prefix[i] } + + // Version + result += 2 + + // Mode + result += when (this) { + is QrCodeData.VerifyingAnotherUser -> 0 + is QrCodeData.SelfVerifyingMasterKeyTrusted -> 1 + is QrCodeData.SelfVerifyingMasterKeyNotTrusted -> 2 + }.toByte() + + // TransactionId length + val length = transactionId.length + result += ((length and 0xFF00) shr 8).toByte() + result += length.toByte() + + // TransactionId + transactionId.forEach { + result += it.toByte() + } + + // Keys + firstKey.fromBase64NoPadding().forEach { + result += it + } + secondKey.fromBase64NoPadding().forEach { + result += it + } + + // Secret + sharedSecret.fromBase64NoPadding().forEach { + result += it + } + + return result.toString(Charsets.ISO_8859_1) } fun String.toQrCodeData(): QrCodeData? { - if (!startsWith(PermalinkFactory.MATRIX_TO_URL_BASE)) { + val byteArray = toByteArray(Charsets.ISO_8859_1) + + // Size should be min 6 + 1 + 1 + 2 + ? + 32 + 32 + ? = 74 + transactionLength + secretLength + + // Check header + // MATRIX + if (byteArray.size < 10) return null + + for (i in prefix.indices) { + if (byteArray[i] != prefix[i]) { + return null + } + } + + var cursor = prefix.size // 6 + + // Version + if (byteArray[cursor] != 2.toByte()) { + return null + } + cursor++ + + // Get mode + val mode = byteArray[cursor].toInt() + cursor++ + + // Get transaction length + val bigEndian1 = byteArray[cursor].toUnsignedInt() + val bigEndian2 = byteArray[cursor + 1].toUnsignedInt() + + val transactionLength = bigEndian1 * 0x0100 + bigEndian2 + + cursor++ + cursor++ + + val secretLength = byteArray.size - 74 - transactionLength + + // ensure the secret length is 8 bytes min + if (secretLength < 8) { return null } - val fragment = substringAfter("#") - if (fragment.isEmpty()) { - return null + val transactionId = byteArray.copyOfRange(cursor, cursor + transactionLength).toString(Charsets.ISO_8859_1) + cursor += transactionLength + val key1 = byteArray.copyOfRange(cursor, cursor + 32).toBase64NoPadding() + cursor += 32 + val key2 = byteArray.copyOfRange(cursor, cursor + 32).toBase64NoPadding() + cursor += 32 + val secret = byteArray.copyOfRange(cursor, byteArray.size).toBase64NoPadding() + + return when (mode) { + 0 -> QrCodeData.VerifyingAnotherUser(transactionId, key1, key2, secret) + 1 -> QrCodeData.SelfVerifyingMasterKeyTrusted(transactionId, key1, key2, secret) + 2 -> QrCodeData.SelfVerifyingMasterKeyNotTrusted(transactionId, key1, key2, secret) + else -> null } - - val safeFragment = fragment.substringBefore("?") - - // we are limiting to 2 params - val params = safeFragment - .split(MatrixPatterns.SEP_REGEX.toRegex()) - .filter { it.isNotEmpty() } - - if (params.size != 1) { - return null - } - - val userId = params.getOrNull(0) - ?.let { PermalinkFactory.unescape(it) } - ?.takeIf { MatrixPatterns.isUserId(it) } ?: return null - - val urlParams = fragment.substringAfter("?") - .split("&".toRegex()) - .filter { it.isNotEmpty() } - - val keyValues = urlParams.map { - (it.substringBefore("=") to it.substringAfter("=").let { value -> URLDecoder.decode(value, ENCODING) }) - }.toMap() - - val action = keyValues["action"]?.takeIf { it.isNotBlank() } ?: return null - - val requestEventId = keyValues["request"]?.takeIf { it.isNotBlank() } ?: return null - val sharedSecret = keyValues["secret"]?.takeIf { it.isNotBlank() } ?: return null - val otherUserKey = keyValues["other_user_key"] - val otherDeviceKey = keyValues["other_device_key"] - - val keys = keyValues.keys - .filter { it.startsWith("key_") } - .map { - URLDecoder.decode(it.substringAfter("key_"), ENCODING) to (keyValues[it] ?: return null) - } - .toMap() - - return QrCodeData( - userId, - requestEventId, - action, - keys, - sharedSecret, - otherUserKey, - otherDeviceKey - ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeData.kt index 0f9a31ab32..934c0c82b9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeData.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 New Vector Ltd + * Copyright (c) 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. @@ -19,27 +19,84 @@ package im.vector.matrix.android.internal.crypto.verification.qrcode /** * Ref: https://github.com/uhoreg/matrix-doc/blob/qr_key_verification/proposals/1543-qr_code_key_verification.md#qr-code-format */ -data class QrCodeData( - val userId: String, - // Request Id. Can be an arbitrary value. In DM, it will be the event ID of the associated verification request event. - val requestId: String, - // The action - val action: String, - // key_: each key that the user wants verified will have an entry of this form, where the value is the key in unpadded base64. - // The QR code should contain at least the user's master cross-signing key. In the case where a device does not have a cross-signing key - // (as in the case where a user logs in to a new device, and is verifying against another device), thin the QR code should contain at - // least the device's key. - val keys: Map, - // random single-use shared secret in unpadded base64. It must be at least 256-bits long (43 characters when base64-encoded). - val sharedSecret: String, - // the other user's master cross-signing key, in unpadded base64. In other words, if Alice is displaying the QR code, - // this would be the copy of Bob's master cross-signing key that Alice has. - val otherUserKey: String?, - // The other device's key, in unpadded base64 - // This is only needed when a user is verifying their own devices, where the other device has not yet been signed with the cross-signing key. - val otherDeviceKey: String? +sealed class QrCodeData( + /** + * the event ID or transaction_id of the associated verification + */ + open val transactionId: String, + /** + * First key (32 bytes, in base64 no padding) + */ + val firstKey: String, + /** + * Second key (32 bytes, in base64 no padding) + */ + val secondKey: String, + /** + * a random shared secret (in base64 no padding) + */ + open val sharedSecret: String ) { - companion object { - const val ACTION_VERIFY = "verify" - } + /** + * verifying another user with cross-signing + * QR code verification mode: 0x00 + */ + data class VerifyingAnotherUser( + override val transactionId: String, + /** + * the user's own master cross-signing public key + */ + val userMasterCrossSigningPublicKey: String, + /** + * what the device thinks the other user's master cross-signing key is + */ + val otherUserMasterCrossSigningPublicKey: String, + override val sharedSecret: String + ) : QrCodeData( + transactionId, + userMasterCrossSigningPublicKey, + otherUserMasterCrossSigningPublicKey, + sharedSecret) + + /** + * self-verifying in which the current device does trust the master key + * QR code verification mode: 0x01 + */ + data class SelfVerifyingMasterKeyTrusted( + override val transactionId: String, + /** + * the user's own master cross-signing public key + */ + val userMasterCrossSigningPublicKey: String, + /** + * what the device thinks the other device's device key is + */ + val otherDeviceKey: String, + override val sharedSecret: String + ) : QrCodeData( + transactionId, + userMasterCrossSigningPublicKey, + otherDeviceKey, + sharedSecret) + + /** + * self-verifying in which the current device does not yet trust the master key + * QR code verification mode: 0x02 + */ + data class SelfVerifyingMasterKeyNotTrusted( + override val transactionId: String, + /** + * the current device's device key + */ + val deviceKey: String, + /** + * what the device thinks the user's master cross-signing key is + */ + val userMasterCrossSigningPublicKey: String, + override val sharedSecret: String + ) : QrCodeData( + transactionId, + deviceKey, + userMasterCrossSigningPublicKey, + sharedSecret) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/SharedSecret.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/SharedSecret.kt index d319ebd88c..397cad0c53 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/SharedSecret.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/SharedSecret.kt @@ -19,11 +19,11 @@ package im.vector.matrix.android.internal.crypto.verification.qrcode import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding import java.security.SecureRandom -fun generateSharedSecret(): String { +fun generateSharedSecretV2(): String { val secureRandom = SecureRandom() - // 256 bits long - val secretBytes = ByteArray(32) + // 8 bytes long + val secretBytes = ByteArray(8) secureRandom.nextBytes(secretBytes) return secretBytes.toBase64NoPadding() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt index ee6d8e507c..0312e6d4f0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt @@ -18,7 +18,11 @@ package im.vector.matrix.android.internal.database import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.internal.util.createBackgroundHandler -import io.realm.* +import io.realm.OrderedRealmCollectionChangeListener +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.RealmObject +import io.realm.RealmResults import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancelChildren diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt index 98544d46f7..446ecc32d1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt @@ -16,8 +16,16 @@ package im.vector.matrix.android.internal.database -import io.realm.* -import kotlinx.coroutines.* +import io.realm.Realm +import io.realm.RealmChangeListener +import io.realm.RealmConfiguration +import io.realm.RealmQuery +import io.realm.RealmResults +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout internal suspend fun awaitNotEmptyResult(realmConfiguration: RealmConfiguration, timeoutMillis: Long, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt index 75eb61274d..e6a082c720 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt @@ -32,7 +32,8 @@ internal object EventMapper { val uds = if (event.unsignedData == null) null else MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(event.unsignedData) val eventEntity = EventEntity() - eventEntity.eventId = event.eventId ?: "" + // TODO change this as we shouldn't use event everywhere + eventEntity.eventId = event.eventId ?: "$$roomId-${System.currentTimeMillis()}-${event.hashCode()}" eventEntity.roomId = event.roomId ?: roomId eventEntity.content = ContentMapper.map(event.content) val resolvedPrevContent = event.prevContent ?: event.unsignedData?.prevContent 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 617dc6acfa..2f3cdb9545 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 @@ -16,19 +16,12 @@ package im.vector.matrix.android.internal.database.mapper -import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.tag.RoomTag -import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult import im.vector.matrix.android.internal.database.model.RoomSummaryEntity -import timber.log.Timber -import java.util.UUID import javax.inject.Inject -internal class RoomSummaryMapper @Inject constructor( - private val cryptoService: CryptoService, - private val timelineEventMapper: TimelineEventMapper -) { +internal class RoomSummaryMapper @Inject constructor(private val timelineEventMapper: TimelineEventMapper) { fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary { val tags = roomSummaryEntity.tags.map { @@ -38,21 +31,6 @@ internal class RoomSummaryMapper @Inject constructor( val latestEvent = roomSummaryEntity.latestPreviewableEvent?.let { timelineEventMapper.map(it, buildReadReceipts = false) } - if (latestEvent?.root?.isEncrypted() == true && latestEvent.root.mxDecryptionResult == null) { - // TODO use a global event decryptor? attache to session and that listen to new sessionId? - // for now decrypt sync - try { - val result = cryptoService.decryptEvent(latestEvent.root, latestEvent.root.roomId + UUID.randomUUID().toString()) - latestEvent.root.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain - ) - } catch (e: Throwable) { - Timber.d(e) - } - } return RoomSummary( roomId = roomSummaryEntity.roomId, @@ -77,7 +55,8 @@ internal class RoomSummaryMapper @Inject constructor( isEncrypted = roomSummaryEntity.isEncrypted, typingRoomMemberIds = roomSummaryEntity.typingUserIds.toList(), breadcrumbsIndex = roomSummaryEntity.breadcrumbsIndex, - roomEncryptionTrustLevel = roomSummaryEntity.roomEncryptionTrustLevel + roomEncryptionTrustLevel = roomSummaryEntity.roomEncryptionTrustLevel, + inviterId = roomSummaryEntity.inviterId ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt index d7bb49b1fe..72015afc43 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt @@ -22,9 +22,8 @@ import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResu import im.vector.matrix.android.internal.di.MoshiProvider import io.realm.RealmObject import io.realm.annotations.Index -import io.realm.annotations.PrimaryKey -internal open class EventEntity(@PrimaryKey var eventId: String = "", +internal open class EventEntity(@Index var eventId: String = "", @Index var roomId: String = "", @Index var type: String = "", var content: String? = null, 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 c4ebe3cbaa..7009e762fb 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 @@ -49,7 +49,8 @@ internal open class RoomSummaryEntity( var flatAliases: String = "", var isEncrypted: Boolean = false, var typingUserIds: RealmList = RealmList(), - var roomEncryptionTrustLevelStr: String? = null + var roomEncryptionTrustLevelStr: String? = null, + var inviterId: String? = null ) : 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 74768f8797..081a6a5152 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 @@ -53,6 +53,7 @@ import io.realm.annotations.RealmModule DraftEntity::class, HomeServerCapabilitiesEntity::class, RoomMemberSummaryEntity::class, - CurrentStateEventEntity::class + CurrentStateEventEntity::class, + UserAccountDataEntity::class ]) internal class SessionRealmModule diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/UserAccountDataEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/UserAccountDataEntity.kt new file mode 100644 index 0000000000..90f73381dc --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/UserAccountDataEntity.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 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.database.model + +import io.realm.RealmObject +import io.realm.annotations.Index + +/** + * Clients can store custom config data for their account on their HomeServer. + * This account data will be synced between different devices and can persist across installations on a particular device. + * Users may only view the account data for their own account. + * The account_data may be either global or scoped to a particular rooms. + */ +internal open class UserAccountDataEntity( + @Index var type: String? = null, + var contentStr: String? = null +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt index 59908aa990..d998c41ccb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt @@ -23,6 +23,13 @@ import io.realm.RealmList import io.realm.RealmQuery import io.realm.kotlin.where +internal fun EventEntity.copyToRealmOrIgnore(realm: Realm): EventEntity { + return realm.where() + .equalTo(EventEntityFields.EVENT_ID, eventId) + .equalTo(EventEntityFields.ROOM_ID, roomId) + .findFirst() ?: realm.copyToRealm(this) +} + internal fun EventEntity.Companion.where(realm: Realm, eventId: String): RealmQuery { return realm.where() .equalTo(EventEntityFields.EVENT_ID, eventId) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/PushersQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/PushersQueries.kt index 42e7770114..cdec6b590e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/PushersQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/PushersQueries.kt @@ -16,10 +16,12 @@ package im.vector.matrix.android.internal.database.query import im.vector.matrix.android.api.pushrules.RuleKind -import im.vector.matrix.android.internal.database.model.* import im.vector.matrix.android.internal.database.model.PushRuleEntity +import im.vector.matrix.android.internal.database.model.PushRuleEntityFields import im.vector.matrix.android.internal.database.model.PushRulesEntity +import im.vector.matrix.android.internal.database.model.PushRulesEntityFields import im.vector.matrix.android.internal.database.model.PusherEntity +import im.vector.matrix.android.internal.database.model.PusherEntityFields import io.realm.Realm import io.realm.RealmQuery import io.realm.kotlin.where diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt index c19c686329..3164bf75c0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt @@ -17,13 +17,27 @@ package im.vector.matrix.android.internal.di import com.squareup.moshi.Moshi -import im.vector.matrix.android.api.session.room.model.message.* +import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageDefaultContent +import im.vector.matrix.android.api.session.room.model.message.MessageEmoteContent +import im.vector.matrix.android.api.session.room.model.message.MessageFileContent +import im.vector.matrix.android.api.session.room.model.message.MessageImageContent +import im.vector.matrix.android.api.session.room.model.message.MessageLocationContent +import im.vector.matrix.android.api.session.room.model.message.MessageNoticeContent +import im.vector.matrix.android.api.session.room.model.message.MessageOptionsContent +import im.vector.matrix.android.api.session.room.model.message.MessagePollResponseContent +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.api.session.room.model.message.MessageVerificationRequestContent +import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent +import im.vector.matrix.android.internal.network.parsing.ForceToBooleanJsonAdapter import im.vector.matrix.android.internal.network.parsing.RuntimeJsonAdapterFactory import im.vector.matrix.android.internal.network.parsing.UriMoshiAdapter import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataBreadcrumbs import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataDirectMessages -import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataFallback +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataIgnoredUsers import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataPushRules @@ -31,7 +45,8 @@ object MoshiProvider { private val moshi: Moshi = Moshi.Builder() .add(UriMoshiAdapter()) - .add(RuntimeJsonAdapterFactory.of(UserAccountData::class.java, "type", UserAccountDataFallback::class.java) + .add(ForceToBooleanJsonAdapter()) + .add(RuntimeJsonAdapterFactory.of(UserAccountData::class.java, "type", UserAccountDataEvent::class.java) .registerSubtype(UserAccountDataDirectMessages::class.java, UserAccountData.TYPE_DIRECT_MESSAGES) .registerSubtype(UserAccountDataIgnoredUsers::class.java, UserAccountData.TYPE_IGNORED_USER_LIST) .registerSubtype(UserAccountDataPushRules::class.java, UserAccountData.TYPE_PUSH_RULES) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/WorkManagerProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/WorkManagerProvider.kt index 82091be697..5a0202719b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/WorkManagerProvider.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/WorkManagerProvider.kt @@ -17,7 +17,11 @@ package im.vector.matrix.android.internal.di import android.content.Context -import androidx.work.* +import androidx.work.Constraints +import androidx.work.ListenableWorker +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager import javax.inject.Inject internal class WorkManagerProvider @Inject constructor( @@ -54,5 +58,7 @@ internal class WorkManagerProvider @Inject constructor( val workConstraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() + + const val BACKOFF_DELAY = 10_000L } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/extensions/Try.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/extensions/Try.kt index 6a23ca9094..8918362ff5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/extensions/Try.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/extensions/Try.kt @@ -16,7 +16,11 @@ package im.vector.matrix.android.internal.extensions -import arrow.core.* +import arrow.core.Failure +import arrow.core.Success +import arrow.core.Try +import arrow.core.TryOf +import arrow.core.fix import im.vector.matrix.android.api.MatrixCallback inline fun TryOf.onError(f: (Throwable) -> Unit): Try = fix() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkCallbackStrategy.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkCallbackStrategy.kt index 83df3c0a4f..a639c61678 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkCallbackStrategy.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkCallbackStrategy.kt @@ -22,6 +22,7 @@ import android.content.IntentFilter import android.net.ConnectivityManager import android.net.Network import android.os.Build +import timber.log.Timber import javax.inject.Inject internal interface NetworkCallbackStrategy { @@ -70,7 +71,16 @@ internal class PreferredNetworkCallbackStrategy @Inject constructor(context: Con } override fun unregister() { + // It can crash after an application update, if not registered + val doUnregister = hasChangedCallback != null hasChangedCallback = null - conn.unregisterNetworkCallback(networkCallback) + if (doUnregister) { + // Add a try catch for safety + try { + conn.unregisterNetworkCallback(networkCallback) + } catch (t: Throwable) { + Timber.e(t, "Unable to unregister network callback") + } + } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ProgressRequestBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ProgressRequestBody.kt index 6e371f7a5c..b02af88270 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ProgressRequestBody.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ProgressRequestBody.kt @@ -18,7 +18,11 @@ package im.vector.matrix.android.internal.network import okhttp3.MediaType import okhttp3.RequestBody -import okio.* +import okio.Buffer +import okio.BufferedSink +import okio.ForwardingSink +import okio.Sink +import okio.buffer import java.io.IOException internal class ProgressRequestBody(private val delegate: RequestBody, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/parsing/ForceToBoolean.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/parsing/ForceToBoolean.kt new file mode 100644 index 0000000000..b823104b4d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/parsing/ForceToBoolean.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 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.network.parsing + +import com.squareup.moshi.FromJson +import com.squareup.moshi.JsonQualifier +import com.squareup.moshi.JsonReader +import com.squareup.moshi.ToJson +import timber.log.Timber + +@JsonQualifier +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.FUNCTION) +annotation class ForceToBoolean + +internal class ForceToBooleanJsonAdapter { + @ToJson + fun toJson(@ForceToBoolean b: Boolean): Boolean { + return b + } + + @FromJson + @ForceToBoolean + fun fromJson(reader: JsonReader): Boolean { + return when (val token = reader.peek()) { + JsonReader.Token.NUMBER -> reader.nextInt() != 0 + JsonReader.Token.BOOLEAN -> reader.nextBoolean() + else -> { + Timber.e("Expecting a boolean or a int but get: $token") + reader.skipValue() + false + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ssl/CertUtil.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ssl/CertUtil.kt index 501fed0bd4..b304791b1c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ssl/CertUtil.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ssl/CertUtil.kt @@ -24,7 +24,14 @@ import java.security.KeyStore import java.security.MessageDigest import java.security.cert.CertificateException import java.security.cert.X509Certificate -import javax.net.ssl.* +import javax.net.ssl.HostnameVerifier +import javax.net.ssl.HttpsURLConnection +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLPeerUnverifiedException +import javax.net.ssl.SSLSocketFactory +import javax.net.ssl.TrustManager +import javax.net.ssl.TrustManagerFactory +import javax.net.ssl.X509TrustManager import kotlin.experimental.and /** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index 77cd3685d7..84b76345c8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -25,6 +25,7 @@ import im.vector.matrix.android.api.failure.GlobalError import im.vector.matrix.android.api.pushrules.PushRuleService import im.vector.matrix.android.api.session.InitialSyncProgressService import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.accountdata.AccountDataService import im.vector.matrix.android.api.session.cache.CacheService import im.vector.matrix.android.api.session.content.ContentUploadStateTracker import im.vector.matrix.android.api.session.content.ContentUrlResolver @@ -37,6 +38,7 @@ import im.vector.matrix.android.api.session.pushers.PushersService import im.vector.matrix.android.api.session.room.RoomDirectoryService import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.api.session.securestorage.SecureStorageService +import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService import im.vector.matrix.android.api.session.signout.SignOutService import im.vector.matrix.android.api.session.sync.FilterService import im.vector.matrix.android.api.session.sync.SyncState @@ -47,7 +49,7 @@ import im.vector.matrix.android.internal.crypto.crosssigning.ShieldTrustUpdater import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.di.WorkManagerProvider -import im.vector.matrix.android.internal.session.sync.SyncTaskSequencer +import im.vector.matrix.android.internal.session.room.timeline.TimelineEventDecryptor import im.vector.matrix.android.internal.session.sync.SyncTokenStore import im.vector.matrix.android.internal.session.sync.job.SyncThread import im.vector.matrix.android.internal.session.sync.job.SyncWorker @@ -86,18 +88,19 @@ internal class DefaultSession @Inject constructor( private val syncThreadProvider: Provider, private val contentUrlResolver: ContentUrlResolver, private val syncTokenStore: SyncTokenStore, - private val syncTaskSequencer: SyncTaskSequencer, private val sessionParamsStore: SessionParamsStore, private val contentUploadProgressTracker: ContentUploadStateTracker, private val initialSyncProgressService: Lazy, private val homeServerCapabilitiesService: Lazy, + private val accountDataService: Lazy, + private val _sharedSecretStorageService: Lazy, + private val timelineEventDecryptor: TimelineEventDecryptor, private val shieldTrustUpdater: ShieldTrustUpdater) : Session, RoomService by roomService.get(), RoomDirectoryService by roomDirectoryService.get(), GroupService by groupService.get(), UserService by userService.get(), - CryptoService by cryptoService.get(), SignOutService by signOutService.get(), FilterService by filterService.get(), PushRuleService by pushRuleService.get(), @@ -106,7 +109,11 @@ internal class DefaultSession @Inject constructor( InitialSyncProgressService by initialSyncProgressService.get(), SecureStorageService by secureStorageService.get(), HomeServerCapabilitiesService by homeServerCapabilitiesService.get(), - ProfileService by profileService.get() { + ProfileService by profileService.get(), + AccountDataService by accountDataService.get() { + + override val sharedSecretStorageService: SharedSecretStorageService + get() = _sharedSecretStorageService.get() private var isOpen = false @@ -121,6 +128,7 @@ internal class DefaultSession @Inject constructor( isOpen = true liveEntityObservers.forEach { it.start() } eventBus.register(this) + timelineEventDecryptor.start() shieldTrustUpdater.start() } @@ -158,11 +166,11 @@ internal class DefaultSession @Inject constructor( override fun close() { assert(isOpen) stopSync() + timelineEventDecryptor.destroy() liveEntityObservers.forEach { it.dispose() } cryptoService.get().close() isOpen = false eventBus.unregister(this) - syncTaskSequencer.close() shieldTrustUpdater.stop() } @@ -204,6 +212,8 @@ internal class DefaultSession @Inject constructor( override fun contentUploadProgressTracker() = contentUploadProgressTracker + override fun cryptoService(): CryptoService = cryptoService.get() + override fun addListener(listener: Session.Listener) { sessionListeners.addListener(listener) } @@ -211,4 +221,9 @@ internal class DefaultSession @Inject constructor( override fun removeListener(listener: Session.Listener) { sessionListeners.removeListener(listener) } + + // For easy debugging + override fun toString(): String { + return "$myUserId - ${sessionParams.credentials.deviceId}" + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt index 4a0e4424d0..1b07377fa1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt @@ -38,6 +38,7 @@ import im.vector.matrix.android.internal.session.pushers.PushersModule import im.vector.matrix.android.internal.session.room.RoomModule import im.vector.matrix.android.internal.session.room.relation.SendRelationWorker import im.vector.matrix.android.internal.session.room.send.EncryptEventWorker +import im.vector.matrix.android.internal.session.room.send.MultipleEventSendingDispatcherWorker import im.vector.matrix.android.internal.session.room.send.RedactEventWorker import im.vector.matrix.android.internal.session.room.send.SendEventWorker import im.vector.matrix.android.internal.session.signout.SignOutModule @@ -85,23 +86,25 @@ internal interface SessionComponent { fun taskExecutor(): TaskExecutor - fun inject(sendEventWorker: SendEventWorker) + fun inject(worker: SendEventWorker) - fun inject(sendEventWorker: SendRelationWorker) + fun inject(worker: SendRelationWorker) - fun inject(encryptEventWorker: EncryptEventWorker) + fun inject(worker: EncryptEventWorker) - fun inject(redactEventWorker: RedactEventWorker) + fun inject(worker: MultipleEventSendingDispatcherWorker) - fun inject(getGroupDataWorker: GetGroupDataWorker) + fun inject(worker: RedactEventWorker) - fun inject(uploadContentWorker: UploadContentWorker) + fun inject(worker: GetGroupDataWorker) - fun inject(syncWorker: SyncWorker) + fun inject(worker: UploadContentWorker) - fun inject(addHttpPusherWorker: AddHttpPusherWorker) + fun inject(worker: SyncWorker) - fun inject(sendVerificationMessageWorker: SendVerificationMessageWorker) + fun inject(worker: AddHttpPusherWorker) + + fun inject(worker: SendVerificationMessageWorker) @Component.Factory interface Factory { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index 969a968a91..908c610914 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -32,8 +32,11 @@ import im.vector.matrix.android.api.auth.data.sessionId import im.vector.matrix.android.api.crypto.MXCryptoConfig import im.vector.matrix.android.api.session.InitialSyncProgressService import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.accountdata.AccountDataService import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService import im.vector.matrix.android.api.session.securestorage.SecureStorageService +import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService +import im.vector.matrix.android.internal.crypto.secrets.DefaultSharedSecretStorageService import im.vector.matrix.android.internal.crypto.verification.VerificationMessageLiveObserver import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory @@ -61,6 +64,7 @@ import im.vector.matrix.android.internal.session.room.create.RoomCreateEventLive import im.vector.matrix.android.internal.session.room.prune.EventsPruner import im.vector.matrix.android.internal.session.room.tombstone.RoomTombstoneEventLiveObserver import im.vector.matrix.android.internal.session.securestorage.DefaultSecureStorageService +import im.vector.matrix.android.internal.session.user.accountdata.DefaultAccountDataService import im.vector.matrix.android.internal.util.md5 import io.realm.RealmConfiguration import okhttp3.OkHttpClient @@ -263,4 +267,10 @@ internal abstract class SessionModule { @Binds abstract fun bindHomeServerCapabilitiesService(homeServerCapabilitiesService: DefaultHomeServerCapabilitiesService): HomeServerCapabilitiesService + + @Binds + abstract fun bindAccountDataService(service: DefaultAccountDataService): AccountDataService + + @Binds + abstract fun bindSharedSecretStorageService(service: DefaultSharedSecretStorageService): SharedSecretStorageService } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ThumbnailExtractor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ThumbnailExtractor.kt index f8935e9283..083cac0278 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ThumbnailExtractor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ThumbnailExtractor.kt @@ -46,7 +46,7 @@ internal object ThumbnailExtractor { } private fun extractVideoThumbnail(attachment: ContentAttachmentData): ThumbnailData? { - val thumbnail = ThumbnailUtils.createVideoThumbnail(attachment.path, MediaStore.Video.Thumbnails.MINI_KIND) + val thumbnail = ThumbnailUtils.createVideoThumbnail(attachment.path, MediaStore.Video.Thumbnails.MINI_KIND) ?: return null val outputStream = ByteArrayOutputStream() thumbnail.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) val thumbnailWidth = thumbnail.width diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt index 1725ef99aa..94bdb11edb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt @@ -17,18 +17,25 @@ package im.vector.matrix.android.internal.session.content import android.content.Context +import android.graphics.BitmapFactory import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass +import id.zelory.compressor.Compressor +import id.zelory.compressor.constraint.default import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.toContent import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.model.message.* +import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageFileContent +import im.vector.matrix.android.api.session.room.model.message.MessageImageContent +import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent 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.network.ProgressRequestBody -import im.vector.matrix.android.internal.session.room.send.SendEventWorker +import im.vector.matrix.android.internal.session.room.send.MultipleEventSendingDispatcherWorker import im.vector.matrix.android.internal.worker.SessionWorkerParams import im.vector.matrix.android.internal.worker.WorkerParamsFactory import im.vector.matrix.android.internal.worker.getSessionComponent @@ -38,15 +45,21 @@ import java.io.File import java.io.FileInputStream import javax.inject.Inject -internal class UploadContentWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { +private data class NewImageAttributes( + val newWidth: Int?, + val newHeight: Int?, + val newFileSize: Int +) + +internal class UploadContentWorker(val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { @JsonClass(generateAdapter = true) internal data class Params( override val sessionId: String, - val roomId: String, - val event: Event, + val events: List, val attachment: ContentAttachmentData, val isRoomEncrypted: Boolean, + val compressBeforeSending: Boolean, override val lastFailureMessage: String? = null ) : SessionWorkerParams @@ -67,20 +80,50 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() sessionComponent.inject(this) - val eventId = params.event.eventId ?: return Result.success() val attachment = params.attachment + var newImageAttributes: NewImageAttributes? = null + val attachmentFile = try { File(attachment.path) } catch (e: Exception) { Timber.e(e) - contentUploadStateTracker.setFailure(params.event.eventId, e) + notifyTracker(params) { contentUploadStateTracker.setFailure(it, e) } return Result.success( WorkerParamsFactory.toData(params.copy( lastFailureMessage = e.localizedMessage )) ) } + .let { originalFile -> + if (attachment.type == ContentAttachmentData.Type.IMAGE) { + if (params.compressBeforeSending) { + Compressor.compress(context, originalFile) { + default( + width = MAX_IMAGE_SIZE, + height = MAX_IMAGE_SIZE + ) + }.also { compressedFile -> + // Update the params + val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } + BitmapFactory.decodeFile(compressedFile.absolutePath, options) + val fileSize = compressedFile.length().toInt() + + newImageAttributes = NewImageAttributes( + options.outWidth, + options.outHeight, + fileSize + ) + } + } else { + // TODO Fix here the image rotation issue + originalFile + } + } else { + // Other type + originalFile + } + } var uploadedThumbnailUrl: String? = null var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null @@ -88,14 +131,14 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : ThumbnailExtractor.extractThumbnail(params.attachment)?.let { thumbnailData -> val thumbnailProgressListener = object : ProgressRequestBody.Listener { override fun onProgress(current: Long, total: Long) { - contentUploadStateTracker.setProgressThumbnail(eventId, current, total) + notifyTracker(params) { contentUploadStateTracker.setProgressThumbnail(it, current, total) } } } try { val contentUploadResponse = if (params.isRoomEncrypted) { Timber.v("Encrypt thumbnail") - contentUploadStateTracker.setEncryptingThumbnail(eventId) + notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) } val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType) uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo fileUploader.uploadByteArray(encryptionResult.encryptedByteArray, @@ -118,10 +161,12 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : val progressListener = object : ProgressRequestBody.Listener { override fun onProgress(current: Long, total: Long) { - if (isStopped) { - contentUploadStateTracker.setFailure(eventId, Throwable("Cancelled")) - } else { - contentUploadStateTracker.setProgress(eventId, current, total) + notifyTracker(params) { + if (isStopped) { + contentUploadStateTracker.setFailure(it, Throwable("Cancelled")) + } else { + contentUploadStateTracker.setProgress(it, current, total) + } } } } @@ -131,7 +176,7 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : return try { val contentUploadResponse = if (params.isRoomEncrypted) { Timber.v("Encrypt file") - contentUploadStateTracker.setEncrypting(eventId) + notifyTracker(params) { contentUploadStateTracker.setEncrypting(it) } val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.mimeType) uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo @@ -143,7 +188,12 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : .uploadFile(attachmentFile, attachment.name, attachment.mimeType, progressListener) } - handleSuccess(params, contentUploadResponse.contentUri, uploadedFileEncryptedFileInfo, uploadedThumbnailUrl, uploadedThumbnailEncryptedFileInfo) + handleSuccess(params, + contentUploadResponse.contentUri, + uploadedFileEncryptedFileInfo, + uploadedThumbnailUrl, + uploadedThumbnailEncryptedFileInfo, + newImageAttributes) } catch (t: Throwable) { Timber.e(t) handleFailure(params, t) @@ -151,7 +201,8 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : } private fun handleFailure(params: Params, failure: Throwable): Result { - contentUploadStateTracker.setFailure(params.event.eventId!!, failure) + notifyTracker(params) { contentUploadStateTracker.setFailure(it, failure) } + return Result.success( WorkerParamsFactory.toData( params.copy( @@ -165,11 +216,17 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : attachmentUrl: String, encryptedFileInfo: EncryptedFileInfo?, thumbnailUrl: String?, - thumbnailEncryptedFileInfo: EncryptedFileInfo?): Result { + thumbnailEncryptedFileInfo: EncryptedFileInfo?, + newImageAttributes: NewImageAttributes?): Result { Timber.v("handleSuccess $attachmentUrl, work is stopped $isStopped") - contentUploadStateTracker.setSuccess(params.event.eventId!!) - val event = updateEvent(params.event, attachmentUrl, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo) - val sendParams = SendEventWorker.Params(params.sessionId, params.roomId, event) + notifyTracker(params) { contentUploadStateTracker.setSuccess(it) } + + val updatedEvents = params.events + .map { + updateEvent(it, attachmentUrl, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo, newImageAttributes) + } + + val sendParams = MultipleEventSendingDispatcherWorker.Params(params.sessionId, updatedEvents, params.isRoomEncrypted) return Result.success(WorkerParamsFactory.toData(sendParams)) } @@ -177,10 +234,11 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : url: String, encryptedFileInfo: EncryptedFileInfo?, thumbnailUrl: String? = null, - thumbnailEncryptedFileInfo: EncryptedFileInfo?): Event { + thumbnailEncryptedFileInfo: EncryptedFileInfo?, + newImageAttributes: NewImageAttributes?): Event { val messageContent: MessageContent = event.content.toModel() ?: return event val updatedContent = when (messageContent) { - is MessageImageContent -> messageContent.update(url, encryptedFileInfo) + is MessageImageContent -> messageContent.update(url, encryptedFileInfo, newImageAttributes) is MessageVideoContent -> messageContent.update(url, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo) is MessageFileContent -> messageContent.update(url, encryptedFileInfo) is MessageAudioContent -> messageContent.update(url, encryptedFileInfo) @@ -189,11 +247,23 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : return event.copy(content = updatedContent.toContent()) } + private fun notifyTracker(params: Params, function: (String) -> Unit) { + params.events + .mapNotNull { it.eventId } + .forEach { eventId -> function.invoke(eventId) } + } + private fun MessageImageContent.update(url: String, - encryptedFileInfo: EncryptedFileInfo?): MessageImageContent { + encryptedFileInfo: EncryptedFileInfo?, + newImageAttributes: NewImageAttributes?): MessageImageContent { return copy( url = if (encryptedFileInfo == null) url else null, - encryptedFileInfo = encryptedFileInfo?.copy(url = url) + encryptedFileInfo = encryptedFileInfo?.copy(url = url), + info = info?.copy( + width = newImageAttributes?.newWidth ?: info.width, + height = newImageAttributes?.newHeight ?: info.height, + size = newImageAttributes?.newFileSize ?: info.size + ) ) } @@ -226,4 +296,8 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : encryptedFileInfo = encryptedFileInfo?.copy(url = url) ) } + + companion object { + private const val MAX_IMAGE_SIZE = 640 + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/Filter.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/Filter.kt index 9acdacd897..fc0472e32f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/Filter.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/Filter.kt @@ -24,13 +24,13 @@ import com.squareup.moshi.JsonClass */ @JsonClass(generateAdapter = true) data class Filter( - @Json(name = "limit") var limit: Int? = null, - @Json(name = "senders") var senders: MutableList? = null, - @Json(name = "not_senders") var notSenders: MutableList? = null, - @Json(name = "types") var types: MutableList? = null, - @Json(name = "not_types") var notTypes: MutableList? = null, - @Json(name = "rooms") var rooms: MutableList? = null, - @Json(name = "not_rooms") var notRooms: MutableList? = null + @Json(name = "limit") val limit: Int? = null, + @Json(name = "senders") val senders: List? = null, + @Json(name = "not_senders") val notSenders: List? = null, + @Json(name = "types") val types: List? = null, + @Json(name = "not_types") val notTypes: List? = null, + @Json(name = "rooms") val rooms: List? = null, + @Json(name = "not_rooms") val notRooms: List? = null ) { fun hasData(): Boolean { return (limit != null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterBody.kt index fa66470c9b..535c66f637 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterBody.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterBody.kt @@ -26,11 +26,11 @@ import im.vector.matrix.android.internal.di.MoshiProvider */ @JsonClass(generateAdapter = true) internal data class FilterBody( - @Json(name = "event_fields") var eventFields: List? = null, - @Json(name = "event_format") var eventFormat: String? = null, - @Json(name = "presence") var presence: Filter? = null, - @Json(name = "account_data") var accountData: Filter? = null, - @Json(name = "room") var room: RoomFilter? = null + @Json(name = "event_fields") val eventFields: List? = null, + @Json(name = "event_format") val eventFormat: String? = null, + @Json(name = "presence") val presence: Filter? = null, + @Json(name = "account_data") val accountData: Filter? = null, + @Json(name = "room") val room: RoomFilter? = null ) { fun toJSONString(): String { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterFactory.kt index 86c94d3dfa..a070759de9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterFactory.kt @@ -21,32 +21,30 @@ import im.vector.matrix.android.api.session.events.model.EventType internal object FilterFactory { fun createDefaultFilterBody(): FilterBody { - val filterBody = FilterBody() - FilterUtil.enableLazyLoading(filterBody, true) - return filterBody + return FilterUtil.enableLazyLoading(FilterBody(), true) } fun createRiotFilterBody(): FilterBody { - val filterBody = FilterBody() - filterBody.room = RoomFilter().apply { - timeline = createRiotTimelineFilter() - state = createRiotStateFilter() - } - return filterBody + return FilterBody( + room = RoomFilter( + timeline = createRiotTimelineFilter(), + state = createRiotStateFilter() + ) + ) } fun createDefaultRoomFilter(): RoomEventFilter { - return RoomEventFilter().apply { - lazyLoadMembers = true - } + return RoomEventFilter( + lazyLoadMembers = true + ) } fun createRiotRoomFilter(): RoomEventFilter { - return RoomEventFilter().apply { - lazyLoadMembers = true - // TODO Enable this for optimization - // types = (listOfSupportedEventTypes + listOfSupportedStateEventTypes).toMutableList() - } + return RoomEventFilter( + lazyLoadMembers = true + // TODO Enable this for optimization + // types = (listOfSupportedEventTypes + listOfSupportedStateEventTypes).toMutableList() + ) } private fun createRiotTimelineFilter(): RoomEventFilter { @@ -57,9 +55,9 @@ internal object FilterFactory { } private fun createRiotStateFilter(): RoomEventFilter { - return RoomEventFilter().apply { - lazyLoadMembers = true - } + return RoomEventFilter( + lazyLoadMembers = true + ) } // Get only managed types by Riot diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterResponse.kt index b27ddae9d3..75e2c23da9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterResponse.kt @@ -24,5 +24,5 @@ import com.squareup.moshi.JsonClass */ @JsonClass(generateAdapter = true) data class FilterResponse( - @Json(name = "filter_id") var filterId: String + @Json(name = "filter_id") val filterId: String ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterUtil.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterUtil.kt index d3020b3fa6..3f4e61e6b5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterUtil.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterUtil.kt @@ -21,7 +21,6 @@ internal object FilterUtil { /** * Patch the filterBody to enable or disable the data save mode * - * * If data save mode is on, FilterBody will contains * FIXME New expected filter: * "{\"room\": {\"ephemeral\": {\"notTypes\": [\"m.typing\"]}}, \"presence\":{\"notTypes\": [\"*\"]}}" @@ -29,6 +28,7 @@ internal object FilterUtil { * @param filterBody filterBody to patch * @param useDataSaveMode true to enable data save mode */ + /* fun enableDataSaveMode(filterBody: FilterBody, useDataSaveMode: Boolean) { if (useDataSaveMode) { // Enable data save mode @@ -78,10 +78,10 @@ internal object FilterUtil { filterBody.presence = null } } - } + } */ /** - * Patch the filterBody to enable or disable the lazy loading + * Compute a new filterBody to enable or disable the lazy loading * * * If lazy loading is on, the filterBody will looks like @@ -90,29 +90,23 @@ internal object FilterUtil { * @param filterBody filterBody to patch * @param useLazyLoading true to enable lazy loading */ - fun enableLazyLoading(filterBody: FilterBody, useLazyLoading: Boolean) { + fun enableLazyLoading(filterBody: FilterBody, useLazyLoading: Boolean): FilterBody { if (useLazyLoading) { // Enable lazy loading - if (filterBody.room == null) { - filterBody.room = RoomFilter() - } - if (filterBody.room!!.state == null) { - filterBody.room!!.state = RoomEventFilter() - } - - filterBody.room!!.state!!.lazyLoadMembers = true + return filterBody.copy( + room = filterBody.room?.copy( + state = filterBody.room.state?.copy(lazyLoadMembers = true) + ?: RoomEventFilter(lazyLoadMembers = true) + ) + ?: RoomFilter(state = RoomEventFilter(lazyLoadMembers = true)) + ) } else { - if (filterBody.room != null && filterBody.room!!.state != null) { - filterBody.room!!.state!!.lazyLoadMembers = null + val newRoomEventFilter = filterBody.room?.state?.copy(lazyLoadMembers = null)?.takeIf { it.hasData() } + val newRoomFilter = filterBody.room?.copy(state = newRoomEventFilter)?.takeIf { it.hasData() } - if (!filterBody.room!!.state!!.hasData()) { - filterBody.room!!.state = null - } - - if (!filterBody.room!!.hasData()) { - filterBody.room = null - } - } + return filterBody.copy( + room = newRoomFilter + ) } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/RoomEventFilter.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/RoomEventFilter.kt index ee81e399ee..9cdccc5c8b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/RoomEventFilter.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/RoomEventFilter.kt @@ -26,14 +26,14 @@ import im.vector.matrix.android.internal.di.MoshiProvider @JsonClass(generateAdapter = true) data class RoomEventFilter( @Json(name = "limit") var limit: Int? = null, - @Json(name = "not_senders") var notSenders: MutableList? = null, - @Json(name = "not_types") var notTypes: MutableList? = null, - @Json(name = "senders") var senders: MutableList? = null, - @Json(name = "types") var types: MutableList? = null, - @Json(name = "rooms") var rooms: MutableList? = null, - @Json(name = "not_rooms") var notRooms: List? = null, - @Json(name = "contains_url") var containsUrl: Boolean? = null, - @Json(name = "lazy_load_members") var lazyLoadMembers: Boolean? = null + @Json(name = "not_senders") val notSenders: List? = null, + @Json(name = "not_types") val notTypes: List? = null, + @Json(name = "senders") val senders: List? = null, + @Json(name = "types") val types: List? = null, + @Json(name = "rooms") val rooms: List? = null, + @Json(name = "not_rooms") val notRooms: List? = null, + @Json(name = "contains_url") val containsUrl: Boolean? = null, + @Json(name = "lazy_load_members") val lazyLoadMembers: Boolean? = null ) { fun toJSONString(): String { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/RoomFilter.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/RoomFilter.kt index 4742bdb988..3109763570 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/RoomFilter.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/RoomFilter.kt @@ -24,13 +24,13 @@ import com.squareup.moshi.JsonClass */ @JsonClass(generateAdapter = true) data class RoomFilter( - @Json(name = "not_rooms") var notRooms: List? = null, - @Json(name = "rooms") var rooms: List? = null, - @Json(name = "ephemeral") var ephemeral: RoomEventFilter? = null, - @Json(name = "include_leave") var includeLeave: Boolean? = null, - @Json(name = "state") var state: RoomEventFilter? = null, - @Json(name = "timeline") var timeline: RoomEventFilter? = null, - @Json(name = "account_data") var accountData: RoomEventFilter? = null + @Json(name = "not_rooms") val notRooms: List? = null, + @Json(name = "rooms") val rooms: List? = null, + @Json(name = "ephemeral") val ephemeral: RoomEventFilter? = null, + @Json(name = "include_leave") val includeLeave: Boolean? = null, + @Json(name = "state") val state: RoomEventFilter? = null, + @Json(name = "timeline") val timeline: RoomEventFilter? = null, + @Json(name = "account_data") val accountData: RoomEventFilter? = null ) { fun hasData(): Boolean { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt index 3837d893f9..2dbd627ce5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt @@ -24,7 +24,7 @@ import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.awaitTransaction import org.greenrobot.eventbus.EventBus -import java.util.* +import java.util.Date import javax.inject.Inject internal interface GetHomeServerCapabilitiesTask : Task diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/PushRulesApi.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/PushRulesApi.kt index 786c1c90e6..db7d2a15ed 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/PushRulesApi.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/PushRulesApi.kt @@ -19,7 +19,11 @@ import im.vector.matrix.android.api.pushrules.rest.GetPushRulesResponse import im.vector.matrix.android.api.pushrules.rest.PushRule import im.vector.matrix.android.internal.network.NetworkConstants import retrofit2.Call -import retrofit2.http.* +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.PUT +import retrofit2.http.Path internal interface PushRulesApi { /** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt index 711e2bd97c..ef55702de6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt @@ -44,9 +44,9 @@ internal class DefaultRoomDirectoryService @Inject constructor(private val getPu .executeBy(taskExecutor) } - override fun joinRoom(roomId: String, reason: String?, callback: MatrixCallback): Cancelable { + override fun joinRoom(roomIdOrAlias: String, reason: String?, callback: MatrixCallback): Cancelable { return joinRoomTask - .configureWith(JoinRoomTask.Params(roomId, reason)) { + .configureWith(JoinRoomTask.Params(roomIdOrAlias, reason)) { this.callback = callback } .executeBy(taskExecutor) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt index e049c1869d..84fc357160 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt @@ -139,9 +139,9 @@ internal class DefaultRoomService @Inject constructor( .executeBy(taskExecutor) } - override fun joinRoom(roomId: String, reason: String?, viaServers: List, callback: MatrixCallback): Cancelable { + override fun joinRoom(roomIdOrAlias: String, reason: String?, viaServers: List, callback: MatrixCallback): Cancelable { return joinRoomTask - .configureWith(JoinRoomTask.Params(roomId, reason, viaServers)) { + .configureWith(JoinRoomTask.Params(roomIdOrAlias, reason, viaServers)) { this.callback = callback } .executeBy(taskExecutor) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt index bd0a2e1b2e..98046a4a36 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt @@ -73,6 +73,21 @@ fun VerificationState.isCanceled(): Boolean { return this == VerificationState.CANCELED_BY_ME || this == VerificationState.CANCELED_BY_OTHER } +// State transition with control +private fun VerificationState?.toState(newState: VerificationState): VerificationState { + // Cancel is always prioritary ? + // Eg id i found that mac or keys mismatch and send a cancel and the other send a done, i have to + // consider as canceled + if (newState.isCanceled()) { + return newState + } + // never move out of cancel + if (this?.isCanceled() == true) { + return this + } + return newState +} + /** * Called by EventRelationAggregationUpdater, when new events that can affect relations are inserted in base. */ @@ -550,38 +565,26 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( } else { ContentMapper.map(verifSummary.content)?.toModel() var data = ContentMapper.map(verifSummary.content)?.toModel() - ?: ReferencesAggregatedContent(VerificationState.REQUEST.name) + ?: ReferencesAggregatedContent(VerificationState.REQUEST) // TODO ignore invalid messages? e.g a START after a CANCEL? // i.e. never change state if already canceled/done - val currentState = VerificationState.values().firstOrNull { data.verificationSummary == it.name } + val currentState = data.verificationState val newState = when (event.getClearType()) { - EventType.KEY_VERIFICATION_START -> { - updateVerificationState(currentState, VerificationState.WAITING) - } - EventType.KEY_VERIFICATION_ACCEPT -> { - updateVerificationState(currentState, VerificationState.WAITING) - } - EventType.KEY_VERIFICATION_READY -> { - updateVerificationState(currentState, VerificationState.WAITING) - } - EventType.KEY_VERIFICATION_KEY -> { - updateVerificationState(currentState, VerificationState.WAITING) - } - EventType.KEY_VERIFICATION_MAC -> { - updateVerificationState(currentState, VerificationState.WAITING) - } - EventType.KEY_VERIFICATION_CANCEL -> { - updateVerificationState(currentState, if (event.senderId == userId) { - VerificationState.CANCELED_BY_ME - } else VerificationState.CANCELED_BY_OTHER) - } - EventType.KEY_VERIFICATION_DONE -> { - updateVerificationState(currentState, VerificationState.DONE) - } + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_READY, + EventType.KEY_VERIFICATION_KEY, + EventType.KEY_VERIFICATION_MAC -> currentState.toState(VerificationState.WAITING) + EventType.KEY_VERIFICATION_CANCEL -> currentState.toState(if (event.senderId == userId) { + VerificationState.CANCELED_BY_ME + } else { + VerificationState.CANCELED_BY_OTHER + }) + EventType.KEY_VERIFICATION_DONE -> currentState.toState(VerificationState.DONE) else -> VerificationState.REQUEST } - data = data.copy(verificationSummary = newState.name) + data = data.copy(verificationState = newState) verifSummary.content = ContentMapper.map(data.toContent()) } @@ -592,18 +595,4 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( verifSummary.sourceEvents.add(event.eventId) } } - - private fun updateVerificationState(oldState: VerificationState?, newState: VerificationState): VerificationState { - // Cancel is always prioritary ? - // Eg id i found that mac or keys mismatch and send a cancel and the other send a done, i have to - // consider as canceled - if (newState == VerificationState.CANCELED_BY_OTHER || newState == VerificationState.CANCELED_BY_ME) { - return newState - } - // never move out of cancel - if (oldState == VerificationState.CANCELED_BY_OTHER || oldState == VerificationState.CANCELED_BY_ME) { - return oldState - } - return newState - } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt index 9be63565fe..f5ddf6ae4b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt @@ -20,6 +20,7 @@ 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.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse +import im.vector.matrix.android.api.session.room.model.create.JoinRoomResponse import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsParams import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsResponse import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProtocol @@ -223,13 +224,13 @@ internal interface RoomAPI { * Join the given room. * * @param roomIdOrAlias the room id or alias - * @param server_name the servers to attempt to join the room through + * @param viaServers the servers to attempt to join the room through * @param params the request body */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "join/{roomIdOrAlias}") fun join(@Path("roomIdOrAlias") roomIdOrAlias: String, @Query("server_name") viaServers: List, - @Body params: Map): Call + @Body params: Map): Call /** * Leave the given room. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt index a441d17196..17541de9aa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.session.room import com.zhuinden.monarchy.Monarchy +import dagger.Lazy import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel @@ -41,17 +42,20 @@ import im.vector.matrix.android.internal.database.query.whereType import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.session.room.membership.RoomDisplayNameResolver import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper +import im.vector.matrix.android.internal.session.room.timeline.TimelineEventDecryptor import im.vector.matrix.android.internal.session.sync.RoomSyncHandler import im.vector.matrix.android.internal.session.sync.model.RoomSyncSummary import im.vector.matrix.android.internal.session.sync.model.RoomSyncUnreadNotifications import io.realm.Realm import org.greenrobot.eventbus.EventBus +import timber.log.Timber import javax.inject.Inject internal class RoomSummaryUpdater @Inject constructor( @UserId private val userId: String, private val roomDisplayNameResolver: RoomDisplayNameResolver, private val roomAvatarResolver: RoomAvatarResolver, + private val timelineEventDecryptor: Lazy, private val eventBus: EventBus, private val monarchy: Monarchy) { @@ -81,7 +85,8 @@ internal class RoomSummaryUpdater @Inject constructor( roomSummary: RoomSyncSummary? = null, unreadNotifications: RoomSyncUnreadNotifications? = null, updateMembers: Boolean = false, - ephemeralResult: RoomSyncHandler.EphemeralResult? = null) { + ephemeralResult: RoomSyncHandler.EphemeralResult? = null, + inviterId: String? = null) { val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId) if (roomSummary != null) { if (roomSummary.heroes.isNotEmpty()) { @@ -134,6 +139,17 @@ internal class RoomSummaryUpdater @Inject constructor( roomSummaryEntity.typingUserIds.clear() roomSummaryEntity.typingUserIds.addAll(ephemeralResult?.typingUserIds.orEmpty()) + if (roomSummaryEntity.membership == Membership.INVITE && inviterId != null) { + roomSummaryEntity.inviterId = inviterId + } else if (roomSummaryEntity.membership != Membership.INVITE) { + roomSummaryEntity.inviterId = null + } + + if (latestPreviewableEvent?.root?.type == EventType.ENCRYPTED && latestPreviewableEvent.root?.decryptionResultJson == null) { + Timber.v("Should decrypt ${latestPreviewableEvent.eventId}") + timelineEventDecryptor.get().requestDecryption(TimelineEventDecryptor.DecryptionRequest(latestPreviewableEvent.eventId, "")) + } + if (updateMembers) { val otherRoomMembers = RoomMemberHelper(realm, roomId) .queryRoomMembersEvent() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt index f120130739..7314f55961 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt @@ -66,7 +66,7 @@ internal class DefaultCreateRoomTask @Inject constructor( val createRoomResponse = executeRequest(eventBus) { apiCall = roomAPI.createRoom(createRoomParams) } - val roomId = createRoomResponse.roomId!! + val roomId = createRoomResponse.roomId // Wait for room to come back from the sync (but it can maybe be in the DB if the sync response is received before) try { awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventLiveObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventLiveObserver.kt index e71fe8d0d1..fb3880e38d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventLiveObserver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventLiveObserver.kt @@ -27,8 +27,8 @@ import im.vector.matrix.android.internal.database.awaitTransaction import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity -import im.vector.matrix.android.internal.database.query.whereTypes import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.database.query.whereTypes import im.vector.matrix.android.internal.di.SessionDatabase import io.realm.OrderedCollectionChangeSet import io.realm.RealmConfiguration diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/LoadRoomMembersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/LoadRoomMembersTask.kt index 5dfe54a7bb..41993c323a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/LoadRoomMembersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/LoadRoomMembersTask.kt @@ -22,6 +22,7 @@ import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.internal.database.mapper.toEntity import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity import im.vector.matrix.android.internal.database.model.RoomEntity +import im.vector.matrix.android.internal.database.query.copyToRealmOrIgnore import im.vector.matrix.android.internal.database.query.getOrCreate import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.network.executeRequest @@ -73,9 +74,7 @@ internal class DefaultLoadRoomMembersTask @Inject constructor( if (roomMemberEvent.eventId == null || roomMemberEvent.stateKey == null) { continue } - val eventEntity = roomMemberEvent.toEntity(roomId, SendState.SYNCED).let { - realm.copyToRealmOrUpdate(it) - } + val eventEntity = roomMemberEvent.toEntity(roomId, SendState.SYNCED).copyToRealmOrIgnore(realm) CurrentStateEventEntity.getOrCreate(realm, roomId, roomMemberEvent.stateKey, roomMemberEvent.type).apply { eventId = roomMemberEvent.eventId root = eventEntity diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt index 0153930226..635f3955c2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.session.room.membership.joining import im.vector.matrix.android.api.session.room.failure.JoinRoomFailure +import im.vector.matrix.android.api.session.room.model.create.JoinRoomResponse import im.vector.matrix.android.internal.database.awaitNotEmptyResult import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntityFields @@ -33,7 +34,7 @@ import javax.inject.Inject internal interface JoinRoomTask : Task { data class Params( - val roomId: String, + val roomIdOrAlias: String, val reason: String?, val viaServers: List = emptyList() ) @@ -48,19 +49,20 @@ internal class DefaultJoinRoomTask @Inject constructor( ) : JoinRoomTask { override suspend fun execute(params: JoinRoomTask.Params) { - executeRequest(eventBus) { - apiCall = roomAPI.join(params.roomId, params.viaServers, mapOf("reason" to params.reason)) + val joinRoomResponse = executeRequest(eventBus) { + apiCall = roomAPI.join(params.roomIdOrAlias, params.viaServers, mapOf("reason" to params.reason)) } // Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before) + val roomId = joinRoomResponse.roomId try { awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> realm.where(RoomEntity::class.java) - .equalTo(RoomEntityFields.ROOM_ID, params.roomId) + .equalTo(RoomEntityFields.ROOM_ID, roomId) } } catch (exception: TimeoutCancellationException) { throw JoinRoomFailure.JoinedWithTimeout } - setReadMarkers(params.roomId) + setReadMarkers(roomId) } private suspend fun setReadMarkers(roomId: String) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/notification/RoomPushRuleMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/notification/RoomPushRuleMapper.kt index 770ce55cde..f715cde4c1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/notification/RoomPushRuleMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/notification/RoomPushRuleMapper.kt @@ -16,9 +16,13 @@ package im.vector.matrix.android.internal.session.room.notification -import im.vector.matrix.android.api.pushrules.* +import im.vector.matrix.android.api.pushrules.Action +import im.vector.matrix.android.api.pushrules.Condition +import im.vector.matrix.android.api.pushrules.RuleSetKey +import im.vector.matrix.android.api.pushrules.getActions import im.vector.matrix.android.api.pushrules.rest.PushCondition import im.vector.matrix.android.api.pushrules.rest.PushRule +import im.vector.matrix.android.api.pushrules.toJson import im.vector.matrix.android.api.session.room.notification.RoomNotificationState import im.vector.matrix.android.internal.database.mapper.PushRulesMapper import im.vector.matrix.android.internal.database.model.PushRuleEntity diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt index 0310020b5a..1f199322af 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt @@ -196,13 +196,13 @@ internal class DefaultRelationService @AssistedInject constructor( private fun createEncryptEventWork(event: Event, keepKeys: List?): OneTimeWorkRequest { // Same parameter - val params = EncryptEventWorker.Params(sessionId, roomId, event, keepKeys) + val params = EncryptEventWorker.Params(sessionId, event, keepKeys) val sendWorkData = WorkerParamsFactory.toData(params) return timeLineSendEventWorkCommon.createWork(sendWorkData, true) } private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { - val sendContentWorkerParams = SendEventWorker.Params(sessionId, roomId, event) + val sendContentWorkerParams = SendEventWorker.Params(sessionId, event) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) return timeLineSendEventWorkCommon.createWork(sendWorkData, startChain) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index fbb80adf83..a99337695a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt @@ -22,7 +22,6 @@ import androidx.work.OneTimeWorkRequest import androidx.work.Operation import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject -import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.events.model.Event @@ -49,7 +48,6 @@ import java.util.concurrent.Executors import java.util.concurrent.TimeUnit private const val UPLOAD_WORK = "UPLOAD_WORK" -private const val BACKOFF_DELAY = 10_000L internal class DefaultSendService @AssistedInject constructor( @Assisted private val roomId: String, @@ -58,7 +56,6 @@ internal class DefaultSendService @AssistedInject constructor( @SessionId private val sessionId: String, private val localEchoEventFactory: LocalEchoEventFactory, private val cryptoService: CryptoService, - private val monarchy: Monarchy, private val taskExecutor: TaskExecutor, private val localEchoRepository: LocalEchoRepository ) : SendService { @@ -103,6 +100,7 @@ internal class DefaultSendService @AssistedInject constructor( return if (cryptoService.isRoomEncrypted(roomId)) { Timber.v("Send event in encrypted room") val encryptWork = createEncryptEventWork(event, true) + // Note that event will be replaced by the result of the previous work val sendWork = createSendEventWork(event, false) timelineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, sendWork) } else { @@ -111,9 +109,11 @@ internal class DefaultSendService @AssistedInject constructor( } } - override fun sendMedias(attachments: List): Cancelable { + override fun sendMedias(attachments: List, + compressBeforeSending: Boolean, + roomIds: Set): Cancelable { return attachments.mapTo(CancelableBag()) { - sendMedia(it) + sendMedia(it, compressBeforeSending, roomIds) } } @@ -201,43 +201,56 @@ internal class DefaultSendService @AssistedInject constructor( } } - override fun sendMedia(attachment: ContentAttachmentData): Cancelable { + override fun sendMedia(attachment: ContentAttachmentData, + compressBeforeSending: Boolean, + roomIds: Set): Cancelable { // Create an event with the media file path - val event = localEchoEventFactory.createMediaEvent(roomId, attachment).also { - createLocalEcho(it) + // Ensure current roomId is included in the set + val allRoomIds = (roomIds + roomId).toList() + + // Create local echo for each room + val allLocalEchoes = allRoomIds.map { + localEchoEventFactory.createMediaEvent(it, attachment).also { event -> + createLocalEcho(event) + } } - return internalSendMedia(event, attachment) + return internalSendMedia(allLocalEchoes, attachment, compressBeforeSending) } - private fun internalSendMedia(localEcho: Event, attachment: ContentAttachmentData): Cancelable { - val isRoomEncrypted = cryptoService.isRoomEncrypted(roomId) + /** + * We use the roomId of the local echo event + */ + private fun internalSendMedia(allLocalEchoes: List, attachment: ContentAttachmentData, compressBeforeSending: Boolean): Cancelable { + val cancelableBag = CancelableBag() - val uploadWork = createUploadMediaWork(localEcho, attachment, isRoomEncrypted, startChain = true) - val sendWork = createSendEventWork(localEcho, false) + allLocalEchoes.groupBy { cryptoService.isRoomEncrypted(it.roomId!!) } + .apply { + keys.forEach { isRoomEncrypted -> + // Should never be empty + val localEchoes = get(isRoomEncrypted).orEmpty() + val uploadWork = createUploadMediaWork(localEchoes, attachment, isRoomEncrypted, compressBeforeSending, startChain = true) - if (isRoomEncrypted) { - val encryptWork = createEncryptEventWork(localEcho, false /*not start of chain, take input error*/) + val dispatcherWork = createMultipleEventDispatcherWork(isRoomEncrypted) - val op: Operation = workManagerProvider.workManager - .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork) - .then(encryptWork) - .then(sendWork) - .enqueue() - op.result.addListener(Runnable { - if (op.result.isCancelled) { - Timber.e("CHAIN WAS CANCELLED") - } else if (op.state.value is Operation.State.FAILURE) { - Timber.e("CHAIN DID FAIL") + workManagerProvider.workManager + .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork) + .then(dispatcherWork) + .enqueue() + .also { operation -> + operation.result.addListener(Runnable { + if (operation.result.isCancelled) { + Timber.e("CHAIN WAS CANCELLED") + } else if (operation.state.value is Operation.State.FAILURE) { + Timber.e("CHAIN DID FAIL") + } + }, workerFutureListenerExecutor) + } + + cancelableBag.add(CancelableWork(workManagerProvider.workManager, dispatcherWork.id)) + } } - }, workerFutureListenerExecutor) - } else { - workManagerProvider.workManager - .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork) - .then(sendWork) - .enqueue() - } - return CancelableWork(workManagerProvider.workManager, sendWork.id) + return cancelableBag } private fun createLocalEcho(event: Event) { @@ -250,19 +263,19 @@ internal class DefaultSendService @AssistedInject constructor( private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { // Same parameter - val params = EncryptEventWorker.Params(sessionId, roomId, event) + val params = EncryptEventWorker.Params(sessionId, event) val sendWorkData = WorkerParamsFactory.toData(params) return workManagerProvider.matrixOneTimeWorkRequestBuilder() .setConstraints(WorkManagerProvider.workConstraints) .setInputData(sendWorkData) .startChain(startChain) - .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS) .build() } private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { - val sendContentWorkerParams = SendEventWorker.Params(sessionId, roomId, event) + val sendContentWorkerParams = SendEventWorker.Params(sessionId, event) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) return timelineSendEventWorkCommon.createWork(sendWorkData, startChain) @@ -277,18 +290,33 @@ internal class DefaultSendService @AssistedInject constructor( return timelineSendEventWorkCommon.createWork(redactWorkData, true) } - private fun createUploadMediaWork(event: Event, + private fun createUploadMediaWork(allLocalEchos: List, attachment: ContentAttachmentData, isRoomEncrypted: Boolean, + compressBeforeSending: Boolean, startChain: Boolean): OneTimeWorkRequest { - val uploadMediaWorkerParams = UploadContentWorker.Params(sessionId, roomId, event, attachment, isRoomEncrypted) + val uploadMediaWorkerParams = UploadContentWorker.Params(sessionId, allLocalEchos, attachment, isRoomEncrypted, compressBeforeSending) val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams) return workManagerProvider.matrixOneTimeWorkRequestBuilder() .setConstraints(WorkManagerProvider.workConstraints) .startChain(startChain) .setInputData(uploadWorkData) - .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() + } + + private fun createMultipleEventDispatcherWork(isRoomEncrypted: Boolean): OneTimeWorkRequest { + // the list of events will be replaced by the result of the media upload work + val params = MultipleEventSendingDispatcherWorker.Params(sessionId, emptyList(), isRoomEncrypted) + val workData = WorkerParamsFactory.toData(params) + + return workManagerProvider.matrixOneTimeWorkRequestBuilder() + // No constraint + // .setConstraints(WorkManagerProvider.workConstraints) + .startChain(false) + .setInputData(workData) + .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS) .build() } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt index 21080d9037..72f5ee56b8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt @@ -20,7 +20,6 @@ import android.content.Context import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass -import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.events.model.Event @@ -39,9 +38,8 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) @JsonClass(generateAdapter = true) internal data class Params( override val sessionId: String, - val roomId: String, val event: Event, - /**Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to)*/ + /** Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to) */ val keepKeys: List? = null, override val lastFailureMessage: String? = null ) : SessionWorkerParams @@ -53,7 +51,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) Timber.v("Start Encrypt work") val params = WorkerParamsFactory.fromData(inputData) ?: return Result.success().also { - Timber.v("Work cancelled due to input error from parent") + Timber.e("Work cancelled due to input error from parent") } Timber.v("Start Encrypt work for event ${params.event.eventId}") @@ -80,7 +78,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) var result: MXEncryptEventContentResult? = null try { result = awaitCallback { - crypto.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it) + crypto.encryptEventContent(localMutableContent, localEvent.type, localEvent.roomId!!, it) } } catch (throwable: Throwable) { error = throwable @@ -98,7 +96,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) type = safeResult.eventType, content = safeResult.eventContent ) - val nextWorkerParams = SendEventWorker.Params(params.sessionId, params.roomId, encryptedEvent) + val nextWorkerParams = SendEventWorker.Params(params.sessionId, encryptedEvent) return Result.success(WorkerParamsFactory.toData(nextWorkerParams)) } else { val sendState = when (error) { @@ -107,7 +105,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) } localEchoUpdater.updateSendState(localEvent.eventId, sendState) // always return success, or the chain will be stuck for ever! - val nextWorkerParams = SendEventWorker.Params(params.sessionId, params.roomId, localEvent, error?.localizedMessage + val nextWorkerParams = SendEventWorker.Params(params.sessionId, localEvent, error?.localizedMessage ?: "Error") return Result.success(WorkerParamsFactory.toData(nextWorkerParams)) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index f77f4b7f3a..51de36291d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.internal.session.room.send +import android.graphics.Bitmap import android.media.MediaMetadataRetriever import androidx.exifinterface.media.ExifInterface import im.vector.matrix.android.R @@ -275,9 +276,9 @@ internal class LocalEchoEventFactory @Inject constructor( mediaDataRetriever.setDataSource(attachment.path) // Use frame to calculate height and width as we are sure to get the right ones - val firstFrame = mediaDataRetriever.frameAtTime - val height = firstFrame.height - val width = firstFrame.width + val firstFrame: Bitmap? = mediaDataRetriever.frameAtTime + val height = firstFrame?.height ?: 0 + val width = firstFrame?.width ?: 0 mediaDataRetriever.release() val thumbnailInfo = ThumbnailExtractor.extractThumbnail(attachment)?.let { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt index fa43dde95e..af593198af 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt @@ -40,7 +40,6 @@ import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.Realm import org.greenrobot.eventbus.EventBus import timber.log.Timber -import java.lang.IllegalStateException import javax.inject.Inject internal class LocalEchoRepository @Inject constructor(private val monarchy: Monarchy, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt new file mode 100644 index 0000000000..03db817dd6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt @@ -0,0 +1,103 @@ +/* + * Copyright (c) 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.session.room.send + +import android.content.Context +import androidx.work.BackoffPolicy +import androidx.work.CoroutineWorker +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.internal.di.WorkManagerProvider +import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon +import im.vector.matrix.android.internal.worker.SessionWorkerParams +import im.vector.matrix.android.internal.worker.WorkerParamsFactory +import im.vector.matrix.android.internal.worker.getSessionComponent +import im.vector.matrix.android.internal.worker.startChain +import timber.log.Timber +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +/** + * This worker creates a new work for each events passed in parameter + */ +internal class MultipleEventSendingDispatcherWorker(context: Context, params: WorkerParameters) + : CoroutineWorker(context, params) { + + @JsonClass(generateAdapter = true) + internal data class Params( + override val sessionId: String, + val events: List, + val isEncrypted: Boolean, + override val lastFailureMessage: String? = null + ) : SessionWorkerParams + + @Inject lateinit var workManagerProvider: WorkManagerProvider + @Inject lateinit var timelineSendEventWorkCommon: TimelineSendEventWorkCommon + + override suspend fun doWork(): Result { + Timber.v("Start dispatch sending multiple event work") + val params = WorkerParamsFactory.fromData(inputData) + ?: return Result.success().also { + Timber.e("Work cancelled due to input error from parent") + } + + if (params.lastFailureMessage != null) { + // Transmit the error + return Result.success(inputData) + } + + val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() + sessionComponent.inject(this) + + // Create a work for every event + params.events.forEach { event -> + if (params.isEncrypted) { + Timber.v("Send event in encrypted room") + val encryptWork = createEncryptEventWork(params.sessionId, event, true) + // Note that event will be replaced by the result of the previous work + val sendWork = createSendEventWork(params.sessionId, event, false) + timelineSendEventWorkCommon.postSequentialWorks(event.roomId!!, encryptWork, sendWork) + } else { + val sendWork = createSendEventWork(params.sessionId, event, true) + timelineSendEventWorkCommon.postWork(event.roomId!!, sendWork) + } + } + + return Result.success() + } + + private fun createEncryptEventWork(sessionId: String, event: Event, startChain: Boolean): OneTimeWorkRequest { + val params = EncryptEventWorker.Params(sessionId, event) + val sendWorkData = WorkerParamsFactory.toData(params) + + return workManagerProvider.matrixOneTimeWorkRequestBuilder() + .setConstraints(WorkManagerProvider.workConstraints) + .setInputData(sendWorkData) + .startChain(startChain) + .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() + } + + private fun createSendEventWork(sessionId: String, event: Event, startChain: Boolean): OneTimeWorkRequest { + val sendContentWorkerParams = SendEventWorker.Params(sessionId, event) + val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) + + return timelineSendEventWorkCommon.createWork(sendWorkData, startChain) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt index 3d038a0c82..69d288a932 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt @@ -21,7 +21,6 @@ import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.failure.shouldBeRetried -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.room.send.SendState import im.vector.matrix.android.internal.network.executeRequest @@ -30,6 +29,7 @@ import im.vector.matrix.android.internal.worker.SessionWorkerParams import im.vector.matrix.android.internal.worker.WorkerParamsFactory import im.vector.matrix.android.internal.worker.getSessionComponent import org.greenrobot.eventbus.EventBus +import timber.log.Timber import javax.inject.Inject internal class SendEventWorker(context: Context, @@ -39,7 +39,6 @@ internal class SendEventWorker(context: Context, @JsonClass(generateAdapter = true) internal data class Params( override val sessionId: String, - val roomId: String, val event: Event, override val lastFailureMessage: String? = null ) : SessionWorkerParams @@ -50,7 +49,9 @@ internal class SendEventWorker(context: Context, override suspend fun doWork(): Result { val params = WorkerParamsFactory.fromData(inputData) - ?: return Result.success() + ?: return Result.success().also { + Timber.e("Work cancelled due to input error from parent") + } val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() sessionComponent.inject(this) @@ -66,7 +67,7 @@ internal class SendEventWorker(context: Context, return Result.success(inputData) } return try { - sendEvent(event.eventId, event.type, event.content, params.roomId) + sendEvent(event) Result.success() } catch (exception: Throwable) { if (exception.shouldBeRetried()) { @@ -79,16 +80,16 @@ internal class SendEventWorker(context: Context, } } - private suspend fun sendEvent(eventId: String, eventType: String, content: Content?, roomId: String) { - localEchoUpdater.updateSendState(eventId, SendState.SENDING) + private suspend fun sendEvent(event: Event) { + localEchoUpdater.updateSendState(event.eventId!!, SendState.SENDING) executeRequest(eventBus) { apiCall = roomAPI.send( - eventId, - roomId, - eventType, - content + event.eventId, + event.roomId!!, + event.type, + event.content ) } - localEchoUpdater.updateSendState(eventId, SendState.SENT) + localEchoUpdater.updateSendState(event.eventId, SendState.SENT) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt index 1a7b8228b9..6055592eab 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt @@ -17,7 +17,7 @@ package im.vector.matrix.android.internal.session.room.send.pills import android.text.SpannableString import im.vector.matrix.android.api.session.room.send.MatrixItemSpan -import java.util.* +import java.util.Collections import javax.inject.Inject /** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index 19b87122e8..53bd620e51 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.session.room.timeline import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.RelationType import im.vector.matrix.android.api.session.events.model.toModel @@ -73,11 +72,11 @@ internal class DefaultTimeline( private val taskExecutor: TaskExecutor, private val contextOfEventTask: GetContextOfEventTask, private val paginationTask: PaginationTask, - private val cryptoService: CryptoService, private val timelineEventMapper: TimelineEventMapper, private val settings: TimelineSettings, private val hiddenReadReceipts: TimelineHiddenReadReceipts, - private val eventBus: EventBus + private val eventBus: EventBus, + private val eventDecryptor: TimelineEventDecryptor ) : Timeline, TimelineHiddenReadReceipts.Delegate { data class OnNewTimelineEvents(val roomId: String, val eventIds: List) @@ -114,8 +113,6 @@ internal class DefaultTimeline( override val isLive get() = !hasMoreToLoad(Timeline.Direction.FORWARDS) - private val eventDecryptor = TimelineEventDecryptor(realmConfiguration, timelineID, cryptoService) - private val eventsChangeListener = OrderedRealmCollectionChangeListener> { results, changeSet -> if (!results.isLoaded || !results.isValid) { return@OrderedRealmCollectionChangeListener @@ -607,7 +604,7 @@ internal class DefaultTimeline( if (timelineEvent.isEncrypted() && timelineEvent.root.mxDecryptionResult == null) { - timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) } + timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(it, timelineID)) } } val position = if (direction == Timeline.Direction.FORWARDS) 0 else builtEvents.size diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt index 3e783f98a4..c02bb915ef 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt @@ -21,7 +21,6 @@ import androidx.lifecycle.Transformations import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.session.crypto.CryptoService 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.TimelineService @@ -41,7 +40,7 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv private val eventBus: EventBus, private val taskExecutor: TaskExecutor, private val contextOfEventTask: GetContextOfEventTask, - private val cryptoService: CryptoService, + private val eventDecryptor: TimelineEventDecryptor, private val paginationTask: PaginationTask, private val timelineEventMapper: TimelineEventMapper, private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper @@ -60,11 +59,11 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv taskExecutor = taskExecutor, contextOfEventTask = contextOfEventTask, paginationTask = paginationTask, - cryptoService = cryptoService, timelineEventMapper = timelineEventMapper, settings = settings, hiddenReadReceipts = TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings), - eventBus = eventBus + eventBus = eventBus, + eventDecryptor = eventDecryptor ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt index 800e6c8d11..2d6656c2e3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt @@ -23,15 +23,19 @@ import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventConten import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.di.SessionDatabase +import im.vector.matrix.android.internal.session.SessionScope import io.realm.Realm import io.realm.RealmConfiguration import timber.log.Timber import java.util.concurrent.ExecutorService import java.util.concurrent.Executors +import javax.inject.Inject -internal class TimelineEventDecryptor( +@SessionScope +internal class TimelineEventDecryptor @Inject constructor( + @SessionDatabase private val realmConfiguration: RealmConfiguration, - private val timelineId: String, private val cryptoService: CryptoService ) { @@ -53,9 +57,9 @@ internal class TimelineEventDecryptor( private var executor: ExecutorService? = null // Set of eventIds which are currently decrypting - private val existingRequests = mutableSetOf() + private val existingRequests = mutableSetOf() // sessionId -> list of eventIds - private val unknownSessionsFailure = mutableMapOf>() + private val unknownSessionsFailure = mutableMapOf>() fun start() { executor = Executors.newSingleThreadExecutor() @@ -74,53 +78,51 @@ internal class TimelineEventDecryptor( } } - fun requestDecryption(eventId: String) { + fun requestDecryption(request: DecryptionRequest) { synchronized(unknownSessionsFailure) { - for (eventIds in unknownSessionsFailure.values) { - if (eventId in eventIds) { - Timber.d("Skip Decryption request for event $eventId, unknown session") + for (requests in unknownSessionsFailure.values) { + if (request in requests) { + Timber.d("Skip Decryption request for event ${request.eventId}, unknown session") return } } } synchronized(existingRequests) { - if (!existingRequests.add(eventId)) { - Timber.d("Skip Decryption request for event $eventId, already requested") + if (!existingRequests.add(request)) { + Timber.d("Skip Decryption request for event ${request.eventId}, already requested") return } } executor?.execute { Realm.getInstance(realmConfiguration).use { realm -> - processDecryptRequest(eventId, realm) + processDecryptRequest(request, realm) } } } - private fun processDecryptRequest(eventId: String, realm: Realm) { + private fun processDecryptRequest(request: DecryptionRequest, realm: Realm) = realm.executeTransaction { + val eventId = request.eventId + val timelineId = request.timelineId Timber.v("Decryption request for event $eventId") val eventEntity = EventEntity.where(realm, eventId = eventId).findFirst() - ?: return Unit.also { + ?: return@executeTransaction Unit.also { Timber.d("Decryption request for unknown message") } val event = eventEntity.asDomain() try { val result = cryptoService.decryptEvent(event, timelineId) Timber.v("Successfully decrypted event $eventId") - realm.executeTransaction { - eventEntity.setDecryptionResult(result) - } + eventEntity.setDecryptionResult(result) } catch (e: MXCryptoError) { Timber.w(e, "Failed to decrypt event $eventId") if (e is MXCryptoError.Base && e.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) { // Keep track of unknown sessions to automatically try to decrypt on new session - realm.executeTransaction { - eventEntity.decryptionErrorCode = e.errorType.name - } + eventEntity.decryptionErrorCode = e.errorType.name event.content?.toModel()?.let { content -> content.sessionId?.let { sessionId -> synchronized(unknownSessionsFailure) { val list = unknownSessionsFailure.getOrPut(sessionId) { mutableSetOf() } - list.add(eventId) + list.add(request) } } } @@ -129,8 +131,13 @@ internal class TimelineEventDecryptor( Timber.e(t, "Failed to decrypt event $eventId") } finally { synchronized(existingRequests) { - existingRequests.remove(eventId) + existingRequests.remove(request) } } } + + data class DecryptionRequest( + val eventId: String, + val timelineId: String + ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineSendEventWorkCommon.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineSendEventWorkCommon.kt index ff3cedf044..5621866917 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineSendEventWorkCommon.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineSendEventWorkCommon.kt @@ -15,7 +15,11 @@ */ package im.vector.matrix.android.internal.session.room.timeline -import androidx.work.* +import androidx.work.BackoffPolicy +import androidx.work.Data +import androidx.work.ExistingWorkPolicy +import androidx.work.ListenableWorker +import androidx.work.OneTimeWorkRequest import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.NoOpCancellable import im.vector.matrix.android.internal.di.WorkManagerProvider diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt index 41920158c4..164626224b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -31,6 +31,7 @@ import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity +import im.vector.matrix.android.internal.database.query.copyToRealmOrIgnore import im.vector.matrix.android.internal.database.query.create import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.database.query.findAllIncludingEvents @@ -199,9 +200,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy val stateEvents = receivedChunk.stateEvents for (stateEvent in stateEvents) { - val stateEventEntity = stateEvent.toEntity(roomId, SendState.SYNCED).let { - realm.copyToRealmOrUpdate(it) - } + val stateEventEntity = stateEvent.toEntity(roomId, SendState.SYNCED).copyToRealmOrIgnore(realm) currentChunk.addStateEvent(roomId, stateEventEntity, direction) if (stateEvent.type == EventType.STATE_ROOM_MEMBER && stateEvent.stateKey != null) { roomMemberContentsByUser[stateEvent.stateKey] = stateEvent.content.toModel() @@ -213,9 +212,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy continue } eventIds.add(event.eventId) - val eventEntity = event.toEntity(roomId, SendState.SYNCED).let { - realm.copyToRealmOrUpdate(it) - } + val eventEntity = event.toEntity(roomId, SendState.SYNCED).copyToRealmOrIgnore(realm) if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null) { val contentToUse = if (direction == PaginationDirection.BACKWARDS) { event.prevContent diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventLiveObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventLiveObserver.kt index 12f7f92794..7ca8aaa1d6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventLiveObserver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventLiveObserver.kt @@ -27,8 +27,8 @@ import im.vector.matrix.android.internal.database.awaitTransaction import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity -import im.vector.matrix.android.internal.database.query.whereTypes import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.database.query.whereTypes import im.vector.matrix.android.internal.di.SessionDatabase import io.realm.OrderedCollectionChangeSet import io.realm.RealmConfiguration @@ -62,7 +62,7 @@ internal class RoomTombstoneEventLiveObserver @Inject constructor(@SessionDataba for (event in tombstoneEvents) { if (event.roomId == null) continue val createRoomContent = event.getClearContent().toModel() - if (createRoomContent?.replacementRoom == null) continue + if (createRoomContent?.replacementRoomId == null) continue val predecessorRoomSummary = RoomSummaryEntity.where(realm, event.roomId).findFirst() ?: RoomSummaryEntity(event.roomId) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/SecretStoringUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/SecretStoringUtils.kt index 592191975e..7043c3abbe 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/SecretStoringUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/SecretStoringUtils.kt @@ -25,14 +25,25 @@ import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import androidx.annotation.RequiresApi import timber.log.Timber -import java.io.* +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.io.OutputStream import java.math.BigInteger import java.security.KeyPairGenerator import java.security.KeyStore import java.security.KeyStoreException import java.security.SecureRandom import java.util.Calendar -import javax.crypto.* +import javax.crypto.Cipher +import javax.crypto.CipherInputStream +import javax.crypto.CipherOutputStream +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import javax.crypto.SecretKeyFactory import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.PBEKeySpec diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt index 05a2324047..b14a7758c5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt @@ -23,7 +23,13 @@ import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.auth.SessionParamsStore import im.vector.matrix.android.internal.crypto.CryptoModule import im.vector.matrix.android.internal.database.RealmKeysUtils -import im.vector.matrix.android.internal.di.* +import im.vector.matrix.android.internal.di.CryptoDatabase +import im.vector.matrix.android.internal.di.SessionCacheDirectory +import im.vector.matrix.android.internal.di.SessionDatabase +import im.vector.matrix.android.internal.di.SessionFilesDirectory +import im.vector.matrix.android.internal.di.SessionId +import im.vector.matrix.android.internal.di.UserMd5 +import im.vector.matrix.android.internal.di.WorkManagerProvider import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.SessionModule import im.vector.matrix.android.internal.session.cache.ClearCacheTask diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt index ab2c9a48a6..5b7c39a3d9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt @@ -32,6 +32,7 @@ import im.vector.matrix.android.internal.database.mapper.toEntity import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity import im.vector.matrix.android.internal.database.model.RoomEntity +import im.vector.matrix.android.internal.database.query.copyToRealmOrIgnore import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom import im.vector.matrix.android.internal.database.query.getOrCreate @@ -93,7 +94,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle } is HandlingStrategy.INVITED -> handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_invited_rooms, 0.1f) { - handleInvitedRoom(realm, it.key, it.value, syncLocalTimeStampMillis) + handleInvitedRoom(realm, it.key, it.value) } is HandlingStrategy.LEFT -> { @@ -134,9 +135,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle if (event.eventId == null || event.stateKey == null) { continue } - val eventEntity = event.toEntity(roomId, SendState.SYNCED).let { - realm.copyToRealmOrUpdate(it) - } + val eventEntity = event.toEntity(roomId, SendState.SYNCED).copyToRealmOrIgnore(realm) CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { eventId = event.eventId root = eventEntity @@ -177,19 +176,27 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle private fun handleInvitedRoom(realm: Realm, roomId: String, - roomSync: InvitedRoomSync, - syncLocalTimestampMillis: Long): RoomEntity { + roomSync: InvitedRoomSync): RoomEntity { Timber.v("Handle invited sync for room $roomId") val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) roomEntity.membership = Membership.INVITE if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) { - val chunkEntity = handleTimelineEvents(realm, roomId, roomEntity, roomSync.inviteState.events, syncLocalTimestampMillis = syncLocalTimestampMillis) - roomEntity.addOrUpdate(chunkEntity) + roomSync.inviteState.events.forEach { + if (it.stateKey == null) { + return@forEach + } + val eventEntity = it.toEntity(roomId, SendState.SYNCED).copyToRealmOrIgnore(realm) + CurrentStateEventEntity.getOrCreate(realm, roomId, it.stateKey, it.type).apply { + eventId = eventEntity.eventId + root = eventEntity + } + roomMemberEventHandler.handle(realm, roomId, it) + } } - val hasRoomMember = roomSync.inviteState?.events?.firstOrNull { + val inviterEvent = roomSync.inviteState?.events?.lastOrNull { it.type == EventType.STATE_ROOM_MEMBER - } != null - roomSummaryUpdater.update(realm, roomId, Membership.INVITE, updateMembers = hasRoomMember) + } + roomSummaryUpdater.update(realm, roomId, Membership.INVITE, updateMembers = true, inviterId = inviterEvent?.senderId) return roomEntity } @@ -197,7 +204,6 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle roomId: String, roomSync: RoomSync): RoomEntity { val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) - roomEntity.membership = Membership.LEAVE roomEntity.chunks.deleteAllFromRealm() roomSummaryUpdater.update(realm, roomId, Membership.LEAVE, roomSync.summary, roomSync.unreadNotifications) @@ -229,9 +235,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle } eventIds.add(event.eventId) val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } - val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).let { - realm.copyToRealmOrUpdate(it) - } + val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm) if (event.isStateEvent() && event.stateKey != null) { CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { eventId = event.eventId diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTaskSequencer.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTaskSequencer.kt index bfa49b7af5..d2d393de00 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTaskSequencer.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTaskSequencer.kt @@ -17,8 +17,8 @@ package im.vector.matrix.android.internal.session.sync import im.vector.matrix.android.internal.session.SessionScope -import im.vector.matrix.android.internal.task.ChannelCoroutineSequencer +import im.vector.matrix.android.internal.task.SemaphoreCoroutineSequencer import javax.inject.Inject @SessionScope -internal class SyncTaskSequencer @Inject constructor() : ChannelCoroutineSequencer() +internal class SyncTaskSequencer @Inject constructor() : SemaphoreCoroutineSequencer() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt index f76c2ff448..c530578538 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt @@ -19,9 +19,12 @@ package im.vector.matrix.android.internal.session.sync import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.pushrules.RuleScope import im.vector.matrix.android.api.pushrules.RuleSetKey +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.toModel import im.vector.matrix.android.api.session.room.model.RoomMemberContent import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.matrix.android.internal.database.mapper.ContentMapper import im.vector.matrix.android.internal.database.mapper.PushRulesMapper import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.BreadcrumbsEntity @@ -29,15 +32,18 @@ import im.vector.matrix.android.internal.database.model.IgnoredUserEntity import im.vector.matrix.android.internal.database.model.PushRulesEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields +import im.vector.matrix.android.internal.database.model.UserAccountDataEntity +import im.vector.matrix.android.internal.database.model.UserAccountDataEntityFields import im.vector.matrix.android.internal.database.query.getDirectRooms import im.vector.matrix.android.internal.database.query.getOrCreate import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataBreadcrumbs import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataDirectMessages -import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataFallback import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataIgnoredUsers import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataPushRules import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataSync @@ -45,6 +51,7 @@ import im.vector.matrix.android.internal.session.user.accountdata.DirectChatsHel import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask import io.realm.Realm import io.realm.RealmList +import io.realm.kotlin.where import timber.log.Timber import javax.inject.Inject @@ -56,21 +63,23 @@ internal class UserAccountDataSyncHandler @Inject constructor( fun handle(realm: Realm, accountData: UserAccountDataSync?) { accountData?.list?.forEach { - when (it) { - is UserAccountDataDirectMessages -> handleDirectChatRooms(realm, it) - is UserAccountDataPushRules -> handlePushRules(realm, it) - is UserAccountDataIgnoredUsers -> handleIgnoredUsers(realm, it) - is UserAccountDataBreadcrumbs -> handleBreadcrumbs(realm, it) - is UserAccountDataFallback -> Timber.d("Receive account data of unhandled type ${it.type}") - else -> error("Missing code here!") + // Generic handling, just save in base + handleGenericAccountData(realm, it.type, it.content) + + // Didn't want to break too much thing, so i re-serialize to jsonString before reparsing + // TODO would be better to have a mapper? + val toJson = MoshiProvider.providesMoshi().adapter(Event::class.java).toJson(it) + val model = toJson?.let { json -> + MoshiProvider.providesMoshi().adapter(UserAccountData::class.java).fromJson(json) + } + // Specific parsing + when (model) { + is UserAccountDataDirectMessages -> handleDirectChatRooms(realm, model) + is UserAccountDataPushRules -> handlePushRules(realm, model) + is UserAccountDataIgnoredUsers -> handleIgnoredUsers(realm, model) + is UserAccountDataBreadcrumbs -> handleBreadcrumbs(realm, model) } } - - // TODO Store all account data, app can be interested of it - // accountData?.list?.forEach { - // it.toString() - // MoshiProvider.providesMoshi() - // } } // If we get some direct chat invites, we synchronize the user account data including those. @@ -200,4 +209,19 @@ internal class UserAccountDataSyncHandler @Inject constructor( ?.breadcrumbsIndex = index } } + + fun handleGenericAccountData(realm: Realm, type: String, content: Content?) { + val existing = realm.where() + .equalTo(UserAccountDataEntityFields.TYPE, type) + .findFirst() + if (existing != null) { + // Update current value + existing.contentStr = ContentMapper.map(content) + } else { + realm.createObject(UserAccountDataEntity::class.java).let { accountDataEntity -> + accountDataEntity.type = type + accountDataEntity.contentStr = ContentMapper.map(content) + } + } + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/GroupSyncProfile.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/GroupSyncProfile.kt index 6d31e84d61..00e3377aa9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/GroupSyncProfile.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/GroupSyncProfile.kt @@ -24,10 +24,10 @@ internal data class GroupSyncProfile( /** * The name of the group, if any. May be nil. */ - @Json(name = "name") var name: String? = null, + @Json(name = "name") val name: String? = null, /** * The URL for the group's avatar. May be nil. */ - @Json(name = "avatar_url") var avatarUrl: String? = null + @Json(name = "avatar_url") val avatarUrl: String? = null ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt index accc9c900f..c508413665 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt @@ -17,8 +17,9 @@ package im.vector.matrix.android.internal.session.sync.model.accountdata import com.squareup.moshi.Json +import im.vector.matrix.android.internal.session.user.accountdata.AccountDataContent -internal abstract class UserAccountData { +abstract class UserAccountData : AccountDataContent { @Json(name = "type") abstract val type: String diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataFallback.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataEvent.kt similarity index 95% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataFallback.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataEvent.kt index a8b8235d37..a4ba0fc91a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataFallback.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataEvent.kt @@ -20,7 +20,7 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -internal data class UserAccountDataFallback( +data class UserAccountDataEvent( @Json(name = "type") override val type: String, @Json(name = "content") val content: Map ) : UserAccountData() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataSync.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataSync.kt index c7f8bfa4c2..8acac86e1a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataSync.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataSync.kt @@ -18,8 +18,9 @@ package im.vector.matrix.android.internal.session.sync.model.accountdata import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.events.model.Event @JsonClass(generateAdapter = true) internal data class UserAccountDataSync( - @Json(name = "events") val list: List = emptyList() + @Json(name = "events") val list: List = emptyList() ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataContent.kt new file mode 100644 index 0000000000..5335f8a7f1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataContent.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 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.session.user.accountdata + +/** + * Tag class to identify every account data content + */ +internal interface AccountDataContent diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DefaultAccountDataService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DefaultAccountDataService.kt new file mode 100644 index 0000000000..7756b22510 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DefaultAccountDataService.kt @@ -0,0 +1,114 @@ +/* + * Copyright (c) 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.session.user.accountdata + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.accountdata.AccountDataService +import im.vector.matrix.android.api.session.events.model.Content +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.api.util.JSON_DICT_PARAMETERIZED_TYPE +import im.vector.matrix.android.api.util.Optional +import im.vector.matrix.android.api.util.toOptional +import im.vector.matrix.android.internal.database.model.UserAccountDataEntity +import im.vector.matrix.android.internal.database.model.UserAccountDataEntityFields +import im.vector.matrix.android.internal.di.MoshiProvider +import im.vector.matrix.android.internal.session.sync.UserAccountDataSyncHandler +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith +import javax.inject.Inject + +internal class DefaultAccountDataService @Inject constructor( + private val monarchy: Monarchy, + private val updateUserAccountDataTask: UpdateUserAccountDataTask, + private val userAccountDataSyncHandler: UserAccountDataSyncHandler, + private val taskExecutor: TaskExecutor +) : AccountDataService { + + private val moshi = MoshiProvider.providesMoshi() + private val adapter = moshi.adapter>(JSON_DICT_PARAMETERIZED_TYPE) + + override fun getAccountDataEvent(type: String): UserAccountDataEvent? { + return getAccountDataEvents(setOf(type)).firstOrNull() + } + + override fun getLiveAccountDataEvent(type: String): LiveData> { + return Transformations.map(getLiveAccountDataEvents(setOf(type))) { + it.firstOrNull()?.toOptional() + } + } + + override fun getAccountDataEvents(types: Set): List { + return monarchy.fetchAllCopiedSync { realm -> + realm.where(UserAccountDataEntity::class.java) + .apply { + if (types.isNotEmpty()) { + `in`(UserAccountDataEntityFields.TYPE, types.toTypedArray()) + } + } + }.mapNotNull { entity -> + entity.type?.let { type -> + UserAccountDataEvent( + type = type, + content = entity.contentStr?.let { adapter.fromJson(it) } ?: emptyMap() + ) + } + } + } + + override fun getLiveAccountDataEvents(types: Set): LiveData> { + return monarchy.findAllMappedWithChanges({ realm -> + realm.where(UserAccountDataEntity::class.java) + .apply { + if (types.isNotEmpty()) { + `in`(UserAccountDataEntityFields.TYPE, types.toTypedArray()) + } + } + }, { entity -> + UserAccountDataEvent( + type = entity.type ?: "", + content = entity.contentStr?.let { adapter.fromJson(it) } ?: emptyMap() + ) + }) + } + + override fun updateAccountData(type: String, content: Content, callback: MatrixCallback?): Cancelable { + return updateUserAccountDataTask.configureWith(UpdateUserAccountDataTask.AnyParams( + type = type, + any = content + )) { + this.retryCount = 5 + this.callback = object : MatrixCallback { + override fun onSuccess(data: Unit) { + // TODO Move that to the task (but it created a circular dependencies...) + monarchy.runTransactionSync { realm -> + userAccountDataSyncHandler.handleGenericAccountData(realm, type, content) + } + callback?.onSuccess(data) + } + + override fun onFailure(failure: Throwable) { + callback?.onFailure(failure) + } + } + } + .executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt index 068ce4777a..beb3a0fcc0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt @@ -49,6 +49,14 @@ internal interface UpdateUserAccountDataTask : Task { +internal interface CoroutineSequencer { /** * @param block the suspendable block to execute * @return the result of the block */ - suspend fun post(block: suspend () -> T): T - - /** - * Cancel all and close, so you won't be able to post anything else after - */ - fun close() + suspend fun post(block: suspend () -> T): T } -internal open class ChannelCoroutineSequencer : CoroutineSequencer { +internal open class SemaphoreCoroutineSequencer : CoroutineSequencer { - private data class Message( - val block: suspend () -> T, - val deferred: CompletableDeferred - ) + // Permits 1 suspend function at a time. + private val semaphore = Semaphore(1) - private var messageChannel: Channel> = Channel() - private val coroutineScope = CoroutineScope(SupervisorJob()) - // This will ensure - private val singleDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() - - init { - launchCoroutine() - } - - private fun launchCoroutine() { - coroutineScope.launch(singleDispatcher) { - for (message in messageChannel) { - try { - val result = message.block() - message.deferred.complete(result) - } catch (exception: Throwable) { - message.deferred.completeExceptionally(exception) - } - } - } - } - - override fun close() { - coroutineScope.coroutineContext.cancelChildren() - messageChannel.close() - } - - override suspend fun post(block: suspend () -> T): T { - val deferred = CompletableDeferred() - val message = Message(block, deferred) - messageChannel.send(message) - return try { - deferred.await() - } catch (cancellation: CancellationException) { - // In case of cancellation, we stop the current coroutine context - // and relaunch one to consume next messages - coroutineScope.coroutineContext.cancelChildren() - launchCoroutine() - throw cancellation + override suspend fun post(block: suspend () -> T): T { + return semaphore.withPermit { + block() } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CancelableWork.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CancelableWork.kt index cba104ebe8..e1a21ff8c5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CancelableWork.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CancelableWork.kt @@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.util import androidx.work.WorkManager import im.vector.matrix.android.api.util.Cancelable -import java.util.* +import java.util.UUID internal class CancelableWork(private val workManager: WorkManager, private val workId: UUID) : Cancelable { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CompatUtil.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CompatUtil.kt index 2df2bd2bf2..fa97d6a9eb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CompatUtil.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CompatUtil.kt @@ -32,13 +32,28 @@ import java.io.IOException import java.io.InputStream import java.io.OutputStream import java.math.BigInteger -import java.security.* +import java.security.InvalidAlgorithmParameterException +import java.security.InvalidKeyException +import java.security.KeyPairGenerator +import java.security.KeyStore +import java.security.KeyStoreException +import java.security.NoSuchAlgorithmException +import java.security.NoSuchProviderException +import java.security.PrivateKey +import java.security.SecureRandom +import java.security.UnrecoverableKeyException import java.security.cert.CertificateException import java.security.spec.AlgorithmParameterSpec import java.security.spec.RSAKeyGenParameterSpec import java.util.Calendar import java.util.zip.GZIPOutputStream -import javax.crypto.* +import javax.crypto.Cipher +import javax.crypto.CipherInputStream +import javax.crypto.CipherOutputStream +import javax.crypto.IllegalBlockSizeException +import javax.crypto.KeyGenerator +import javax.crypto.NoSuchPaddingException +import javax.crypto.SecretKey import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Exhaustive.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Exhaustive.kt new file mode 100644 index 0000000000..8f6beea92d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Exhaustive.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) 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.util + +// Trick to ensure that when block is exhaustive +internal val T.exhaustive: T get() = this diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushRuleActionsTest.kt b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushRuleActionsTest.kt index 17543e9d25..1d5d6a4d19 100644 --- a/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushRuleActionsTest.kt +++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushRuleActionsTest.kt @@ -18,7 +18,9 @@ package im.vector.matrix.android.api.pushrules import im.vector.matrix.android.api.pushrules.rest.PushRule import im.vector.matrix.android.internal.di.MoshiProvider -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue import org.junit.Test class PushRuleActionsTest { diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/crypto/keysbackup/util/RecoveryKeyTest.kt b/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/crypto/keysbackup/util/RecoveryKeyTest.kt index 47a2aa08df..2566275952 100644 --- a/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/crypto/keysbackup/util/RecoveryKeyTest.kt +++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/crypto/keysbackup/util/RecoveryKeyTest.kt @@ -16,7 +16,10 @@ package im.vector.matrix.android.internal.crypto.keysbackup.util -import org.junit.Assert.* +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Test class RecoveryKeyTest { diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/crypto/verification/qrcode/BinaryStringTest.kt b/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/crypto/verification/qrcode/BinaryStringTest.kt new file mode 100644 index 0000000000..df5499cb6f --- /dev/null +++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/crypto/verification/qrcode/BinaryStringTest.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 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.crypto.verification.qrcode + +import org.amshove.kluent.shouldEqualTo +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runners.MethodSorters + +@FixMethodOrder(MethodSorters.JVM) +class BinaryStringTest { + + /** + * I want to put bytes to a String, and vice versa + */ + @Test + fun testNominalCase() { + val byteArray = ByteArray(256) + for (i in byteArray.indices) { + byteArray[i] = i.toByte() // Random.nextInt(255).toByte() + } + + val str = byteArray.toString(Charsets.ISO_8859_1) + + str.length shouldEqualTo 256 + + // Ok convert back to bytearray + + val result = str.toByteArray(Charsets.ISO_8859_1) + + result.size shouldEqualTo 256 + + for (i in 0..255) { + result[i] shouldEqualTo i.toByte() + result[i] shouldEqualTo byteArray[i] + } + } +} diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeTest.kt b/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeTest.kt deleted file mode 100644 index 2dd0649be1..0000000000 --- a/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeTest.kt +++ /dev/null @@ -1,246 +0,0 @@ -/* - * 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.crypto.verification.qrcode - -import org.amshove.kluent.shouldBe -import org.amshove.kluent.shouldBeEqualTo -import org.amshove.kluent.shouldBeNull -import org.amshove.kluent.shouldNotBeNull -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runners.MethodSorters - -@Suppress("SpellCheckingInspection") -@FixMethodOrder(MethodSorters.JVM) -class QrCodeTest { - - private val basicQrCodeData = QrCodeData( - userId = "@benoit:matrix.org", - requestId = "\$azertyazerty", - action = QrCodeData.ACTION_VERIFY, - keys = mapOf( - "1" to "abcdef", - "2" to "ghijql" - ), - sharedSecret = "sharedSecret", - otherUserKey = "otherUserKey", - otherDeviceKey = "otherDeviceKey" - ) - - private val basicUrl = "https://matrix.to/#/@benoit:matrix.org" + - "?request=%24azertyazerty" + - "&action=verify" + - "&key_1=abcdef" + - "&key_2=ghijql" + - "&secret=sharedSecret" + - "&other_user_key=otherUserKey" + - "&other_device_key=otherDeviceKey" - - @Test - fun testNominalCase() { - val url = basicQrCodeData.toUrl() - - url shouldBeEqualTo basicUrl - - val decodedData = url.toQrCodeData() - - decodedData.shouldNotBeNull() - - decodedData.userId shouldBeEqualTo "@benoit:matrix.org" - decodedData.requestId shouldBeEqualTo "\$azertyazerty" - decodedData.keys["1"]?.shouldBeEqualTo("abcdef") - decodedData.keys["2"]?.shouldBeEqualTo("ghijql") - decodedData.sharedSecret shouldBeEqualTo "sharedSecret" - decodedData.otherUserKey?.shouldBeEqualTo("otherUserKey") - decodedData.otherDeviceKey?.shouldBeEqualTo("otherDeviceKey") - } - - @Test - fun testSlashCase() { - val url = basicQrCodeData - .copy( - userId = "@benoit/foo:matrix.org", - requestId = "\$azertyazerty/bar" - ) - .toUrl() - - url shouldBeEqualTo basicUrl - .replace("@benoit", "@benoit%2Ffoo") - .replace("azertyazerty", "azertyazerty%2Fbar") - - val decodedData = url.toQrCodeData() - - decodedData.shouldNotBeNull() - - decodedData.userId shouldBeEqualTo "@benoit/foo:matrix.org" - decodedData.requestId shouldBeEqualTo "\$azertyazerty/bar" - decodedData.keys["1"]?.shouldBeEqualTo("abcdef") - decodedData.keys["2"]?.shouldBeEqualTo("ghijql") - decodedData.sharedSecret shouldBeEqualTo "sharedSecret" - decodedData.otherUserKey!! shouldBeEqualTo "otherUserKey" - decodedData.otherDeviceKey!! shouldBeEqualTo "otherDeviceKey" - } - - @Test - fun testNoOtherUserKey() { - val url = basicQrCodeData - .copy( - otherUserKey = null - ) - .toUrl() - - url shouldBeEqualTo basicUrl - .replace("&other_user_key=otherUserKey", "") - - val decodedData = url.toQrCodeData() - - decodedData.shouldNotBeNull() - - decodedData.userId shouldBeEqualTo "@benoit:matrix.org" - decodedData.requestId shouldBeEqualTo "\$azertyazerty" - decodedData.keys["1"]?.shouldBeEqualTo("abcdef") - decodedData.keys["2"]?.shouldBeEqualTo("ghijql") - decodedData.sharedSecret shouldBeEqualTo "sharedSecret" - decodedData.otherUserKey shouldBe null - decodedData.otherDeviceKey?.shouldBeEqualTo("otherDeviceKey") - } - - @Test - fun testNoOtherDeviceKey() { - val url = basicQrCodeData - .copy( - otherDeviceKey = null - ) - .toUrl() - - url shouldBeEqualTo basicUrl - .replace("&other_device_key=otherDeviceKey", "") - - val decodedData = url.toQrCodeData() - - decodedData.shouldNotBeNull() - - decodedData.userId shouldBeEqualTo "@benoit:matrix.org" - decodedData.requestId shouldBeEqualTo "\$azertyazerty" - decodedData.keys["1"]?.shouldBeEqualTo("abcdef") - decodedData.keys["2"]?.shouldBeEqualTo("ghijql") - decodedData.sharedSecret shouldBeEqualTo "sharedSecret" - decodedData.otherUserKey?.shouldBeEqualTo("otherUserKey") - decodedData.otherDeviceKey shouldBe null - } - - @Test - fun testUrlCharInKeys() { - val url = basicQrCodeData - .copy( - keys = mapOf( - "/=" to "abcdef", - "&?" to "ghijql" - ) - ) - .toUrl() - - url shouldBeEqualTo basicUrl - .replace("key_1=abcdef", "key_%2F%3D=abcdef") - .replace("key_2=ghijql", "key_%26%3F=ghijql") - - val decodedData = url.toQrCodeData() - - decodedData.shouldNotBeNull() - - decodedData.keys["/="]?.shouldBeEqualTo("abcdef") - decodedData.keys["&&"]?.shouldBeEqualTo("ghijql") - } - - @Test - fun testMissingActionCase() { - basicUrl.replace("&action=verify", "") - .toQrCodeData() - .shouldBeNull() - } - - @Test - fun testEmptyActionCase() { - basicUrl.replace("&action=verify", "&action=") - .toQrCodeData() - .shouldBeNull() - } - - @Test - fun testOtherActionCase() { - basicUrl.replace("&action=verify", "&action=confirm") - .toQrCodeData() - ?.action - ?.shouldBeEqualTo("confirm") - } - - @Test - fun testMissingRequestId() { - basicUrl.replace("request=%24azertyazerty", "") - .toQrCodeData() - .shouldBeNull() - } - - @Test - fun testEmptyRequestId() { - basicUrl.replace("request=%24azertyazerty", "request=") - .toQrCodeData() - .shouldBeNull() - } - - @Test - fun testMissingUserId() { - basicUrl.replace("@benoit:matrix.org", "") - .toQrCodeData() - .shouldBeNull() - } - - @Test - fun testBadUserId() { - basicUrl.replace("@benoit:matrix.org", "@benoit") - .toQrCodeData() - .shouldBeNull() - } - - @Test - fun testMissingSecret() { - basicUrl.replace("&secret=sharedSecret", "") - .toQrCodeData() - .shouldBeNull() - } - - @Test - fun testEmptySecret() { - basicUrl.replace("&secret=sharedSecret", "&secret=") - .toQrCodeData() - .shouldBeNull() - } - - @Test - fun testSelfSigning() { - // request is not an eventId in this case - val url = "https://matrix.to/#/@benoit0815:matrix.org" + - "?request=local.4dff40e1-7bf1-4e80-81ed-c6090d43bf20" + - "&action=verify" + - "&key_utbSRFcFjFDYf0KcNv3FoBHFSbvUPXtCYutuOg6WQ%2Bs=utbSRFcFjFDYf0KcNv3FoBHFSbvUPXtCYutuOg6WQ%2Bs" + - "&key_YSOXZVBXIZ=F0XWqgUePgwm5HMYG3yhBNneHmscrAxxlooLHjy8YQc" + - "&secret=LYVcEQmfdorbJ3vbQnq7nbNZc%2BGmDxUen1rByV9hRM4" + - "&other_device_key=eGoUqZqAroCYpjp7FLGIkTEzYHBFED4uUAfJ267gqQQ" - - url.toQrCodeData()!!.requestId shouldBeEqualTo "local.4dff40e1-7bf1-4e80-81ed-c6090d43bf20" - } -} diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/task/CoroutineSequencersTest.kt b/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/task/CoroutineSequencersTest.kt index 9591feaa32..a70713c44b 100644 --- a/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/task/CoroutineSequencersTest.kt +++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/task/CoroutineSequencersTest.kt @@ -32,7 +32,7 @@ class CoroutineSequencersTest { @Test fun sequencer_should_run_sequential() { - val sequencer = ChannelCoroutineSequencer() + val sequencer = SemaphoreCoroutineSequencer() val results = ArrayList() val jobs = listOf( @@ -63,9 +63,9 @@ class CoroutineSequencersTest { @Test fun sequencer_should_run_parallel() { - val sequencer1 = ChannelCoroutineSequencer() - val sequencer2 = ChannelCoroutineSequencer() - val sequencer3 = ChannelCoroutineSequencer() + val sequencer1 = SemaphoreCoroutineSequencer() + val sequencer2 = SemaphoreCoroutineSequencer() + val sequencer3 = SemaphoreCoroutineSequencer() val results = ArrayList() val jobs = listOf( GlobalScope.launch(dispatcher) { @@ -92,7 +92,7 @@ class CoroutineSequencersTest { @Test fun sequencer_should_jump_to_next_when_current_job_canceled() { - val sequencer = ChannelCoroutineSequencer() + val sequencer = SemaphoreCoroutineSequencer() val results = ArrayList() val jobs = listOf( GlobalScope.launch(dispatcher) { diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index 5c5b600828..a0bd725118 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -67,9 +67,10 @@ DO NOT COMMIT ### invalid formatting \s{8}/\*\n \* -[^\w]if\( -while\( -for\( +# Now checked by ktlint +# [^\w]if\( +# while\( +# for\( # Add space after // # DISABLED To re-enable when code will be formatted globally diff --git a/vector/build.gradle b/vector/build.gradle index 0517482904..483cd53831 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -15,7 +15,7 @@ androidExtensions { } ext.versionMajor = 0 -ext.versionMinor = 16 +ext.versionMinor = 17 ext.versionPatch = 0 static def getGitTimestamp() { @@ -341,6 +341,7 @@ dependencies { implementation "com.github.bumptech.glide:glide:$glide_version" kapt "com.github.bumptech.glide:compiler:$glide_version" implementation 'com.danikula:videocache:2.7.1' + implementation 'com.github.yalantis:ucrop:2.2.4' // Badge for compatibility implementation 'me.leolin:ShortcutBadger:1.1.22@aar' @@ -368,6 +369,8 @@ dependencies { implementation "androidx.emoji:emoji-appcompat:1.0.0" + implementation 'com.github.BillCarsonFr:JsonViewer:0.4' + // QR-code // Stick to 3.3.3 because of https://github.com/zxing/zxing/issues/1170 implementation 'com.google.zxing:core:3.3.3' @@ -375,8 +378,11 @@ dependencies { // TESTS testImplementation 'junit:junit:4.12' + testImplementation 'org.amshove.kluent:kluent-android:1.44' + androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + androidTestImplementation 'org.amshove.kluent:kluent-android:1.44' } if (getGradle().getStartParameter().getTaskRequests().toString().contains("Gplay")) { diff --git a/vector/src/debug/java/im/vector/riotx/features/debug/DebugMenuActivity.kt b/vector/src/debug/java/im/vector/riotx/features/debug/DebugMenuActivity.kt index 81708182b7..c0040f3e42 100644 --- a/vector/src/debug/java/im/vector/riotx/features/debug/DebugMenuActivity.kt +++ b/vector/src/debug/java/im/vector/riotx/features/debug/DebugMenuActivity.kt @@ -25,6 +25,7 @@ import android.os.Build import androidx.core.app.NotificationCompat import androidx.core.app.Person import butterknife.OnClick +import im.vector.matrix.android.internal.crypto.verification.qrcode.toQrCodeData import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.ScreenComponent @@ -37,6 +38,7 @@ import im.vector.riotx.core.utils.toast import im.vector.riotx.features.debug.sas.DebugSasEmojiActivity import im.vector.riotx.features.qrcode.QrCodeScannerActivity import kotlinx.android.synthetic.debug.activity_debug_menu.* +import timber.log.Timber import javax.inject.Inject class DebugMenuActivity : VectorBaseActivity() { @@ -50,8 +52,19 @@ class DebugMenuActivity : VectorBaseActivity() { injector.inject(this) } + private lateinit var buffer: ByteArray + override fun initUiAndData() { - renderQrCode("https://www.example.org") + // renderQrCode("https://www.example.org") + + buffer = ByteArray(256) + for (i in buffer.indices) { + buffer[i] = i.toByte() + } + + val string = buffer.toString(Charsets.ISO_8859_1) + + renderQrCode(string) } private fun renderQrCode(text: String) { @@ -194,7 +207,23 @@ class DebugMenuActivity : VectorBaseActivity() { toast("QrCode: " + QrCodeScannerActivity.getResultText(data) + " is QRCode: " + QrCodeScannerActivity.getResultIsQrCode(data)) // Also update the current QR Code (reverse operation) - renderQrCode(QrCodeScannerActivity.getResultText(data) ?: "") + // renderQrCode(QrCodeScannerActivity.getResultText(data) ?: "") + val result = QrCodeScannerActivity.getResultText(data)!! + + val qrCodeData = result.toQrCodeData() + Timber.e("qrCodeData: $qrCodeData") + + if (result.length != buffer.size) { + Timber.e("Error, length are not the same") + } else { + // Convert to ByteArray + val byteArrayResult = result.toByteArray(Charsets.ISO_8859_1) + for (i in byteArrayResult.indices) { + if (buffer[i] != byteArrayResult[i]) { + Timber.e("Error for byte $i, expecting ${buffer[i]} and get ${byteArrayResult[i]}") + } + } + } } } } diff --git a/vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiController.kt b/vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiController.kt index 6804828b20..5629497de8 100644 --- a/vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiController.kt +++ b/vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiController.kt @@ -17,7 +17,7 @@ package im.vector.riotx.features.debug.sas import com.airbnb.epoxy.TypedEpoxyController -import im.vector.matrix.android.api.session.crypto.sas.EmojiRepresentation +import im.vector.matrix.android.api.session.crypto.verification.EmojiRepresentation data class SasState( val emojiList: List diff --git a/vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiItem.kt b/vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiItem.kt index cf35873f6b..7403ead43c 100644 --- a/vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiItem.kt +++ b/vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiItem.kt @@ -20,7 +20,7 @@ import android.annotation.SuppressLint import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import im.vector.matrix.android.api.session.crypto.sas.EmojiRepresentation +import im.vector.matrix.android.api.session.crypto.verification.EmojiRepresentation import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel diff --git a/vector/src/debug/java/im/vector/riotx/receivers/DebugReceiver.kt b/vector/src/debug/java/im/vector/riotx/receivers/DebugReceiver.kt index 933ee3fb14..887fba364c 100644 --- a/vector/src/debug/java/im/vector/riotx/receivers/DebugReceiver.kt +++ b/vector/src/debug/java/im/vector/riotx/receivers/DebugReceiver.kt @@ -16,7 +16,11 @@ package im.vector.riotx.receivers -import android.content.* +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.SharedPreferences import android.preference.PreferenceManager import androidx.core.content.edit import im.vector.riotx.core.utils.lsFiles @@ -32,8 +36,8 @@ class DebugReceiver : BroadcastReceiver() { intent.action?.let { when { - it.endsWith(DEBUG_ACTION_DUMP_FILESYSTEM) -> lsFiles(context) - it.endsWith(DEBUG_ACTION_DUMP_PREFERENCES) -> dumpPreferences(context) + it.endsWith(DEBUG_ACTION_DUMP_FILESYSTEM) -> lsFiles(context) + it.endsWith(DEBUG_ACTION_DUMP_PREFERENCES) -> dumpPreferences(context) it.endsWith(DEBUG_ACTION_ALTER_SCALAR_TOKEN) -> alterScalarToken(context) } } diff --git a/vector/src/fdroid/java/im/vector/riotx/fdroid/receiver/AlarmSyncBroadcastReceiver.kt b/vector/src/fdroid/java/im/vector/riotx/fdroid/receiver/AlarmSyncBroadcastReceiver.kt index cf30e2daa2..8c9cdbf63b 100644 --- a/vector/src/fdroid/java/im/vector/riotx/fdroid/receiver/AlarmSyncBroadcastReceiver.kt +++ b/vector/src/fdroid/java/im/vector/riotx/fdroid/receiver/AlarmSyncBroadcastReceiver.kt @@ -48,7 +48,7 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() { } } - val sessionId = intent.getStringExtra(SyncService.EXTRA_SESSION_ID) + val sessionId = intent.getStringExtra(SyncService.EXTRA_SESSION_ID) ?: return // This method is called when the BroadcastReceiver is receiving an Intent broadcast. Timber.d("RestartBroadcastReceiver received intent") VectorSyncService.newIntent(context, sessionId).let { diff --git a/vector/src/fdroid/java/im/vector/riotx/push/fcm/NotificationTroubleshootTestManagerFactory.kt b/vector/src/fdroid/java/im/vector/riotx/push/fcm/NotificationTroubleshootTestManagerFactory.kt index 25c06d5b6f..723a0b9327 100644 --- a/vector/src/fdroid/java/im/vector/riotx/push/fcm/NotificationTroubleshootTestManagerFactory.kt +++ b/vector/src/fdroid/java/im/vector/riotx/push/fcm/NotificationTroubleshootTestManagerFactory.kt @@ -18,7 +18,11 @@ package im.vector.riotx.push.fcm import androidx.fragment.app.Fragment import im.vector.riotx.fdroid.features.settings.troubleshoot.TestAutoStartBoot import im.vector.riotx.fdroid.features.settings.troubleshoot.TestBackgroundRestrictions -import im.vector.riotx.features.settings.troubleshoot.* +import im.vector.riotx.features.settings.troubleshoot.NotificationTroubleshootTestManager +import im.vector.riotx.features.settings.troubleshoot.TestAccountSettings +import im.vector.riotx.features.settings.troubleshoot.TestBingRulesSettings +import im.vector.riotx.features.settings.troubleshoot.TestDeviceSettings +import im.vector.riotx.features.settings.troubleshoot.TestSystemSettings import javax.inject.Inject class NotificationTroubleshootTestManagerFactory @Inject constructor(private val testSystemSettings: TestSystemSettings, diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 3207ab257a..488f357c4b 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -40,6 +40,7 @@ android:name=".features.login.LoginActivity" android:windowSoftInputMode="adjustResize" /> + @@ -88,7 +89,13 @@ - + + + @@ -134,6 +141,15 @@ + + + + + @@ -314,6 +315,11 @@ SOFTWARE.
Copyright (c) 2012-2016 Dan Wheeler and Dropbox, Inc. +
  • + Compressor +
    + Copyright (c) 2016 Zetra. +
  • com.otaliastudios:autocomplete
    @@ -374,6 +380,14 @@ SOFTWARE.
    Copyright (c) 2014 Dushyanth Maguluru
  • +
  • + uCrop +
    + Copyright 2017, Yalantis +
  • +
  • + BillCarsonFr/JsonViewer +
  •  Apache License
    diff --git a/vector/src/main/java/im/vector/riotx/AppStateHandler.kt b/vector/src/main/java/im/vector/riotx/AppStateHandler.kt
    index 9a281e5728..cf700cded8 100644
    --- a/vector/src/main/java/im/vector/riotx/AppStateHandler.kt
    +++ b/vector/src/main/java/im/vector/riotx/AppStateHandler.kt
    @@ -24,9 +24,9 @@ import im.vector.matrix.android.api.session.group.model.GroupSummary
     import im.vector.matrix.android.api.session.room.model.RoomSummary
     import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
     import im.vector.matrix.rx.rx
    -import im.vector.riotx.features.home.HomeRoomListDataSource
     import im.vector.riotx.features.grouplist.ALL_COMMUNITIES_GROUP_ID
     import im.vector.riotx.features.grouplist.SelectedGroupDataSource
    +import im.vector.riotx.features.home.HomeRoomListDataSource
     import im.vector.riotx.features.home.room.list.ChronologicalRoomComparator
     import io.reactivex.Observable
     import io.reactivex.android.schedulers.AndroidSchedulers
    diff --git a/vector/src/main/java/im/vector/riotx/VectorApplication.kt b/vector/src/main/java/im/vector/riotx/VectorApplication.kt
    index c76af027ba..81cf1402b0 100644
    --- a/vector/src/main/java/im/vector/riotx/VectorApplication.kt
    +++ b/vector/src/main/java/im/vector/riotx/VectorApplication.kt
    @@ -55,7 +55,8 @@ import im.vector.riotx.features.version.VersionProvider
     import im.vector.riotx.push.fcm.FcmHelper
     import timber.log.Timber
     import java.text.SimpleDateFormat
    -import java.util.*
    +import java.util.Date
    +import java.util.Locale
     import javax.inject.Inject
     
     class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.Provider, androidx.work.Configuration.Provider {
    diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
    index 6031a9f6cf..d7e89a62f6 100644
    --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
    @@ -22,6 +22,7 @@ import androidx.fragment.app.FragmentFactory
     import dagger.Binds
     import dagger.Module
     import dagger.multibindings.IntoMap
    +import im.vector.riotx.features.attachments.preview.AttachmentsPreviewFragment
     import im.vector.riotx.features.createdirect.CreateDirectRoomDirectoryUsersFragment
     import im.vector.riotx.features.createdirect.CreateDirectRoomKnownUsersFragment
     import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment
    @@ -72,8 +73,10 @@ import im.vector.riotx.features.settings.VectorSettingsPreferencesFragment
     import im.vector.riotx.features.settings.VectorSettingsSecurityPrivacyFragment
     import im.vector.riotx.features.settings.crosssigning.CrossSigningSettingsFragment
     import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment
    +import im.vector.riotx.features.settings.devtools.AccountDataFragment
     import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment
     import im.vector.riotx.features.settings.push.PushGatewaysFragment
    +import im.vector.riotx.features.share.IncomingShareFragment
     import im.vector.riotx.features.signout.soft.SoftLogoutFragment
     
     @Module
    @@ -348,4 +351,19 @@ interface FragmentModule {
         @IntoMap
         @FragmentKey(CrossSigningSettingsFragment::class)
         fun bindCrossSigningSettingsFragment(fragment: CrossSigningSettingsFragment): Fragment
    +
    +    @Binds
    +    @IntoMap
    +    @FragmentKey(AttachmentsPreviewFragment::class)
    +    fun bindAttachmentsPreviewFragment(fragment: AttachmentsPreviewFragment): Fragment
    +
    +    @Binds
    +    @IntoMap
    +    @FragmentKey(IncomingShareFragment::class)
    +    fun bindIncomingShareFragment(fragment: IncomingShareFragment): Fragment
    +
    +    @Binds
    +    @IntoMap
    +    @FragmentKey(AccountDataFragment::class)
    +    fun bindAccountDataFragment(fragment: AccountDataFragment): Fragment
     }
    diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt
    index f03f6cb784..5cd54c6c2b 100644
    --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt
    @@ -26,6 +26,7 @@ import im.vector.riotx.core.preference.UserAvatarPreference
     import im.vector.riotx.features.MainActivity
     import im.vector.riotx.features.createdirect.CreateDirectRoomActivity
     import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
    +import im.vector.riotx.features.crypto.quads.SharedSecureStorageActivity
     import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
     import im.vector.riotx.features.debug.DebugMenuActivity
     import im.vector.riotx.features.home.HomeActivity
    @@ -40,6 +41,7 @@ import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsBotto
     import im.vector.riotx.features.invite.VectorInviteView
     import im.vector.riotx.features.link.LinkHandlerActivity
     import im.vector.riotx.features.login.LoginActivity
    +import im.vector.riotx.features.media.BigImageViewerActivity
     import im.vector.riotx.features.media.ImageMediaViewerActivity
     import im.vector.riotx.features.media.VideoMediaViewerActivity
     import im.vector.riotx.features.navigation.Navigator
    @@ -75,81 +77,69 @@ import im.vector.riotx.features.ui.UiStateRepository
     @ScreenScope
     interface ScreenComponent {
     
    +    /* ==========================================================================================
    +     * Shortcut to VectorComponent elements
    +     * ========================================================================================== */
    +
         fun activeSessionHolder(): ActiveSessionHolder
    -
         fun fragmentFactory(): FragmentFactory
    -
         fun viewModelFactory(): ViewModelProvider.Factory
    -
         fun bugReporter(): BugReporter
    -
         fun rageShake(): RageShake
    -
         fun navigator(): Navigator
    -
         fun errorFormatter(): ErrorFormatter
    -
         fun uiStateRepository(): UiStateRepository
     
    +    /* ==========================================================================================
    +     * Activities
    +     * ========================================================================================== */
    +
         fun inject(activity: HomeActivity)
    -
    -    fun inject(messageActionsBottomSheet: MessageActionsBottomSheet)
    -
    -    fun inject(viewReactionsBottomSheet: ViewReactionsBottomSheet)
    -
    -    fun inject(viewEditHistoryBottomSheet: ViewEditHistoryBottomSheet)
    -
    -    fun inject(vectorSettingsActivity: VectorSettingsActivity)
    -
    -    fun inject(keysBackupManageActivity: KeysBackupManageActivity)
    -
    -    fun inject(emojiReactionPickerActivity: EmojiReactionPickerActivity)
    -
    -    fun inject(loginActivity: LoginActivity)
    -
    -    fun inject(linkHandlerActivity: LinkHandlerActivity)
    -
    -    fun inject(mainActivity: MainActivity)
    -
    -    fun inject(roomDirectoryActivity: RoomDirectoryActivity)
    -
    -    fun inject(bugReportActivity: BugReportActivity)
    -
    -    fun inject(imageMediaViewerActivity: ImageMediaViewerActivity)
    -
    -    fun inject(filteredRoomsActivity: FilteredRoomsActivity)
    -
    -    fun inject(createRoomActivity: CreateRoomActivity)
    -
    -    fun inject(vectorInviteView: VectorInviteView)
    -
    -    fun inject(videoMediaViewerActivity: VideoMediaViewerActivity)
    -
    -    fun inject(userAvatarPreference: UserAvatarPreference)
    -
    -    fun inject(createDirectRoomActivity: CreateDirectRoomActivity)
    -
    -    fun inject(displayReadReceiptsBottomSheet: DisplayReadReceiptsBottomSheet)
    -
    -    fun inject(reactionButton: ReactionButton)
    -
    -    fun inject(incomingShareActivity: IncomingShareActivity)
    -
    -    fun inject(roomListActionsBottomSheet: RoomListQuickActionsBottomSheet)
    -
    +    fun inject(activity: VectorSettingsActivity)
    +    fun inject(activity: KeysBackupManageActivity)
    +    fun inject(activity: EmojiReactionPickerActivity)
    +    fun inject(activity: LoginActivity)
    +    fun inject(activity: LinkHandlerActivity)
    +    fun inject(activity: MainActivity)
    +    fun inject(activity: RoomDirectoryActivity)
    +    fun inject(activity: BugReportActivity)
    +    fun inject(activity: ImageMediaViewerActivity)
    +    fun inject(activity: FilteredRoomsActivity)
    +    fun inject(activity: CreateRoomActivity)
    +    fun inject(activity: VideoMediaViewerActivity)
    +    fun inject(activity: CreateDirectRoomActivity)
    +    fun inject(activity: IncomingShareActivity)
         fun inject(activity: SoftLogoutActivity)
    -
    -    fun inject(verificationBottomSheet: VerificationBottomSheet)
    -
    -    fun inject(permalinkHandlerActivity: PermalinkHandlerActivity)
    -
    +    fun inject(activity: PermalinkHandlerActivity)
         fun inject(activity: QrCodeScannerActivity)
    -
         fun inject(activity: DebugMenuActivity)
    +    fun inject(activity: SharedSecureStorageActivity)
    +    fun inject(activity: BigImageViewerActivity)
     
    -    fun inject(deviceVerificationInfoBottomSheet: DeviceVerificationInfoBottomSheet)
    +    /* ==========================================================================================
    +     * BottomSheets
    +     * ========================================================================================== */
     
    -    fun inject(deviceListBottomSheet: DeviceListBottomSheet)
    +    fun inject(bottomSheet: MessageActionsBottomSheet)
    +    fun inject(bottomSheet: ViewReactionsBottomSheet)
    +    fun inject(bottomSheet: ViewEditHistoryBottomSheet)
    +    fun inject(bottomSheet: DisplayReadReceiptsBottomSheet)
    +    fun inject(bottomSheet: RoomListQuickActionsBottomSheet)
    +    fun inject(bottomSheet: VerificationBottomSheet)
    +    fun inject(bottomSheet: DeviceVerificationInfoBottomSheet)
    +    fun inject(bottomSheet: DeviceListBottomSheet)
    +
    +    /* ==========================================================================================
    +     * Others
    +     * ========================================================================================== */
    +
    +    fun inject(view: VectorInviteView)
    +    fun inject(preference: UserAvatarPreference)
    +    fun inject(button: ReactionButton)
    +
    +    /* ==========================================================================================
    +     * Factory
    +     * ========================================================================================== */
     
         @Component.Factory
         interface Factory {
    diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt
    index f553513bfa..4ae92b29b1 100644
    --- a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt
    @@ -34,20 +34,23 @@ import im.vector.riotx.core.utils.DimensionConverter
     import im.vector.riotx.features.configuration.VectorConfiguration
     import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler
     import im.vector.riotx.features.crypto.verification.IncomingVerificationRequestHandler
    +import im.vector.riotx.features.grouplist.SelectedGroupDataSource
     import im.vector.riotx.features.home.AvatarRenderer
     import im.vector.riotx.features.home.HomeRoomListDataSource
    -import im.vector.riotx.features.grouplist.SelectedGroupDataSource
     import im.vector.riotx.features.html.EventHtmlRenderer
     import im.vector.riotx.features.html.VectorHtmlCompressor
     import im.vector.riotx.features.navigation.Navigator
    -import im.vector.riotx.features.notifications.*
    +import im.vector.riotx.features.notifications.NotifiableEventResolver
    +import im.vector.riotx.features.notifications.NotificationBroadcastReceiver
    +import im.vector.riotx.features.notifications.NotificationDrawerManager
    +import im.vector.riotx.features.notifications.NotificationUtils
    +import im.vector.riotx.features.notifications.PushRuleTriggerListener
     import im.vector.riotx.features.rageshake.BugReporter
     import im.vector.riotx.features.rageshake.VectorFileLogger
     import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
     import im.vector.riotx.features.reactions.data.EmojiDataSource
     import im.vector.riotx.features.session.SessionListener
     import im.vector.riotx.features.settings.VectorPreferences
    -import im.vector.riotx.features.share.ShareRoomListDataSource
     import im.vector.riotx.features.ui.UiStateRepository
     import javax.inject.Singleton
     
    @@ -97,8 +100,6 @@ interface VectorComponent {
     
         fun homeRoomListObservableStore(): HomeRoomListDataSource
     
    -    fun shareRoomListObservableStore(): ShareRoomListDataSource
    -
         fun selectedGroupStore(): SelectedGroupDataSource
     
         fun activeSessionObservableStore(): ActiveSessionDataSource
    diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/LiveData.kt b/vector/src/main/java/im/vector/riotx/core/extensions/LiveData.kt
    index 97215e1e0a..23af9e320f 100644
    --- a/vector/src/main/java/im/vector/riotx/core/extensions/LiveData.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/extensions/LiveData.kt
    @@ -20,8 +20,8 @@ import androidx.lifecycle.LifecycleOwner
     import androidx.lifecycle.LiveData
     import androidx.lifecycle.MutableLiveData
     import androidx.lifecycle.Observer
    -import im.vector.riotx.core.utils.FirstThrottler
     import im.vector.riotx.core.utils.EventObserver
    +import im.vector.riotx.core.utils.FirstThrottler
     import im.vector.riotx.core.utils.LiveEvent
     
     inline fun  LiveData.observeK(owner: LifecycleOwner, crossinline observer: (T?) -> Unit) {
    diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt
    index 0a8345c650..eb3fca66c6 100644
    --- a/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt
    @@ -66,6 +66,6 @@ fun Session.startSyncing(context: Context) {
      * Tell is the session has unsaved e2e keys in the backup
      */
     fun Session.hasUnsavedKeys(): Boolean {
    -    return inboundGroupSessionsCount(false) > 0
    -            && getKeysBackupService().state != KeysBackupState.ReadyToBackUp
    +    return cryptoService().inboundGroupSessionsCount(false) > 0
    +            && cryptoService().keysBackupService().state != KeysBackupState.ReadyToBackUp
     }
    diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Set.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Set.kt
    new file mode 100644
    index 0000000000..43eb1b0d7c
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/core/extensions/Set.kt
    @@ -0,0 +1,26 @@
    +/*
    + * Copyright (c) 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.riotx.core.extensions
    +
    +// Create a new Set including the provided element if not already present, or removing the element if already present
    +fun  Set.toggle(element: T): Set {
    +    return if (contains(element)) {
    +        minus(element)
    +    } else {
    +        plus(element)
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/core/platform/CheckableImageView.kt b/vector/src/main/java/im/vector/riotx/core/platform/CheckableImageView.kt
    new file mode 100644
    index 0000000000..3f8c3f1b45
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/core/platform/CheckableImageView.kt
    @@ -0,0 +1,60 @@
    +/*
    + * Copyright (c) 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.riotx.core.platform
    +
    +import android.content.Context
    +import android.util.AttributeSet
    +import android.widget.Checkable
    +import androidx.appcompat.widget.AppCompatImageView
    +
    +class CheckableImageView : AppCompatImageView, Checkable {
    +
    +    private var mChecked = false
    +
    +    constructor(context: Context) : super(context)
    +
    +    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
    +
    +    constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
    +
    +    override fun isChecked(): Boolean {
    +        return mChecked
    +    }
    +
    +    override fun setChecked(b: Boolean) {
    +        if (b != mChecked) {
    +            mChecked = b
    +            refreshDrawableState()
    +        }
    +    }
    +
    +    override fun toggle() {
    +        isChecked = !mChecked
    +    }
    +
    +    override fun onCreateDrawableState(extraSpace: Int): IntArray {
    +        val drawableState = super.onCreateDrawableState(extraSpace + 1)
    +        if (isChecked) {
    +            mergeDrawableStates(drawableState, CHECKED_STATE_SET)
    +        }
    +        return drawableState
    +    }
    +
    +    companion object {
    +        private val CHECKED_STATE_SET = intArrayOf(android.R.attr.state_checked)
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt b/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt
    index 9d119f4063..2e5786b57d 100644
    --- a/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt
    @@ -38,6 +38,7 @@ import android.text.TextUtils.substring
     import android.text.style.ForegroundColorSpan
     import android.util.AttributeSet
     import androidx.appcompat.widget.AppCompatTextView
    +import timber.log.Timber
     import java.util.ArrayList
     import java.util.regex.Pattern
     
    @@ -116,7 +117,7 @@ class EllipsizingTextView @JvmOverloads constructor(context: Context, attrs: Att
             super.setLineSpacing(add, mult)
         }
     
    -    override fun setText(text: CharSequence, type: BufferType) {
    +    override fun setText(text: CharSequence?, type: BufferType) {
             if (!programmaticChange) {
                 fullText = if (text is Spanned) text else text
                 isStale = true
    @@ -242,7 +243,7 @@ class EllipsizingTextView @JvmOverloads constructor(context: Context, attrs: Att
             @Suppress("DEPRECATION")
             protected fun createWorkingLayout(workingText: CharSequence?): Layout {
                 return StaticLayout(
    -                    workingText,
    +                    workingText ?: "",
                         paint,
                         width - compoundPaddingLeft - compoundPaddingRight,
                         Layout.Alignment.ALIGN_NORMAL,
    @@ -300,7 +301,13 @@ class EllipsizingTextView @JvmOverloads constructor(context: Context, attrs: Att
         private inner class EllipsizeEndStrategy : EllipsizeStrategy() {
             override fun createEllipsizedText(fullText: CharSequence?): CharSequence? {
                 val layout = createWorkingLayout(fullText)
    -            val cutOffIndex = layout.getLineEnd(maxLines - 1)
    +            val cutOffIndex = try {
    +                layout.getLineEnd(maxLines - 1)
    +            } catch (exception: IndexOutOfBoundsException) {
    +                // Not sure to understand why this is happening
    +                Timber.e(exception, "IndexOutOfBoundsException, maxLine: $maxLines")
    +                0
    +            }
                 val textLength = fullText!!.length
                 var cutOffLength = textLength - cutOffIndex
                 if (cutOffLength < ELLIPSIS.length) cutOffLength = ELLIPSIS.length
    diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt
    index 8261b5991b..cbb0e904e4 100644
    --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt
    @@ -263,6 +263,9 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
             }
         }
     
    +    // This should be provided by the framework
    +    protected fun invalidateOptionsMenu() = requireActivity().invalidateOptionsMenu()
    +
         /* ==========================================================================================
          * Common Dialogs
          * ========================================================================================== */
    diff --git a/vector/src/main/java/im/vector/riotx/core/resources/ColorProvider.kt b/vector/src/main/java/im/vector/riotx/core/resources/ColorProvider.kt
    index d19354240c..76c7088174 100644
    --- a/vector/src/main/java/im/vector/riotx/core/resources/ColorProvider.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/resources/ColorProvider.kt
    @@ -26,6 +26,7 @@ import javax.inject.Inject
     
     class ColorProvider @Inject constructor(private val context: Context) {
     
    +    @ColorInt
         fun getColor(@ColorRes colorRes: Int): Int {
             return ContextCompat.getColor(context, colorRes)
         }
    diff --git a/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt b/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt
    index f8cdeb3de6..4790b26ad0 100644
    --- a/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt
    @@ -68,6 +68,7 @@ const val PERMISSION_REQUEST_CODE_CHANGE_AVATAR = 574
     const val PERMISSION_REQUEST_CODE_DOWNLOAD_FILE = 575
     const val PERMISSION_REQUEST_CODE_PICK_ATTACHMENT = 576
     const val PERMISSION_REQUEST_CODE_INCOMING_URI = 577
    +const val PERMISSION_REQUEST_CODE_PREVIEW_FRAGMENT = 578
     
     /**
      * Log the used permissions statuses.
    diff --git a/vector/src/main/java/im/vector/riotx/core/utils/SnapHelperUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/SnapHelperUtils.kt
    new file mode 100644
    index 0000000000..70831730b4
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/core/utils/SnapHelperUtils.kt
    @@ -0,0 +1,76 @@
    +/*
    + * Copyright (c) 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.riotx.core.utils
    +
    +import androidx.recyclerview.widget.RecyclerView
    +import androidx.recyclerview.widget.SnapHelper
    +
    +interface OnSnapPositionChangeListener {
    +
    +    fun onSnapPositionChange(position: Int)
    +}
    +
    +fun RecyclerView.attachSnapHelperWithListener(
    +        snapHelper: SnapHelper,
    +        behavior: SnapOnScrollListener.Behavior = SnapOnScrollListener.Behavior.NOTIFY_ON_SCROLL_STATE_IDLE,
    +        onSnapPositionChangeListener: OnSnapPositionChangeListener) {
    +    snapHelper.attachToRecyclerView(this)
    +    val snapOnScrollListener = SnapOnScrollListener(snapHelper, behavior, onSnapPositionChangeListener)
    +    addOnScrollListener(snapOnScrollListener)
    +}
    +
    +fun SnapHelper.getSnapPosition(recyclerView: RecyclerView): Int {
    +    val layoutManager = recyclerView.layoutManager ?: return RecyclerView.NO_POSITION
    +    val snapView = findSnapView(layoutManager) ?: return RecyclerView.NO_POSITION
    +    return layoutManager.getPosition(snapView)
    +}
    +
    +class SnapOnScrollListener(
    +        private val snapHelper: SnapHelper,
    +        var behavior: Behavior = Behavior.NOTIFY_ON_SCROLL,
    +        var onSnapPositionChangeListener: OnSnapPositionChangeListener? = null
    +) : RecyclerView.OnScrollListener() {
    +
    +    enum class Behavior {
    +        NOTIFY_ON_SCROLL,
    +        NOTIFY_ON_SCROLL_STATE_IDLE
    +    }
    +
    +    private var snapPosition = RecyclerView.NO_POSITION
    +
    +    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
    +        if (behavior == Behavior.NOTIFY_ON_SCROLL) {
    +            maybeNotifySnapPositionChange(recyclerView)
    +        }
    +    }
    +
    +    override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
    +        if (behavior == Behavior.NOTIFY_ON_SCROLL_STATE_IDLE
    +                && newState == RecyclerView.SCROLL_STATE_IDLE) {
    +            maybeNotifySnapPositionChange(recyclerView)
    +        }
    +    }
    +
    +    private fun maybeNotifySnapPositionChange(recyclerView: RecyclerView) {
    +        val snapPosition = snapHelper.getSnapPosition(recyclerView)
    +        val snapPositionChanged = this.snapPosition != snapPosition
    +        if (snapPositionChanged) {
    +            onSnapPositionChangeListener?.onSnapPositionChange(snapPosition)
    +            this.snapPosition = snapPosition
    +        }
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt
    index ba0b99762b..1e005c777b 100644
    --- a/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt
    @@ -18,7 +18,11 @@ package im.vector.riotx.core.utils
     
     import android.annotation.TargetApi
     import android.app.Activity
    -import android.content.*
    +import android.content.ActivityNotFoundException
    +import android.content.ClipData
    +import android.content.ClipboardManager
    +import android.content.Context
    +import android.content.Intent
     import android.net.Uri
     import android.os.Build
     import android.os.PowerManager
    diff --git a/vector/src/main/java/im/vector/riotx/core/utils/UserColor.kt b/vector/src/main/java/im/vector/riotx/core/utils/UserColor.kt
    index 1f8308cd5c..15c4ce8a15 100644
    --- a/vector/src/main/java/im/vector/riotx/core/utils/UserColor.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/utils/UserColor.kt
    @@ -18,6 +18,8 @@ package im.vector.riotx.core.utils
     
     import androidx.annotation.ColorRes
     import im.vector.riotx.R
    +import im.vector.riotx.core.resources.ColorProvider
    +import org.billcarsonfr.jsonviewer.JSonViewerStyleProvider
     import kotlin.math.abs
     
     @ColorRes
    @@ -37,3 +39,14 @@ fun getColorFromUserId(userId: String?): Int {
             else -> R.color.riotx_username_1
         }
     }
    +
    +fun jsonViewerStyler(colorProvider: ColorProvider): JSonViewerStyleProvider {
    +    return JSonViewerStyleProvider(
    +            keyColor = colorProvider.getColor(R.color.riotx_accent),
    +            secondaryColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary),
    +            stringColor = colorProvider.getColorFromAttribute(R.attr.vctr_notice_text_color),
    +            baseColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_primary),
    +            booleanColor = colorProvider.getColorFromAttribute(R.attr.vctr_notice_text_color),
    +            numberColor = colorProvider.getColorFromAttribute(R.attr.vctr_notice_text_color)
    +    )
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt
    index 8a4a0d9309..c9ee1cb6e5 100644
    --- a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt
    @@ -20,11 +20,17 @@ import android.content.Context
     import android.content.Intent
     import android.os.Bundle
     import androidx.fragment.app.Fragment
    -import com.kbeanie.multipicker.api.Picker.*
    +import com.kbeanie.multipicker.api.Picker.PICK_AUDIO
    +import com.kbeanie.multipicker.api.Picker.PICK_CONTACT
    +import com.kbeanie.multipicker.api.Picker.PICK_FILE
    +import com.kbeanie.multipicker.api.Picker.PICK_IMAGE_CAMERA
    +import com.kbeanie.multipicker.api.Picker.PICK_IMAGE_DEVICE
    +import com.kbeanie.multipicker.core.ImagePickerImpl
     import com.kbeanie.multipicker.core.PickerManager
     import im.vector.matrix.android.BuildConfig
     import im.vector.matrix.android.api.session.content.ContentAttachmentData
     import im.vector.riotx.core.platform.Restorable
    +import im.vector.riotx.features.attachments.AttachmentsHelper.Callback
     import timber.log.Timber
     
     private const val CAPTURE_PATH_KEY = "CAPTURE_PATH_KEY"
    @@ -152,6 +158,9 @@ class AttachmentsHelper private constructor(private val context: Context,
             if (resultCode == Activity.RESULT_OK) {
                 val pickerManager = getPickerManagerForRequestCode(requestCode)
                 if (pickerManager != null) {
    +                if (pickerManager is ImagePickerImpl) {
    +                    pickerManager.reinitialize(capturePath)
    +                }
                     pickerManager.submit(data)
                     return true
                 }
    diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt
    index 4b51c548a7..a3de5084de 100644
    --- a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt
    @@ -16,7 +16,11 @@
     
     package im.vector.riotx.features.attachments
     
    -import com.kbeanie.multipicker.api.entity.*
    +import com.kbeanie.multipicker.api.entity.ChosenAudio
    +import com.kbeanie.multipicker.api.entity.ChosenContact
    +import com.kbeanie.multipicker.api.entity.ChosenFile
    +import com.kbeanie.multipicker.api.entity.ChosenImage
    +import com.kbeanie.multipicker.api.entity.ChosenVideo
     import im.vector.matrix.android.api.session.content.ContentAttachmentData
     import timber.log.Timber
     
    @@ -37,7 +41,8 @@ fun ChosenFile.toContentAttachmentData(): ContentAttachmentData {
                 type = mapType(),
                 size = size,
                 date = createdAt?.time ?: System.currentTimeMillis(),
    -            name = displayName
    +            name = displayName,
    +            queryUri = queryUri
         )
     }
     
    @@ -50,7 +55,8 @@ fun ChosenAudio.toContentAttachmentData(): ContentAttachmentData {
                 size = size,
                 date = createdAt?.time ?: System.currentTimeMillis(),
                 name = displayName,
    -            duration = duration
    +            duration = duration,
    +            queryUri = queryUri
         )
     }
     
    @@ -74,7 +80,8 @@ fun ChosenImage.toContentAttachmentData(): ContentAttachmentData {
                 height = height.toLong(),
                 width = width.toLong(),
                 exifOrientation = orientation,
    -            date = createdAt?.time ?: System.currentTimeMillis()
    +            date = createdAt?.time ?: System.currentTimeMillis(),
    +            queryUri = queryUri
         )
     }
     
    @@ -89,6 +96,7 @@ fun ChosenVideo.toContentAttachmentData(): ContentAttachmentData {
                 height = height.toLong(),
                 width = width.toLong(),
                 duration = duration,
    -            name = displayName
    +            name = displayName,
    +            queryUri = queryUri
         )
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsPickerCallback.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsPickerCallback.kt
    index dc7b028aba..62956e08c8 100644
    --- a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsPickerCallback.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsPickerCallback.kt
    @@ -21,7 +21,11 @@ import com.kbeanie.multipicker.api.callbacks.ContactPickerCallback
     import com.kbeanie.multipicker.api.callbacks.FilePickerCallback
     import com.kbeanie.multipicker.api.callbacks.ImagePickerCallback
     import com.kbeanie.multipicker.api.callbacks.VideoPickerCallback
    -import com.kbeanie.multipicker.api.entity.*
    +import com.kbeanie.multipicker.api.entity.ChosenAudio
    +import com.kbeanie.multipicker.api.entity.ChosenContact
    +import com.kbeanie.multipicker.api.entity.ChosenFile
    +import com.kbeanie.multipicker.api.entity.ChosenImage
    +import com.kbeanie.multipicker.api.entity.ChosenVideo
     
     /**
      * This class delegates the PickerManager callbacks to an [AttachmentsHelper.Callback]
    diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/ContentAttachmentData.kt b/vector/src/main/java/im/vector/riotx/features/attachments/ContentAttachmentData.kt
    new file mode 100644
    index 0000000000..8fb717f96e
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/attachments/ContentAttachmentData.kt
    @@ -0,0 +1,38 @@
    +/*
    + * Copyright (c) 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.riotx.features.attachments
    +
    +import im.vector.matrix.android.api.session.content.ContentAttachmentData
    +
    +fun ContentAttachmentData.isPreviewable(): Boolean {
    +    return type == ContentAttachmentData.Type.IMAGE || type == ContentAttachmentData.Type.VIDEO
    +}
    +
    +data class GroupedContentAttachmentData(
    +        val previewables: List,
    +        val notPreviewables: List
    +)
    +
    +fun List.toGroupedContentAttachmentData(): GroupedContentAttachmentData {
    +    return groupBy { it.isPreviewable() }
    +            .let {
    +                GroupedContentAttachmentData(
    +                        it[true].orEmpty(),
    +                        it[false].orEmpty()
    +                )
    +            }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewControllers.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewControllers.kt
    new file mode 100644
    index 0000000000..34f018aaf9
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewControllers.kt
    @@ -0,0 +1,55 @@
    +/*
    + * 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.riotx.features.attachments.preview
    +
    +import com.airbnb.epoxy.TypedEpoxyController
    +import im.vector.matrix.android.api.session.content.ContentAttachmentData
    +import javax.inject.Inject
    +
    +class AttachmentBigPreviewController @Inject constructor() : TypedEpoxyController() {
    +
    +    override fun buildModels(data: AttachmentsPreviewViewState) {
    +        data.attachments.forEach {
    +            attachmentBigPreviewItem {
    +                id(it.path)
    +                attachment(it)
    +            }
    +        }
    +    }
    +}
    +
    +class AttachmentMiniaturePreviewController @Inject constructor() : TypedEpoxyController() {
    +
    +    interface Callback {
    +        fun onAttachmentClicked(position: Int, contentAttachmentData: ContentAttachmentData)
    +    }
    +
    +    var callback: Callback? = null
    +
    +    override fun buildModels(data: AttachmentsPreviewViewState) {
    +        data.attachments.forEachIndexed { index, contentAttachmentData ->
    +            attachmentMiniaturePreviewItem {
    +                id(contentAttachmentData.path)
    +                attachment(contentAttachmentData)
    +                checked(data.currentAttachmentIndex == index)
    +                clickListener { _ ->
    +                    callback?.onAttachmentClicked(index, contentAttachmentData)
    +                }
    +            }
    +        }
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewItems.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewItems.kt
    new file mode 100644
    index 0000000000..3b43fa6e20
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewItems.kt
    @@ -0,0 +1,86 @@
    +/*
    + * 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.riotx.features.attachments.preview
    +
    +import android.view.View
    +import android.widget.ImageView
    +import com.airbnb.epoxy.EpoxyAttribute
    +import com.airbnb.epoxy.EpoxyModelClass
    +import com.bumptech.glide.Glide
    +import com.bumptech.glide.request.RequestOptions
    +import im.vector.matrix.android.api.session.content.ContentAttachmentData
    +import im.vector.riotx.R
    +import im.vector.riotx.core.epoxy.VectorEpoxyHolder
    +import im.vector.riotx.core.epoxy.VectorEpoxyModel
    +import im.vector.riotx.core.platform.CheckableImageView
    +
    +abstract class AttachmentPreviewItem : VectorEpoxyModel() {
    +
    +    abstract val attachment: ContentAttachmentData
    +
    +    override fun bind(holder: H) {
    +        val path = attachment.path
    +        if (attachment.type == ContentAttachmentData.Type.VIDEO || attachment.type == ContentAttachmentData.Type.IMAGE) {
    +            Glide.with(holder.view.context)
    +                    .asBitmap()
    +                    .load(path)
    +                    .apply(RequestOptions().frame(0))
    +                    .into(holder.imageView)
    +        } else {
    +            holder.imageView.setImageResource(R.drawable.filetype_attachment)
    +            holder.imageView.scaleType = ImageView.ScaleType.FIT_CENTER
    +        }
    +    }
    +
    +    abstract class Holder : VectorEpoxyHolder() {
    +        abstract val imageView: ImageView
    +    }
    +}
    +
    +@EpoxyModelClass(layout = R.layout.item_attachment_miniature_preview)
    +abstract class AttachmentMiniaturePreviewItem : AttachmentPreviewItem() {
    +
    +    @EpoxyAttribute override lateinit var attachment: ContentAttachmentData
    +    @EpoxyAttribute
    +    var clickListener: View.OnClickListener? = null
    +    @EpoxyAttribute
    +    var checked: Boolean = false
    +
    +    override fun bind(holder: Holder) {
    +        super.bind(holder)
    +        holder.imageView.isChecked = checked
    +        holder.view.setOnClickListener(clickListener)
    +    }
    +
    +    class Holder : AttachmentPreviewItem.Holder() {
    +        override val imageView: CheckableImageView
    +            get() = miniatureImageView
    +        private val miniatureImageView by bind(R.id.attachmentMiniatureImageView)
    +    }
    +}
    +
    +@EpoxyModelClass(layout = R.layout.item_attachment_big_preview)
    +abstract class AttachmentBigPreviewItem : AttachmentPreviewItem() {
    +
    +    @EpoxyAttribute override lateinit var attachment: ContentAttachmentData
    +
    +    class Holder : AttachmentPreviewItem.Holder() {
    +        override val imageView: ImageView
    +            get() = bigImageView
    +        private val bigImageView by bind(R.id.attachmentBigImageView)
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewAction.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewAction.kt
    new file mode 100644
    index 0000000000..5acc59b035
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewAction.kt
    @@ -0,0 +1,26 @@
    +/*
    + * 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.riotx.features.attachments.preview
    +
    +import im.vector.riotx.core.platform.VectorViewModelAction
    +
    +sealed class AttachmentsPreviewAction : VectorViewModelAction {
    +    object RemoveCurrentAttachment : AttachmentsPreviewAction()
    +    data class SetCurrentAttachment(val index: Int): AttachmentsPreviewAction()
    +    data class UpdatePathOfCurrentAttachment(val newPath: String): AttachmentsPreviewAction()
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewActivity.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewActivity.kt
    new file mode 100644
    index 0000000000..46a90803ca
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewActivity.kt
    @@ -0,0 +1,77 @@
    +/*
    + * 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.riotx.features.attachments.preview
    +
    +import android.content.Context
    +import android.content.Intent
    +import androidx.appcompat.widget.Toolbar
    +import im.vector.matrix.android.api.session.content.ContentAttachmentData
    +import im.vector.riotx.R
    +import im.vector.riotx.core.extensions.addFragment
    +import im.vector.riotx.core.platform.ToolbarConfigurable
    +import im.vector.riotx.core.platform.VectorBaseActivity
    +import im.vector.riotx.features.themes.ActivityOtherThemes
    +
    +class AttachmentsPreviewActivity : VectorBaseActivity(), ToolbarConfigurable {
    +
    +    companion object {
    +        const val REQUEST_CODE = 55
    +
    +        private const val EXTRA_FRAGMENT_ARGS = "EXTRA_FRAGMENT_ARGS"
    +        private const val ATTACHMENTS_PREVIEW_RESULT = "ATTACHMENTS_PREVIEW_RESULT"
    +        private const val KEEP_ORIGINAL_IMAGES_SIZE = "KEEP_ORIGINAL_IMAGES_SIZE"
    +
    +        fun newIntent(context: Context, args: AttachmentsPreviewArgs): Intent {
    +            return Intent(context, AttachmentsPreviewActivity::class.java).apply {
    +                putExtra(EXTRA_FRAGMENT_ARGS, args)
    +            }
    +        }
    +
    +        fun getOutput(intent: Intent): List {
    +            return intent.getParcelableArrayListExtra(ATTACHMENTS_PREVIEW_RESULT)
    +        }
    +
    +        fun getKeepOriginalSize(intent: Intent): Boolean {
    +            return intent.getBooleanExtra(KEEP_ORIGINAL_IMAGES_SIZE, false)
    +        }
    +    }
    +
    +    override fun getOtherThemes() = ActivityOtherThemes.AttachmentsPreview
    +
    +    override fun getLayoutRes() = R.layout.activity_simple
    +
    +    override fun initUiAndData() {
    +        if (isFirstCreation()) {
    +            val fragmentArgs: AttachmentsPreviewArgs = intent?.extras?.getParcelable(EXTRA_FRAGMENT_ARGS) ?: return
    +            addFragment(R.id.simpleFragmentContainer, AttachmentsPreviewFragment::class.java, fragmentArgs)
    +        }
    +    }
    +
    +    fun setResultAndFinish(data: List, keepOriginalImageSize: Boolean) {
    +        val resultIntent = Intent().apply {
    +            putParcelableArrayListExtra(ATTACHMENTS_PREVIEW_RESULT, ArrayList(data))
    +            putExtra(KEEP_ORIGINAL_IMAGES_SIZE, keepOriginalImageSize)
    +        }
    +        setResult(RESULT_OK, resultIntent)
    +        finish()
    +    }
    +
    +    override fun configure(toolbar: Toolbar) {
    +        configureToolbar(toolbar)
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt
    new file mode 100644
    index 0000000000..1d525dddf7
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt
    @@ -0,0 +1,258 @@
    +/*
    + * 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.riotx.features.attachments.preview
    +
    +import android.app.Activity.RESULT_CANCELED
    +import android.app.Activity.RESULT_OK
    +import android.content.Intent
    +import android.graphics.Color
    +import android.os.Bundle
    +import android.os.Parcelable
    +import android.view.Menu
    +import android.view.MenuItem
    +import android.view.View
    +import android.view.ViewGroup
    +import android.widget.Toast
    +import androidx.core.net.toUri
    +import androidx.core.view.ViewCompat
    +import androidx.core.view.updateLayoutParams
    +import androidx.core.view.updatePadding
    +import androidx.recyclerview.widget.LinearLayoutManager
    +import androidx.recyclerview.widget.PagerSnapHelper
    +import com.airbnb.mvrx.args
    +import com.airbnb.mvrx.fragmentViewModel
    +import com.airbnb.mvrx.withState
    +import com.yalantis.ucrop.UCrop
    +import com.yalantis.ucrop.UCropActivity
    +import im.vector.matrix.android.api.session.content.ContentAttachmentData
    +import im.vector.riotx.R
    +import im.vector.riotx.core.extensions.cleanup
    +import im.vector.riotx.core.platform.VectorBaseFragment
    +import im.vector.riotx.core.resources.ColorProvider
    +import im.vector.riotx.core.utils.OnSnapPositionChangeListener
    +import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
    +import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_PREVIEW_FRAGMENT
    +import im.vector.riotx.core.utils.SnapOnScrollListener
    +import im.vector.riotx.core.utils.allGranted
    +import im.vector.riotx.core.utils.attachSnapHelperWithListener
    +import im.vector.riotx.core.utils.checkPermissions
    +import kotlinx.android.parcel.Parcelize
    +import kotlinx.android.synthetic.main.fragment_attachments_preview.*
    +import timber.log.Timber
    +import java.io.File
    +import javax.inject.Inject
    +
    +@Parcelize
    +data class AttachmentsPreviewArgs(
    +        val attachments: List
    +) : Parcelable
    +
    +class AttachmentsPreviewFragment @Inject constructor(
    +        val viewModelFactory: AttachmentsPreviewViewModel.Factory,
    +        private val attachmentMiniaturePreviewController: AttachmentMiniaturePreviewController,
    +        private val attachmentBigPreviewController: AttachmentBigPreviewController,
    +        private val colorProvider: ColorProvider
    +) : VectorBaseFragment(), AttachmentMiniaturePreviewController.Callback {
    +
    +    private val fragmentArgs: AttachmentsPreviewArgs by args()
    +    private val viewModel: AttachmentsPreviewViewModel by fragmentViewModel()
    +
    +    override fun getLayoutResId() = R.layout.fragment_attachments_preview
    +
    +    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    +        super.onViewCreated(view, savedInstanceState)
    +        applyInsets()
    +        setupRecyclerViews()
    +        setupToolbar(attachmentPreviewerToolbar)
    +        attachmentPreviewerSendButton.setOnClickListener {
    +            setResultAndFinish()
    +        }
    +    }
    +
    +    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    +        if (resultCode == RESULT_OK) {
    +            if (requestCode == UCrop.REQUEST_CROP && data != null) {
    +                Timber.v("Crop success")
    +                handleCropResult(data)
    +            }
    +        }
    +        if (resultCode == UCrop.RESULT_ERROR) {
    +            Timber.v("Crop error")
    +        }
    +    }
    +
    +    override fun onOptionsItemSelected(item: MenuItem): Boolean {
    +        return when (item.itemId) {
    +            R.id.attachmentsPreviewRemoveAction -> {
    +                handleRemoveAction()
    +                true
    +            }
    +            R.id.attachmentsPreviewEditAction   -> {
    +                handleEditAction()
    +                true
    +            }
    +            else                                -> {
    +                super.onOptionsItemSelected(item)
    +            }
    +        }
    +    }
    +
    +    override fun onPrepareOptionsMenu(menu: Menu) {
    +        withState(viewModel) { state ->
    +            val editMenuItem = menu.findItem(R.id.attachmentsPreviewEditAction)
    +            val showEditMenuItem = state.attachments[state.currentAttachmentIndex].isEditable()
    +            editMenuItem.setVisible(showEditMenuItem)
    +        }
    +
    +        super.onPrepareOptionsMenu(menu)
    +    }
    +
    +    override fun getMenuRes() = R.menu.vector_attachments_preview
    +
    +    override fun onDestroyView() {
    +        super.onDestroyView()
    +        attachmentPreviewerMiniatureList.cleanup()
    +        attachmentPreviewerBigList.cleanup()
    +        attachmentMiniaturePreviewController.callback = null
    +    }
    +
    +    override fun invalidate() = withState(viewModel) { state ->
    +        invalidateOptionsMenu()
    +        if (state.attachments.isEmpty()) {
    +            requireActivity().setResult(RESULT_CANCELED)
    +            requireActivity().finish()
    +        } else {
    +            attachmentMiniaturePreviewController.setData(state)
    +            attachmentBigPreviewController.setData(state)
    +            attachmentPreviewerBigList.scrollToPosition(state.currentAttachmentIndex)
    +            attachmentPreviewerMiniatureList.scrollToPosition(state.currentAttachmentIndex)
    +            attachmentPreviewerSendImageOriginalSize.text = resources.getQuantityString(R.plurals.send_images_with_original_size, state.attachments.size)
    +        }
    +    }
    +
    +    override fun onAttachmentClicked(position: Int, contentAttachmentData: ContentAttachmentData) {
    +        viewModel.handle(AttachmentsPreviewAction.SetCurrentAttachment(position))
    +    }
    +
    +    private fun setResultAndFinish() = withState(viewModel) {
    +        (requireActivity() as? AttachmentsPreviewActivity)?.setResultAndFinish(
    +                it.attachments,
    +                attachmentPreviewerSendImageOriginalSize.isChecked
    +        )
    +    }
    +
    +    private fun applyInsets() {
    +        view?.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
    +        ViewCompat.setOnApplyWindowInsetsListener(attachmentPreviewerBottomContainer) { v, insets ->
    +            v.updatePadding(bottom = insets.systemWindowInsetBottom)
    +            insets
    +        }
    +        ViewCompat.setOnApplyWindowInsetsListener(attachmentPreviewerToolbar) { v, insets ->
    +            v.updateLayoutParams {
    +                topMargin = insets.systemWindowInsetTop
    +            }
    +            insets
    +        }
    +    }
    +
    +    private fun handleCropResult(result: Intent) {
    +        val resultPath = UCrop.getOutput(result)?.path
    +        if (resultPath != null) {
    +            viewModel.handle(AttachmentsPreviewAction.UpdatePathOfCurrentAttachment(resultPath))
    +        } else {
    +            Toast.makeText(requireContext(), "Cannot retrieve cropped value", Toast.LENGTH_SHORT).show()
    +        }
    +    }
    +
    +    private fun handleRemoveAction() {
    +        viewModel.handle(AttachmentsPreviewAction.RemoveCurrentAttachment)
    +    }
    +
    +    private fun handleEditAction() {
    +        // check permissions
    +        if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_PREVIEW_FRAGMENT)) {
    +            doHandleEditAction()
    +        }
    +    }
    +
    +    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
    +        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    +
    +        if (requestCode == PERMISSION_REQUEST_CODE_PREVIEW_FRAGMENT && allGranted(grantResults)) {
    +            doHandleEditAction()
    +        }
    +    }
    +
    +    private fun doHandleEditAction() = withState(viewModel) {
    +        val currentAttachment = it.attachments.getOrNull(it.currentAttachmentIndex) ?: return@withState
    +        val destinationFile = File(requireContext().cacheDir, "${currentAttachment.name}_edited_image_${System.currentTimeMillis()}")
    +        // Note: using currentAttachment.queryUri.toUri() make the app crash when sharing from Google Photos
    +        val uri = File(currentAttachment.path).toUri()
    +        UCrop.of(uri, destinationFile.toUri())
    +                .withOptions(
    +                        UCrop.Options()
    +                                .apply {
    +                                    setAllowedGestures(
    +                                            /* tabScale = */ UCropActivity.SCALE,
    +                                            /* tabRotate = */ UCropActivity.ALL,
    +                                            /* tabAspectRatio = */ UCropActivity.SCALE
    +                                    )
    +                                    setToolbarTitle(currentAttachment.name)
    +                                    // Disable freestyle crop, usability was not easy
    +                                    // setFreeStyleCropEnabled(true)
    +                                    // Color used for toolbar icon and text
    +                                    setToolbarColor(colorProvider.getColorFromAttribute(R.attr.riotx_background))
    +                                    setToolbarWidgetColor(colorProvider.getColorFromAttribute(R.attr.vctr_toolbar_primary_text_color))
    +                                    // Background
    +                                    setRootViewBackgroundColor(colorProvider.getColorFromAttribute(R.attr.riotx_background))
    +                                    // Status bar color (pb in dark mode, icon of the status bar are dark)
    +                                    setStatusBarColor(colorProvider.getColorFromAttribute(R.attr.riotx_header_panel_background))
    +                                    // Known issue: there is still orange color used by the lib
    +                                    // https://github.com/Yalantis/uCrop/issues/602
    +                                    setActiveControlsWidgetColor(colorProvider.getColor(R.color.riotx_accent))
    +                                    // Hide the logo (does not work)
    +                                    setLogoColor(Color.TRANSPARENT)
    +                                }
    +                )
    +                .start(requireContext(), this)
    +    }
    +
    +    private fun setupRecyclerViews() {
    +        attachmentMiniaturePreviewController.callback = this
    +
    +        attachmentPreviewerMiniatureList.let {
    +            it.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
    +            it.setHasFixedSize(true)
    +            it.adapter = attachmentMiniaturePreviewController.adapter
    +        }
    +
    +        attachmentPreviewerBigList.let {
    +            it.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
    +            it.attachSnapHelperWithListener(
    +                    PagerSnapHelper(),
    +                    SnapOnScrollListener.Behavior.NOTIFY_ON_SCROLL_STATE_IDLE,
    +                    object : OnSnapPositionChangeListener {
    +                        override fun onSnapPositionChange(position: Int) {
    +                            viewModel.handle(AttachmentsPreviewAction.SetCurrentAttachment(position))
    +                        }
    +                    })
    +            it.setHasFixedSize(true)
    +            it.adapter = attachmentBigPreviewController.adapter
    +        }
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/share/ShareRoomListDataSource.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewEvents.kt
    similarity index 61%
    rename from vector/src/main/java/im/vector/riotx/features/share/ShareRoomListDataSource.kt
    rename to vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewEvents.kt
    index b1b4d7b46e..12a9d9aa24 100644
    --- a/vector/src/main/java/im/vector/riotx/features/share/ShareRoomListDataSource.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewEvents.kt
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2019 New Vector Ltd
    + * 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.
    @@ -12,14 +12,11 @@
      * 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.riotx.features.share
    +package im.vector.riotx.features.attachments.preview
     
    -import im.vector.matrix.android.api.session.room.model.RoomSummary
    -import im.vector.riotx.core.utils.BehaviorDataSource
    -import javax.inject.Inject
    -import javax.inject.Singleton
    +import im.vector.riotx.core.platform.VectorViewEvents
     
    -@Singleton
    -class ShareRoomListDataSource @Inject constructor() : BehaviorDataSource>()
    +sealed class AttachmentsPreviewViewEvents : VectorViewEvents
    diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewModel.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewModel.kt
    new file mode 100644
    index 0000000000..1f6c8c2f8b
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewModel.kt
    @@ -0,0 +1,78 @@
    +/*
    + * 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.riotx.features.attachments.preview
    +
    +import com.airbnb.mvrx.FragmentViewModelContext
    +import com.airbnb.mvrx.MvRxViewModelFactory
    +import com.airbnb.mvrx.ViewModelContext
    +import com.squareup.inject.assisted.Assisted
    +import com.squareup.inject.assisted.AssistedInject
    +import im.vector.riotx.core.extensions.exhaustive
    +import im.vector.riotx.core.platform.VectorViewModel
    +
    +class AttachmentsPreviewViewModel @AssistedInject constructor(@Assisted initialState: AttachmentsPreviewViewState)
    +    : VectorViewModel(initialState) {
    +
    +    @AssistedInject.Factory
    +    interface Factory {
    +        fun create(initialState: AttachmentsPreviewViewState): AttachmentsPreviewViewModel
    +    }
    +
    +    companion object : MvRxViewModelFactory {
    +
    +        @JvmStatic
    +        override fun create(viewModelContext: ViewModelContext, state: AttachmentsPreviewViewState): AttachmentsPreviewViewModel? {
    +            val fragment: AttachmentsPreviewFragment = (viewModelContext as FragmentViewModelContext).fragment()
    +            return fragment.viewModelFactory.create(state)
    +        }
    +    }
    +
    +    override fun handle(action: AttachmentsPreviewAction) {
    +        when (action) {
    +            is AttachmentsPreviewAction.SetCurrentAttachment          -> handleSetCurrentAttachment(action)
    +            is AttachmentsPreviewAction.UpdatePathOfCurrentAttachment -> handleUpdatePathOfCurrentAttachment(action)
    +            AttachmentsPreviewAction.RemoveCurrentAttachment          -> handleRemoveCurrentAttachment()
    +        }.exhaustive
    +    }
    +
    +    private fun handleRemoveCurrentAttachment() = withState {
    +        val currentAttachment = it.attachments.getOrNull(it.currentAttachmentIndex) ?: return@withState
    +        val attachments = it.attachments.minusElement(currentAttachment)
    +        val newAttachmentIndex = it.currentAttachmentIndex.coerceAtMost(attachments.size - 1)
    +        setState {
    +            copy(attachments = attachments, currentAttachmentIndex = newAttachmentIndex)
    +        }
    +    }
    +
    +    private fun handleUpdatePathOfCurrentAttachment(action: AttachmentsPreviewAction.UpdatePathOfCurrentAttachment) = withState {
    +        val attachments = it.attachments.mapIndexed { index, contentAttachmentData ->
    +            if (index == it.currentAttachmentIndex) {
    +                contentAttachmentData.copy(path = action.newPath)
    +            } else {
    +                contentAttachmentData
    +            }
    +        }
    +        setState {
    +            copy(attachments = attachments)
    +        }
    +    }
    +
    +    private fun handleSetCurrentAttachment(action: AttachmentsPreviewAction.SetCurrentAttachment) = setState {
    +        copy(currentAttachmentIndex = action.index)
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewState.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewState.kt
    new file mode 100644
    index 0000000000..50214fd9d7
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewState.kt
    @@ -0,0 +1,30 @@
    +/*
    + * 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.riotx.features.attachments.preview
    +
    +import com.airbnb.mvrx.MvRxState
    +import im.vector.matrix.android.api.session.content.ContentAttachmentData
    +
    +data class AttachmentsPreviewViewState(
    +        val attachments: List,
    +        val currentAttachmentIndex: Int = 0,
    +        val sendImagesWithOriginalSize: Boolean = false
    +) : MvRxState {
    +
    +    constructor(args: AttachmentsPreviewArgs) : this(attachments = args.attachments)
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/Extensions.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/Extensions.kt
    new file mode 100644
    index 0000000000..3bd47baa89
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/Extensions.kt
    @@ -0,0 +1,28 @@
    +/*
    + * Copyright (c) 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.riotx.features.attachments.preview
    +
    +import im.vector.matrix.android.api.session.content.ContentAttachmentData
    +
    +/**
    + * All images are editable, expect Gif
    + */
    +fun ContentAttachmentData.isEditable(): Boolean {
    +    return type == ContentAttachmentData.Type.IMAGE
    +            && mimeType?.startsWith("image/") == true
    +            && mimeType != "image/gif"
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt
    index 2ca97a0f18..12674e5cd2 100644
    --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt
    @@ -23,7 +23,11 @@ import android.content.Intent
     import android.os.Bundle
     import android.view.View
     import androidx.appcompat.app.AlertDialog
    -import com.airbnb.mvrx.*
    +import com.airbnb.mvrx.Async
    +import com.airbnb.mvrx.Fail
    +import com.airbnb.mvrx.Loading
    +import com.airbnb.mvrx.Success
    +import com.airbnb.mvrx.viewModel
     import im.vector.matrix.android.api.session.room.failure.CreateRoomFailure
     import im.vector.riotx.R
     import im.vector.riotx.core.di.ScreenComponent
    diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomKnownUsersFragment.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomKnownUsersFragment.kt
    index e06df21be5..24b5394e5c 100644
    --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomKnownUsersFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomKnownUsersFragment.kt
    @@ -23,17 +23,14 @@ import android.view.Menu
     import android.view.MenuItem
     import android.view.View
     import android.widget.ScrollView
    -import androidx.core.view.size
     import com.airbnb.mvrx.activityViewModel
     import com.airbnb.mvrx.withState
     import com.google.android.material.chip.Chip
    -import com.google.android.material.chip.ChipGroup
     import com.jakewharton.rxbinding3.widget.textChanges
     import im.vector.matrix.android.api.session.user.model.User
     import im.vector.riotx.R
     import im.vector.riotx.core.extensions.cleanup
     import im.vector.riotx.core.extensions.configureWith
    -import im.vector.riotx.core.extensions.exhaustive
     import im.vector.riotx.core.extensions.hideKeyboard
     import im.vector.riotx.core.extensions.setupAsSearch
     import im.vector.riotx.core.platform.VectorBaseFragment
    @@ -61,11 +58,6 @@ class CreateDirectRoomKnownUsersFragment @Inject constructor(
             setupFilterView()
             setupAddByMatrixIdView()
             setupCloseView()
    -        viewModel.observeViewEvents {
    -            when (it) {
    -                is CreateDirectRoomViewEvents.SelectUserAction -> updateChipsView(it)
    -            }.exhaustive
    -        }
             viewModel.selectSubscribe(this, CreateDirectRoomViewState::selectedUsers) {
                 renderSelectedUsers(it)
             }
    @@ -138,28 +130,28 @@ class CreateDirectRoomKnownUsersFragment @Inject constructor(
             knownUsersController.setData(it)
         }
     
    -    private fun updateChipsView(data: CreateDirectRoomViewEvents.SelectUserAction) {
    -        if (data.isAdded) {
    -            addChipToGroup(data.user, chipGroup)
    -        } else {
    -            if (chipGroup.size > data.index) {
    -                chipGroup.removeViewAt(data.index)
    +    private fun renderSelectedUsers(selectedUsers: Set) {
    +        invalidateOptionsMenu()
    +
    +        val currentNumberOfChips = chipGroup.childCount
    +        val newNumberOfChips = selectedUsers.size
    +
    +        chipGroup.removeAllViews()
    +        selectedUsers.forEach { addChipToGroup(it) }
    +
    +        // Scroll to the bottom when adding chips. When removing chips, do not scroll
    +        if (newNumberOfChips >= currentNumberOfChips) {
    +            chipGroupScrollView.post {
    +                chipGroupScrollView.fullScroll(ScrollView.FOCUS_DOWN)
                 }
             }
         }
     
    -    private fun renderSelectedUsers(selectedUsers: Set) {
    -        vectorBaseActivity.invalidateOptionsMenu()
    -        if (selectedUsers.isNotEmpty() && chipGroup.size == 0) {
    -            selectedUsers.forEach { addChipToGroup(it, chipGroup) }
    -        }
    -    }
    -
    -    private fun addChipToGroup(user: User, chipGroup: ChipGroup) {
    +    private fun addChipToGroup(user: User) {
             val chip = Chip(requireContext())
             chip.setChipBackgroundColorResource(android.R.color.transparent)
             chip.chipStrokeWidth = dimensionConverter.dpToPx(1).toFloat()
    -        chip.text = if (user.displayName.isNullOrBlank()) user.userId else user.displayName
    +        chip.text = user.getBestName()
             chip.isClickable = true
             chip.isCheckable = false
             chip.isCloseIconVisible = true
    @@ -167,9 +159,6 @@ class CreateDirectRoomKnownUsersFragment @Inject constructor(
             chip.setOnCloseIconClickListener {
                 viewModel.handle(CreateDirectRoomAction.RemoveSelectedUser(user))
             }
    -        chipGroupScrollView.post {
    -            chipGroupScrollView.fullScroll(ScrollView.FOCUS_DOWN)
    -        }
         }
     
         override fun onItemClick(user: User) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewEvents.kt
    index 168f23c9f7..0ed584ac6b 100644
    --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewEvents.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewEvents.kt
    @@ -16,16 +16,9 @@
     
     package im.vector.riotx.features.createdirect
     
    -import im.vector.matrix.android.api.session.user.model.User
     import im.vector.riotx.core.platform.VectorViewEvents
     
     /**
      * Transient events for create direct room screen
      */
    -sealed class CreateDirectRoomViewEvents : VectorViewEvents {
    -    data class SelectUserAction(
    -            val user: User,
    -            val isAdded: Boolean,
    -            val index: Int
    -    ) : CreateDirectRoomViewEvents()
    -}
    +sealed class CreateDirectRoomViewEvents : VectorViewEvents
    diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt
    index b115a623a8..71fae11486 100644
    --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt
    @@ -27,9 +27,9 @@ import com.squareup.inject.assisted.Assisted
     import com.squareup.inject.assisted.AssistedInject
     import im.vector.matrix.android.api.session.Session
     import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
    -import im.vector.matrix.android.api.session.user.model.User
     import im.vector.matrix.android.api.util.toMatrixItem
     import im.vector.matrix.rx.rx
    +import im.vector.riotx.core.extensions.toggle
     import im.vector.riotx.core.platform.VectorViewModel
     import io.reactivex.Single
     import io.reactivex.android.schedulers.AndroidSchedulers
    @@ -91,30 +91,15 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
         }
     
         private fun handleRemoveSelectedUser(action: CreateDirectRoomAction.RemoveSelectedUser) = withState { state ->
    -        val index = state.selectedUsers.indexOfFirst { it.userId == action.user.userId }
             val selectedUsers = state.selectedUsers.minus(action.user)
             setState { copy(selectedUsers = selectedUsers) }
    -        _viewEvents.post(CreateDirectRoomViewEvents.SelectUserAction(action.user, false, index))
         }
     
         private fun handleSelectUser(action: CreateDirectRoomAction.SelectUser) = withState { state ->
             // Reset the filter asap
             directoryUsersSearch.accept("")
    -        val isAddOperation: Boolean
    -        val selectedUsers: Set
    -        val indexOfUser = state.selectedUsers.indexOfFirst { it.userId == action.user.userId }
    -        val changeIndex: Int
    -        if (indexOfUser == -1) {
    -            changeIndex = state.selectedUsers.size
    -            selectedUsers = state.selectedUsers.plus(action.user)
    -            isAddOperation = true
    -        } else {
    -            changeIndex = indexOfUser
    -            selectedUsers = state.selectedUsers.minus(action.user)
    -            isAddOperation = false
    -        }
    +        val selectedUsers = state.selectedUsers.toggle(action.user)
             setState { copy(selectedUsers = selectedUsers) }
    -        _viewEvents.post(CreateDirectRoomViewEvents.SelectUserAction(action.user, isAddOperation, changeIndex))
         }
     
         private fun observeDirectoryUsers() {
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt
    index 9642c2d8c6..cae8b50523 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt
    @@ -38,7 +38,7 @@ class KeysExporter(private val session: Session) {
         fun export(context: Context, password: String, callback: MatrixCallback) {
             GlobalScope.launch(Dispatchers.Main) {
                 runCatching {
    -                val data = awaitCallback { session.exportRoomKeys(password, it) }
    +                val data = awaitCallback { session.cryptoService().exportRoomKeys(password, it) }
                     withContext(Dispatchers.IO) {
                         val parentDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
                         val file = File(parentDir, "riotx-keys-" + System.currentTimeMillis() + ".txt")
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysImporter.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysImporter.kt
    index b60e25af04..5f550c032a 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysImporter.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysImporter.kt
    @@ -59,7 +59,7 @@ class KeysImporter(private val session: Session) {
                         }
     
                         awaitCallback {
    -                        session.importRoomKeys(data, password, null, it)
    +                        session.cryptoService().importRoomKeys(data, password, null, it)
                         }
                     }
                 }.foldToCallback(callback)
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreActivity.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreActivity.kt
    index a12a43d06f..e6d303b3aa 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreActivity.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreActivity.kt
    @@ -19,7 +19,6 @@ import android.app.Activity
     import android.content.Context
     import android.content.Intent
     import androidx.appcompat.app.AlertDialog
    -import androidx.fragment.app.FragmentManager
     import androidx.lifecycle.Observer
     import im.vector.riotx.R
     import im.vector.riotx.core.extensions.addFragmentToBackstack
    @@ -79,7 +78,6 @@ class KeysBackupRestoreActivity : SimpleFragmentActivity() {
                         addFragmentToBackstack(R.id.container, KeysBackupRestoreFromKeyFragment::class.java)
                     }
                     KeysBackupRestoreSharedViewModel.NAVIGATE_TO_SUCCESS          -> {
    -                    supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
                         replaceFragment(R.id.container, KeysBackupRestoreSuccessFragment::class.java)
                     }
                 }
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyViewModel.kt
    index 7ece88e086..0cf297f7f1 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyViewModel.kt
    @@ -47,7 +47,7 @@ class KeysBackupRestoreFromKeyViewModel @Inject constructor() : ViewModel() {
     
         fun recoverKeys(context: Context, sharedViewModel: KeysBackupRestoreSharedViewModel) {
             val session = sharedViewModel.session
    -        val keysBackup = session.getKeysBackupService()
    +        val keysBackup = session.cryptoService().keysBackupService()
     
             recoveryCodeErrorText.value = null
             val recoveryKey = recoveryCode.value!!
    @@ -62,21 +62,21 @@ class KeysBackupRestoreFromKeyViewModel @Inject constructor() : ViewModel() {
                         override fun onStepProgress(step: StepProgressListener.Step) {
                             when (step) {
                                 is StepProgressListener.Step.DownloadingKey -> {
    -                                sharedViewModel.loadingEvent.value = WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message)
    +                                sharedViewModel.loadingEvent.postValue(WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message)
                                             + "\n" + context.getString(R.string.keys_backup_restoring_downloading_backup_waiting_message),
    -                                        isIndeterminate = true)
    +                                        isIndeterminate = true))
                                 }
    -                            is StepProgressListener.Step.ImportingKey -> {
    +                            is StepProgressListener.Step.ImportingKey   -> {
                                     // Progress 0 can take a while, display an indeterminate progress in this case
                                     if (step.progress == 0) {
    -                                    sharedViewModel.loadingEvent.value = WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message)
    +                                    sharedViewModel.loadingEvent.postValue(WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message)
                                                 + "\n" + context.getString(R.string.keys_backup_restoring_importing_keys_waiting_message),
    -                                            isIndeterminate = true)
    +                                            isIndeterminate = true))
                                     } else {
    -                                    sharedViewModel.loadingEvent.value = WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message)
    +                                    sharedViewModel.loadingEvent.postValue(WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message)
                                                 + "\n" + context.getString(R.string.keys_backup_restoring_importing_keys_waiting_message),
                                                 step.progress,
    -                                            step.total)
    +                                            step.total))
                                     }
                                 }
                             }
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseViewModel.kt
    index 4e86f60909..c6ffd730ca 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseViewModel.kt
    @@ -49,7 +49,7 @@ class KeysBackupRestoreFromPassphraseViewModel @Inject constructor() : ViewModel
         }
     
         fun recoverKeys(context: Context, sharedViewModel: KeysBackupRestoreSharedViewModel) {
    -        val keysBackup = sharedViewModel.session.getKeysBackupService()
    +        val keysBackup = sharedViewModel.session.cryptoService().keysBackupService()
     
             passphraseErrorText.value = null
     
    @@ -63,27 +63,28 @@ class KeysBackupRestoreFromPassphraseViewModel @Inject constructor() : ViewModel
                         override fun onStepProgress(step: StepProgressListener.Step) {
                             when (step) {
                                 is StepProgressListener.Step.ComputingKey -> {
    -                                sharedViewModel.loadingEvent.value = WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message)
    +                                sharedViewModel.loadingEvent.postValue(WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message)
                                             + "\n" + context.getString(R.string.keys_backup_restoring_computing_key_waiting_message),
                                             step.progress,
    -                                        step.total)
    +                                        step.total))
                                 }
                                 is StepProgressListener.Step.DownloadingKey -> {
    -                                sharedViewModel.loadingEvent.value = WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message)
    +                                sharedViewModel.loadingEvent.postValue(WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message)
                                             + "\n" + context.getString(R.string.keys_backup_restoring_downloading_backup_waiting_message),
    -                                        isIndeterminate = true)
    +                                        isIndeterminate = true))
                                 }
                                 is StepProgressListener.Step.ImportingKey -> {
    +                                Timber.d("backupKeys.ImportingKey.progress: " + step.progress)
                                     // Progress 0 can take a while, display an indeterminate progress in this case
                                     if (step.progress == 0) {
    -                                    sharedViewModel.loadingEvent.value = WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message)
    +                                    sharedViewModel.loadingEvent.postValue(WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message)
                                                 + "\n" + context.getString(R.string.keys_backup_restoring_importing_keys_waiting_message),
    -                                            isIndeterminate = true)
    +                                            isIndeterminate = true))
                                     } else {
    -                                    sharedViewModel.loadingEvent.value = WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message)
    +                                    sharedViewModel.loadingEvent.postValue(WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message)
                                                 + "\n" + context.getString(R.string.keys_backup_restoring_importing_keys_waiting_message),
                                                 step.progress,
    -                                            step.total)
    +                                            step.total))
                                     }
                                 }
                             }
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreSharedViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreSharedViewModel.kt
    index 83b4daa383..5586d0cf05 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreSharedViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreSharedViewModel.kt
    @@ -63,7 +63,7 @@ class KeysBackupRestoreSharedViewModel @Inject constructor() : ViewModel() {
         }
     
         fun getLatestVersion(context: Context) {
    -        val keysBackup = session.getKeysBackupService()
    +        val keysBackup = session.cryptoService().keysBackupService()
     
             loadingEvent.value = WaitingViewData(context.getString(R.string.keys_backup_restore_is_getting_backup_version))
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreSuccessFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreSuccessFragment.kt
    index 0f681af737..def8e4bf65 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreSuccessFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreSuccessFragment.kt
    @@ -17,6 +17,7 @@ package im.vector.riotx.features.crypto.keysbackup.restore
     
     import android.os.Bundle
     import android.widget.TextView
    +import androidx.core.view.isVisible
     import butterknife.BindView
     import butterknife.OnClick
     import im.vector.riotx.R
    @@ -39,16 +40,20 @@ class KeysBackupRestoreSuccessFragment @Inject constructor() : VectorBaseFragmen
             super.onActivityCreated(savedInstanceState)
             sharedViewModel = activityViewModelProvider.get(KeysBackupRestoreSharedViewModel::class.java)
     
    -        sharedViewModel.importKeyResult?.let {
    -            val part1 = resources.getQuantityString(R.plurals.keys_backup_restore_success_description_part1,
    -                    it.totalNumberOfKeys, it.totalNumberOfKeys)
    -            val part2 = resources.getQuantityString(R.plurals.keys_backup_restore_success_description_part2,
    -                    it.successfullyNumberOfImportedKeys, it.successfullyNumberOfImportedKeys)
    -            mSuccessDetailsText.text = String.format("%s\n%s", part1, part2)
    +        if (compareValues(sharedViewModel.importKeyResult?.totalNumberOfKeys, 0) > 0) {
    +            sharedViewModel.importKeyResult?.let {
    +                val part1 = resources.getQuantityString(R.plurals.keys_backup_restore_success_description_part1,
    +                        it.totalNumberOfKeys, it.totalNumberOfKeys)
    +                val part2 = resources.getQuantityString(R.plurals.keys_backup_restore_success_description_part2,
    +                        it.successfullyNumberOfImportedKeys, it.successfullyNumberOfImportedKeys)
    +                mSuccessDetailsText.text = String.format("%s\n%s", part1, part2)
    +            }
    +            // We don't put emoji in string xml as it will crash on old devices
    +            mSuccessText.text = context?.getString(R.string.keys_backup_restore_success_title, "🎉")
    +        } else {
    +            mSuccessText.text = context?.getString(R.string.keys_backup_restore_success_title_already_up_to_date)
    +            mSuccessDetailsText.isVisible = false
             }
    -
    -        // We don't put emoji in string xml as it will crash on old devices
    -        mSuccessText.text = context?.getString(R.string.keys_backup_restore_success_title, "🎉")
         }
     
         @OnClick(R.id.keys_backup_setup_done_button)
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt
    index 7b60cb2f9b..4f2d806ce3 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt
    @@ -119,8 +119,8 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(private val s
                         style(GenericItem.STYLE.BIG_TEXT)
                         hasIndeterminateProcess(true)
     
    -                    val totalKeys = session.inboundGroupSessionsCount(false)
    -                    val backedUpKeys = session.inboundGroupSessionsCount(true)
    +                    val totalKeys = session.cryptoService().inboundGroupSessionsCount(false)
    +                    val backedUpKeys = session.cryptoService().inboundGroupSessionsCount(true)
     
                         val remainingKeysToBackup = totalKeys - backedUpKeys
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt
    index e4bbd39684..d2e977c209 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt
    @@ -15,7 +15,13 @@
      */
     package im.vector.riotx.features.crypto.keysbackup.settings
     
    -import com.airbnb.mvrx.*
    +import com.airbnb.mvrx.ActivityViewModelContext
    +import com.airbnb.mvrx.Fail
    +import com.airbnb.mvrx.Loading
    +import com.airbnb.mvrx.MvRxViewModelFactory
    +import com.airbnb.mvrx.Success
    +import com.airbnb.mvrx.Uninitialized
    +import com.airbnb.mvrx.ViewModelContext
     import com.squareup.inject.assisted.Assisted
     import com.squareup.inject.assisted.AssistedInject
     import im.vector.matrix.android.api.MatrixCallback
    @@ -47,13 +53,13 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialS
             }
         }
     
    -    private var keysBackupService: KeysBackupService = session.getKeysBackupService()
    +    private val keysBackupService: KeysBackupService = session.cryptoService().keysBackupService()
     
         init {
             setState {
                 this.copy(
    -                    keysBackupState = session.getKeysBackupService().state,
    -                    keysBackupVersion = session.getKeysBackupService().keysBackupVersion
    +                    keysBackupState = keysBackupService.state,
    +                    keysBackupVersion = keysBackupService.keysBackupVersion
                 )
             }
             keysBackupService.addListener(this)
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt
    index b77620b15b..924e25a4d7 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt
    @@ -27,7 +27,11 @@ import im.vector.riotx.core.dialogs.ExportKeysDialog
     import im.vector.riotx.core.extensions.observeEvent
     import im.vector.riotx.core.extensions.replaceFragment
     import im.vector.riotx.core.platform.SimpleFragmentActivity
    -import im.vector.riotx.core.utils.*
    +import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
    +import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_EXPORT_KEYS
    +import im.vector.riotx.core.utils.allGranted
    +import im.vector.riotx.core.utils.checkPermissions
    +import im.vector.riotx.core.utils.toast
     import im.vector.riotx.features.crypto.keys.KeysExporter
     
     class KeysBackupSetupActivity : SimpleFragmentActivity() {
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupSharedViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupSharedViewModel.kt
    index 8d9392a919..d9a90eb457 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupSharedViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupSharedViewModel.kt
    @@ -102,7 +102,7 @@ class KeysBackupSetupSharedViewModel @Inject constructor() : ViewModel() {
             session.let { mxSession ->
                 val requestedId = currentRequestId.value!!
     
    -            mxSession.getKeysBackupService().prepareKeysBackupVersion(withPassphrase,
    +            mxSession.cryptoService().keysBackupService().prepareKeysBackupVersion(withPassphrase,
                         object : ProgressListener {
                             override fun onProgress(progress: Int, total: Int) {
                                 if (requestedId != currentRequestId.value) {
    @@ -125,7 +125,7 @@ class KeysBackupSetupSharedViewModel @Inject constructor() : ViewModel() {
                                 megolmBackupCreationInfo = data
                                 copyHasBeenMade = false
     
    -                            val keyBackup = session.getKeysBackupService()
    +                            val keyBackup = session.cryptoService().keysBackupService()
                                 createKeysBackup(context, keyBackup)
                             }
     
    @@ -145,14 +145,14 @@ class KeysBackupSetupSharedViewModel @Inject constructor() : ViewModel() {
         }
     
         fun forceCreateKeyBackup(context: Context) {
    -        val keyBackup = session.getKeysBackupService()
    +        val keyBackup = session.cryptoService().keysBackupService()
             createKeysBackup(context, keyBackup, true)
         }
     
         fun stopAndKeepAfterDetectingExistingOnServer() {
             loadingStatus.value = null
             navigateEvent.value = LiveEvent(NAVIGATE_FINISH)
    -        session.getKeysBackupService().checkAndStartKeysBackup()
    +        session.cryptoService().keysBackupService().checkAndStartKeysBackup()
         }
     
         private fun createKeysBackup(context: Context, keysBackup: KeysBackupService, forceOverride: Boolean = false) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt
    index 52470a47b2..a224cfb387 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt
    @@ -32,7 +32,13 @@ import im.vector.riotx.R
     import im.vector.riotx.core.files.addEntryToDownloadManager
     import im.vector.riotx.core.files.writeToFile
     import im.vector.riotx.core.platform.VectorBaseFragment
    -import im.vector.riotx.core.utils.*
    +import im.vector.riotx.core.utils.LiveEvent
    +import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
    +import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_EXPORT_KEYS
    +import im.vector.riotx.core.utils.allGranted
    +import im.vector.riotx.core.utils.checkPermissions
    +import im.vector.riotx.core.utils.copyToClipboard
    +import im.vector.riotx.core.utils.startSharePlainTextIntent
     import kotlinx.coroutines.Dispatchers
     import kotlinx.coroutines.GlobalScope
     import kotlinx.coroutines.launch
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt
    index f890aef91b..534de09ce0 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt
    @@ -23,10 +23,10 @@ import android.content.Context
     import im.vector.matrix.android.api.MatrixCallback
     import im.vector.matrix.android.api.session.Session
     import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener
    -import im.vector.matrix.android.api.session.crypto.sas.VerificationService
    -import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
    -import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
    -import im.vector.matrix.android.api.session.crypto.sas.VerificationTransaction
    +import im.vector.matrix.android.api.session.crypto.verification.SasVerificationTransaction
    +import im.vector.matrix.android.api.session.crypto.verification.VerificationService
    +import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
    +import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
     import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
     import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequestCancellation
     import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
    @@ -39,11 +39,10 @@ import im.vector.riotx.features.popup.PopupAlertManager
     import timber.log.Timber
     import java.text.DateFormat
     import java.text.SimpleDateFormat
    -import java.util.*
    +import java.util.Date
    +import java.util.Locale
     import javax.inject.Inject
     import javax.inject.Singleton
    -import kotlin.collections.ArrayList
    -import kotlin.collections.HashMap
     
     /**
      * Manage the key share events.
    @@ -64,13 +63,13 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
     
         fun start(session: Session) {
             this.session = session
    -        session.getVerificationService().addListener(this)
    -        session.addRoomKeysRequestListener(this)
    +        session.cryptoService().verificationService().addListener(this)
    +        session.cryptoService().addRoomKeysRequestListener(this)
         }
     
         fun stop() {
    -        session?.getVerificationService()?.removeListener(this)
    -        session?.removeRoomKeysRequestListener(this)
    +        session?.cryptoService()?.verificationService()?.removeListener(this)
    +        session?.cryptoService()?.removeRoomKeysRequestListener(this)
             session = null
         }
     
    @@ -100,7 +99,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
             alertsToRequests[mappingKey] = ArrayList().apply { this.add(request) }
     
             // Add a notification for every incoming request
    -        session?.downloadKeys(listOf(userId), false, object : MatrixCallback> {
    +        session?.cryptoService()?.downloadKeys(listOf(userId), false, object : MatrixCallback> {
                 override fun onSuccess(data: MXUsersDevicesMap) {
                     val deviceInfo = data.getObject(userId, deviceId)
     
    @@ -111,12 +110,12 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
                     }
     
                     if (deviceInfo.isUnknown) {
    -                    session?.setDeviceVerification(DeviceTrustLevel(false, false), userId, deviceId)
    +                    session?.cryptoService()?.setDeviceVerification(DeviceTrustLevel(false, false), userId, deviceId)
     
                         deviceInfo.trustLevel = DeviceTrustLevel(false, false)
     
                         // can we get more info on this device?
    -                    session?.getDevicesList(object : MatrixCallback {
    +                    session?.cryptoService()?.getDevicesList(object : MatrixCallback {
                             override fun onSuccess(data: DevicesListResponse) {
                                 data.devices?.find { it.deviceId == deviceId }?.let {
                                     postAlert(context, userId, deviceId, true, deviceInfo, it)
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecureStorageAction.kt b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecureStorageAction.kt
    new file mode 100644
    index 0000000000..c2c2b9c42a
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecureStorageAction.kt
    @@ -0,0 +1,39 @@
    +/*
    + * Copyright (c) 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.riotx.features.crypto.quads
    +
    +import im.vector.riotx.core.platform.VectorViewEvents
    +import im.vector.riotx.core.platform.VectorViewModelAction
    +import im.vector.riotx.core.platform.WaitingViewData
    +
    +sealed class SharedSecureStorageAction : VectorViewModelAction {
    +
    +    object TogglePasswordVisibility : SharedSecureStorageAction()
    +    object Cancel : SharedSecureStorageAction()
    +    data class SubmitPassphrase(val passphrase: String) : SharedSecureStorageAction()
    +}
    +
    +sealed class SharedSecureStorageViewEvent : VectorViewEvents {
    +
    +    object Dismiss : SharedSecureStorageViewEvent()
    +    data class FinishSuccess(val cypherResult: String) : SharedSecureStorageViewEvent()
    +    data class Error(val message: String, val dismiss: Boolean = false) : SharedSecureStorageViewEvent()
    +    data class InlineError(val message: String) : SharedSecureStorageViewEvent()
    +    object ShowModalLoading : SharedSecureStorageViewEvent()
    +    object HideModalLoading : SharedSecureStorageViewEvent()
    +    data class UpdateLoadingState(val waitingData: WaitingViewData) : SharedSecureStorageViewEvent()
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecureStorageActivity.kt b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecureStorageActivity.kt
    new file mode 100644
    index 0000000000..1347b6ca19
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecureStorageActivity.kt
    @@ -0,0 +1,129 @@
    +/*
    + * Copyright (c) 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.riotx.features.crypto.quads
    +
    +import android.app.Activity
    +import android.content.Context
    +import android.content.Intent
    +import android.os.Bundle
    +import android.os.Parcelable
    +import android.view.View
    +import androidx.appcompat.app.AlertDialog
    +import com.airbnb.mvrx.MvRx
    +import com.airbnb.mvrx.viewModel
    +import im.vector.riotx.R
    +import im.vector.riotx.core.di.ScreenComponent
    +import im.vector.riotx.core.error.ErrorFormatter
    +import im.vector.riotx.core.extensions.addFragment
    +import im.vector.riotx.core.platform.SimpleFragmentActivity
    +import io.reactivex.android.schedulers.AndroidSchedulers
    +import kotlinx.android.parcel.Parcelize
    +import kotlinx.android.synthetic.main.activity.*
    +import javax.inject.Inject
    +
    +class SharedSecureStorageActivity : SimpleFragmentActivity() {
    +
    +    @Parcelize
    +    data class Args(
    +            val keyId: String?,
    +            val requestedSecrets: List,
    +            val resultKeyStoreAlias: String
    +    ) : Parcelable
    +
    +    private val viewModel: SharedSecureStorageViewModel by viewModel()
    +    @Inject lateinit var viewModelFactory: SharedSecureStorageViewModel.Factory
    +    @Inject lateinit var errorFormatter: ErrorFormatter
    +
    +    override fun injectWith(injector: ScreenComponent) {
    +        super.injectWith(injector)
    +        injector.inject(this)
    +    }
    +
    +    override fun onCreate(savedInstanceState: Bundle?) {
    +        super.onCreate(savedInstanceState)
    +        toolbar.visibility = View.GONE
    +        if (isFirstCreation()) {
    +            addFragment(R.id.container, SharedSecuredStoragePassphraseFragment::class.java)
    +        }
    +
    +        viewModel.viewEvents
    +                .observe()
    +                .observeOn(AndroidSchedulers.mainThread())
    +                .subscribe {
    +                    observeViewEvents(it)
    +                }
    +                .disposeOnDestroy()
    +
    +        viewModel.subscribe(this) {
    +            //            renderState(it)
    +        }
    +    }
    +
    +    private fun observeViewEvents(it: SharedSecureStorageViewEvent?) {
    +        when (it) {
    +            is SharedSecureStorageViewEvent.Dismiss            -> {
    +                finish()
    +            }
    +            is SharedSecureStorageViewEvent.Error              -> {
    +                AlertDialog.Builder(this)
    +                        .setTitle(getString(R.string.dialog_title_error))
    +                        .setMessage(it.message)
    +                        .setCancelable(false)
    +                        .setPositiveButton(R.string.ok) { _, _ ->
    +                            if (it.dismiss) {
    +                                finish()
    +                            }
    +                        }
    +                        .show()
    +            }
    +            is SharedSecureStorageViewEvent.ShowModalLoading   -> {
    +                showWaitingView()
    +            }
    +            is SharedSecureStorageViewEvent.HideModalLoading   -> {
    +                hideWaitingView()
    +            }
    +            is SharedSecureStorageViewEvent.UpdateLoadingState -> {
    +                updateWaitingView(it.waitingData)
    +            }
    +            is SharedSecureStorageViewEvent.FinishSuccess      -> {
    +                val dataResult = Intent()
    +                dataResult.putExtra(EXTRA_DATA_RESULT, it.cypherResult)
    +                setResult(Activity.RESULT_OK, dataResult)
    +                finish()
    +            }
    +        }
    +    }
    +
    +    companion object {
    +        const val EXTRA_DATA_RESULT = "EXTRA_DATA_RESULT"
    +        const val DEFAULT_RESULT_KEYSTORE_ALIAS = "SharedSecureStorageActivity"
    +
    +        fun newIntent(context: Context,
    +                      keyId: String? = null,
    +                      requestedSecrets: List,
    +                      resultKeyStoreAlias: String = DEFAULT_RESULT_KEYSTORE_ALIAS): Intent {
    +            require(requestedSecrets.isNotEmpty())
    +            return Intent(context, SharedSecureStorageActivity::class.java).also {
    +                it.putExtra(MvRx.KEY_ARG, Args(
    +                        keyId,
    +                        requestedSecrets,
    +                        resultKeyStoreAlias
    +                ))
    +            }
    +        }
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecureStorageViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecureStorageViewModel.kt
    new file mode 100644
    index 0000000000..a9f5d33888
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecureStorageViewModel.kt
    @@ -0,0 +1,165 @@
    +/*
    + * Copyright (c) 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.riotx.features.crypto.quads
    +
    +import com.airbnb.mvrx.MvRx
    +import com.airbnb.mvrx.MvRxState
    +import com.airbnb.mvrx.MvRxViewModelFactory
    +import com.airbnb.mvrx.ViewModelContext
    +import com.squareup.inject.assisted.Assisted
    +import com.squareup.inject.assisted.AssistedInject
    +import im.vector.matrix.android.api.listeners.ProgressListener
    +import im.vector.matrix.android.api.session.Session
    +import im.vector.matrix.android.api.session.securestorage.IntegrityResult
    +import im.vector.matrix.android.api.session.securestorage.KeyInfoResult
    +import im.vector.matrix.android.api.session.securestorage.RawBytesKeySpec
    +import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
    +import im.vector.matrix.android.internal.util.awaitCallback
    +import im.vector.riotx.R
    +import im.vector.riotx.core.platform.VectorViewModel
    +import im.vector.riotx.core.platform.WaitingViewData
    +import im.vector.riotx.core.resources.StringProvider
    +import kotlinx.coroutines.Dispatchers
    +import kotlinx.coroutines.GlobalScope
    +import kotlinx.coroutines.launch
    +import kotlinx.coroutines.withContext
    +import java.io.ByteArrayOutputStream
    +
    +data class SharedSecureStorageViewState(
    +        val passphraseVisible: Boolean = false
    +) : MvRxState
    +
    +class SharedSecureStorageViewModel @AssistedInject constructor(
    +        @Assisted initialState: SharedSecureStorageViewState,
    +        @Assisted val args: SharedSecureStorageActivity.Args,
    +        private val stringProvider: StringProvider,
    +        private val session: Session)
    +    : VectorViewModel(initialState) {
    +
    +    @AssistedInject.Factory
    +    interface Factory {
    +        fun create(initialState: SharedSecureStorageViewState, args: SharedSecureStorageActivity.Args): SharedSecureStorageViewModel
    +    }
    +
    +    init {
    +        val isValid = session.sharedSecretStorageService.checkShouldBeAbleToAccessSecrets(args.requestedSecrets, args.keyId) is IntegrityResult.Success
    +        if (!isValid) {
    +            _viewEvents.post(
    +                    SharedSecureStorageViewEvent.Error(
    +                            stringProvider.getString(R.string.enter_secret_storage_invalid),
    +                            true
    +                    )
    +            )
    +        }
    +    }
    +
    +    override fun handle(action: SharedSecureStorageAction) = withState {
    +        when (action) {
    +            is SharedSecureStorageAction.TogglePasswordVisibility -> handleTogglePasswordVisibility()
    +            is SharedSecureStorageAction.Cancel                   -> handleCancel()
    +            is SharedSecureStorageAction.SubmitPassphrase         -> handleSubmitPassphrase(action)
    +        }
    +    }
    +
    +    private fun handleSubmitPassphrase(action: SharedSecureStorageAction.SubmitPassphrase) {
    +        val decryptedSecretMap = HashMap()
    +        GlobalScope.launch(Dispatchers.IO) {
    +            runCatching {
    +                _viewEvents.post(SharedSecureStorageViewEvent.ShowModalLoading)
    +                val passphrase = action.passphrase
    +                val keyInfoResult = session.sharedSecretStorageService.getDefaultKey()
    +                if (!keyInfoResult.isSuccess()) {
    +                    _viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
    +                    _viewEvents.post(SharedSecureStorageViewEvent.Error("Cannot find ssss key"))
    +                    return@launch
    +                }
    +                val keyInfo = (keyInfoResult as KeyInfoResult.Success).keyInfo
    +
    +                _viewEvents.post(SharedSecureStorageViewEvent.UpdateLoadingState(
    +                        WaitingViewData(
    +                                message = stringProvider.getString(R.string.keys_backup_restoring_computing_key_waiting_message),
    +                                isIndeterminate = true
    +                        )
    +                ))
    +                val keySpec = RawBytesKeySpec.fromPassphrase(
    +                        passphrase,
    +                        keyInfo.content.passphrase?.salt ?: "",
    +                        keyInfo.content.passphrase?.iterations ?: 0,
    +                        // TODO
    +                        object : ProgressListener {
    +                            override fun onProgress(progress: Int, total: Int) {
    +                                _viewEvents.post(SharedSecureStorageViewEvent.UpdateLoadingState(
    +                                        WaitingViewData(
    +                                                message = stringProvider.getString(R.string.keys_backup_restoring_computing_key_waiting_message),
    +                                                isIndeterminate = false,
    +                                                progress = progress,
    +                                                progressTotal = total
    +                                        )
    +                                ))
    +                            }
    +                        }
    +                )
    +
    +                withContext(Dispatchers.IO) {
    +                    args.requestedSecrets.forEach {
    +                        val res = awaitCallback { callback ->
    +                            session.sharedSecretStorageService.getSecret(
    +                                    name = it,
    +                                    keyId = keyInfo.id,
    +                                    secretKey = keySpec,
    +                                    callback = callback)
    +                        }
    +                        decryptedSecretMap[it] = res
    +                    }
    +                }
    +            }.fold({
    +                _viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
    +                val safeForIntentCypher = ByteArrayOutputStream().also {
    +                    it.use {
    +                        session.securelyStoreObject(decryptedSecretMap as Map, args.resultKeyStoreAlias, it)
    +                    }
    +                }.toByteArray().toBase64NoPadding()
    +                _viewEvents.post(SharedSecureStorageViewEvent.FinishSuccess(safeForIntentCypher))
    +            }, {
    +                _viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
    +                _viewEvents.post(SharedSecureStorageViewEvent.InlineError(stringProvider.getString(R.string.keys_backup_passphrase_error_decrypt)))
    +            })
    +        }
    +    }
    +
    +    private fun handleCancel() {
    +        _viewEvents.post(SharedSecureStorageViewEvent.Dismiss)
    +    }
    +
    +    private fun handleTogglePasswordVisibility() {
    +        setState {
    +            copy(
    +                    passphraseVisible = !passphraseVisible
    +            )
    +        }
    +    }
    +
    +    companion object : MvRxViewModelFactory {
    +
    +        @JvmStatic
    +        override fun create(viewModelContext: ViewModelContext, state: SharedSecureStorageViewState): SharedSecureStorageViewModel? {
    +            val activity: SharedSecureStorageActivity = viewModelContext.activity()
    +            val args: SharedSecureStorageActivity.Args = activity.intent.getParcelableExtra(MvRx.KEY_ARG)
    +            return activity.viewModelFactory.create(state, args)
    +        }
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt
    new file mode 100644
    index 0000000000..f7a5e7c1bc
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt
    @@ -0,0 +1,116 @@
    +/*
    + * Copyright (c) 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.riotx.features.crypto.quads
    +
    +import android.os.Bundle
    +import android.view.View
    +import android.view.inputmethod.EditorInfo
    +import com.airbnb.mvrx.activityViewModel
    +import com.airbnb.mvrx.withState
    +import com.jakewharton.rxbinding3.view.clicks
    +import com.jakewharton.rxbinding3.widget.editorActionEvents
    +import com.jakewharton.rxbinding3.widget.textChanges
    +import im.vector.riotx.R
    +import im.vector.riotx.core.extensions.showPassword
    +import im.vector.riotx.core.platform.VectorBaseFragment
    +import io.reactivex.android.schedulers.AndroidSchedulers
    +import kotlinx.android.synthetic.main.fragment_ssss_access_from_passphrase.*
    +import me.gujun.android.span.span
    +import java.util.concurrent.TimeUnit
    +
    +class SharedSecuredStoragePassphraseFragment : VectorBaseFragment() {
    +
    +    override fun getLayoutResId() = R.layout.fragment_ssss_access_from_passphrase
    +
    +    val sharedViewModel: SharedSecureStorageViewModel by activityViewModel()
    +
    +    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    +        super.onViewCreated(view, savedInstanceState)
    +
    +        ssss_restore_with_passphrase_warning_text.text = span {
    +            span(getString(R.string.enter_secret_storage_passphrase_warning)) {
    +                textStyle = "bold"
    +            }
    +            +" "
    +            +getString(R.string.enter_secret_storage_passphrase_warning_text)
    +        }
    +
    +        ssss_restore_with_passphrase_warning_reason.text = getString(R.string.enter_secret_storage_passphrase_reason_verify)
    +
    +        ssss_passphrase_enter_edittext.editorActionEvents()
    +                .debounce(300, TimeUnit.MILLISECONDS)
    +                .observeOn(AndroidSchedulers.mainThread())
    +                .subscribe {
    +                    if (it.actionId == EditorInfo.IME_ACTION_DONE) {
    +                        submit()
    +                    }
    +                }
    +                .disposeOnDestroyView()
    +
    +        ssss_passphrase_enter_edittext.textChanges()
    +                .subscribe {
    +                    ssss_passphrase_enter_til.error = null
    +                    ssss_passphrase_submit.isEnabled = it.isNotBlank()
    +                }
    +                .disposeOnDestroyView()
    +
    +        sharedViewModel.observeViewEvents {
    +            when (it) {
    +                is SharedSecureStorageViewEvent.InlineError -> {
    +                    ssss_passphrase_enter_til.error = it.message
    +                }
    +            }
    +        }
    +
    +        ssss_passphrase_submit.clicks()
    +                .debounce(300, TimeUnit.MILLISECONDS)
    +                .observeOn(AndroidSchedulers.mainThread())
    +                .subscribe {
    +                    submit()
    +                }
    +                .disposeOnDestroyView()
    +
    +        ssss_passphrase_cancel.clicks()
    +                .debounce(300, TimeUnit.MILLISECONDS)
    +                .observeOn(AndroidSchedulers.mainThread())
    +                .subscribe {
    +                    sharedViewModel.handle(SharedSecureStorageAction.Cancel)
    +                }
    +                .disposeOnDestroyView()
    +
    +        ssss_view_show_password.clicks()
    +                .debounce(300, TimeUnit.MILLISECONDS)
    +                .observeOn(AndroidSchedulers.mainThread())
    +                .subscribe {
    +                    sharedViewModel.handle(SharedSecureStorageAction.TogglePasswordVisibility)
    +                }
    +                .disposeOnDestroyView()
    +    }
    +
    +    fun submit() {
    +        val text = ssss_passphrase_enter_edittext.text.toString()
    +        if (text.isBlank()) return // Should not reach this point as button disabled
    +        ssss_passphrase_submit.isEnabled = false
    +        sharedViewModel.handle(SharedSecureStorageAction.SubmitPassphrase(text))
    +    }
    +
    +    override fun invalidate() = withState(sharedViewModel) { state ->
    +        val shouldBeVisible = state.passphraseVisible
    +        ssss_passphrase_enter_edittext.showPassword(shouldBeVisible)
    +        ssss_view_show_password.setImageResource(if (shouldBeVisible) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black)
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/Config.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/Config.kt
    index fae7037403..80f2b1153a 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/Config.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/Config.kt
    @@ -16,7 +16,7 @@
     
     package im.vector.riotx.features.crypto.verification
     
    -import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod
    +import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod
     
     val supportedVerificationMethods =
             listOf(
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/IncomingVerificationRequestHandler.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/IncomingVerificationRequestHandler.kt
    index 8765dbc0d9..2a19dd8784 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/IncomingVerificationRequestHandler.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/IncomingVerificationRequestHandler.kt
    @@ -17,9 +17,9 @@ package im.vector.riotx.features.crypto.verification
     
     import android.content.Context
     import im.vector.matrix.android.api.session.Session
    -import im.vector.matrix.android.api.session.crypto.sas.VerificationService
    -import im.vector.matrix.android.api.session.crypto.sas.VerificationTransaction
    -import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
    +import im.vector.matrix.android.api.session.crypto.verification.VerificationService
    +import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
    +import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
     import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest
     import im.vector.riotx.R
     import im.vector.riotx.core.platform.VectorBaseActivity
    @@ -40,11 +40,11 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context
     
         fun start(session: Session) {
             this.session = session
    -        session.getVerificationService().addListener(this)
    +        session.cryptoService().verificationService().addListener(this)
         }
     
         fun stop() {
    -        session?.getVerificationService()?.removeListener(this)
    +        session?.cryptoService()?.verificationService()?.removeListener(this)
             this.session = null
         }
     
    @@ -139,7 +139,7 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context
                                 }
                             }
                             dismissedAction = Runnable {
    -                            session?.getVerificationService()?.declineVerificationRequestInDMs(pr.otherUserId,
    +                            session?.cryptoService()?.verificationService()?.declineVerificationRequestInDMs(pr.otherUserId,
                                         pr.transactionId ?: "",
                                         pr.roomId ?: ""
                                 )
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationAction.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationAction.kt
    index 74c85a75c6..79facb71d0 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationAction.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationAction.kt
    @@ -28,4 +28,7 @@ sealed class VerificationAction : VectorViewModelAction {
         data class SASMatchAction(val otherUserId: String, val sasTransactionId: String) : VerificationAction()
         data class SASDoNotMatchAction(val otherUserId: String, val sasTransactionId: String) : VerificationAction()
         object GotItConclusion : VerificationAction()
    +    object SkipVerification : VerificationAction()
    +    object VerifyFromPassphrase : VerificationAction()
    +    data class GotResultFromSsss(val cypherData: String, val alias: String) : VerificationAction()
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt
    index 75983a1969..d43c2d01b2 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt
    @@ -15,26 +15,32 @@
      */
     package im.vector.riotx.features.crypto.verification
     
    +import android.app.Activity
    +import android.content.Intent
     import android.os.Bundle
     import android.os.Parcelable
     import android.view.View
     import android.widget.ImageView
     import android.widget.TextView
    +import androidx.appcompat.app.AlertDialog
     import androidx.coordinatorlayout.widget.CoordinatorLayout
     import androidx.core.view.isVisible
     import androidx.fragment.app.Fragment
    -import androidx.transition.AutoTransition
    -import androidx.transition.TransitionManager
     import butterknife.BindView
     import com.airbnb.mvrx.MvRx
     import com.airbnb.mvrx.fragmentViewModel
     import com.airbnb.mvrx.withState
    -import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
    +import im.vector.matrix.android.api.session.Session
    +import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
    +import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
    +import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
    +import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
     import im.vector.riotx.R
     import im.vector.riotx.core.di.ScreenComponent
    -import im.vector.riotx.core.extensions.commitTransactionNow
    +import im.vector.riotx.core.extensions.commitTransaction
     import im.vector.riotx.core.extensions.exhaustive
     import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
    +import im.vector.riotx.features.crypto.quads.SharedSecureStorageActivity
     import im.vector.riotx.features.crypto.verification.choose.VerificationChooseMethodFragment
     import im.vector.riotx.features.crypto.verification.conclusion.VerificationConclusionFragment
     import im.vector.riotx.features.crypto.verification.emoji.VerificationEmojiCodeFragment
    @@ -42,7 +48,6 @@ import im.vector.riotx.features.crypto.verification.qrconfirmation.VerificationQ
     import im.vector.riotx.features.crypto.verification.request.VerificationRequestFragment
     import im.vector.riotx.features.home.AvatarRenderer
     import kotlinx.android.parcel.Parcelize
    -import kotlinx.android.synthetic.main.bottom_sheet_verification.*
     import timber.log.Timber
     import javax.inject.Inject
     import kotlin.reflect.KClass
    @@ -54,10 +59,12 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
                 val otherUserId: String,
                 val verificationId: String? = null,
                 val roomId: String? = null,
    -            // Special mode where UX should show loading wheel until other user sends a request/tx
    -            val waitForIncomingRequest: Boolean = false
    +            // Special mode where UX should show loading wheel until other session sends a request/tx
    +            val selfVerificationMode: Boolean = false
         ) : Parcelable
     
    +    override val showExpanded = true
    +
         @Inject
         lateinit var verificationViewModelFactory: VerificationBottomSheetViewModel.Factory
         @Inject
    @@ -85,15 +92,44 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
     
             viewModel.observeViewEvents {
                 when (it) {
    -                is VerificationBottomSheetViewEvents.Dismiss -> dismiss()
    +                is VerificationBottomSheetViewEvents.Dismiss           -> dismiss()
    +                is VerificationBottomSheetViewEvents.AccessSecretStore -> {
    +                    startActivityForResult(SharedSecureStorageActivity.newIntent(
    +                            requireContext(),
    +                            null, // use default key
    +                            listOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME),
    +                            SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS
    +                    ), SECRET_REQUEST_CODE)
    +                }
    +                is VerificationBottomSheetViewEvents.ModalError        -> {
    +                    AlertDialog.Builder(requireContext())
    +                            .setTitle(getString(R.string.dialog_title_error))
    +                            .setMessage(it.errorMessage)
    +                            .setCancelable(false)
    +                            .setPositiveButton(R.string.ok, null)
    +                            .show()
    +                    Unit
    +                }
                 }.exhaustive
             }
         }
     
    +    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    +        if (resultCode == Activity.RESULT_OK && requestCode == SECRET_REQUEST_CODE) {
    +            data?.getStringExtra(SharedSecureStorageActivity.EXTRA_DATA_RESULT)?.let {
    +                viewModel.handle(VerificationAction.GotResultFromSsss(it, SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS))
    +            }
    +        }
    +        super.onActivityResult(requestCode, resultCode, data)
    +    }
    +
         override fun invalidate() = withState(viewModel) { state ->
    +
             state.otherUserMxItem?.let { matrixItem ->
                 if (state.isMe) {
    -                if (state.sasTransactionState == VerificationTxState.Verified || state.qrTransactionState == VerificationTxState.Verified) {
    +                if (state.sasTransactionState == VerificationTxState.Verified
    +                        || state.qrTransactionState == VerificationTxState.Verified
    +                        || state.verifiedFromPrivateKeys) {
                         otherUserAvatarImageView.setImageResource(R.drawable.ic_shield_trusted)
                     } else {
                         otherUserAvatarImageView.setImageResource(R.drawable.ic_shield_warning)
    @@ -113,6 +149,13 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
                 }
             }
     
    +        if (state.selfVerificationMode && state.verifiedFromPrivateKeys) {
    +            showFragment(VerificationConclusionFragment::class, Bundle().apply {
    +                putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(true, null, state.isMe))
    +            })
    +            return@withState
    +        }
    +
             // Did the request result in a SAS transaction?
             if (state.sasTransactionState != null) {
                 when (state.sasTransactionState) {
    @@ -183,7 +226,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
             }
     
             // If it's an outgoing
    -        if (state.pendingRequest.invoke() == null || state.pendingRequest.invoke()?.isIncoming == false || state.waitForOtherUserMode) {
    +        if (state.pendingRequest.invoke() == null || state.pendingRequest.invoke()?.isIncoming == false || state.selfVerificationMode) {
                 Timber.v("## SAS show bottom sheet for outgoing request")
                 if (state.pendingRequest.invoke()?.isReady == true) {
                     Timber.v("## SAS show bottom sheet for outgoing and ready request")
    @@ -214,12 +257,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
     
         private fun showFragment(fragmentClass: KClass, bundle: Bundle) {
             if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) {
    -            // We want to animate the bottomsheet bound changes
    -            bottomSheetFragmentContainer.getParentCoordinatorLayout()?.let { coordinatorLayout ->
    -                TransitionManager.beginDelayedTransition(coordinatorLayout, AutoTransition().apply { duration = 150 })
    -            }
    -            // Commit now, to ensure changes occurs before next rendering frame (or bottomsheet want animate)
    -            childFragmentManager.commitTransactionNow {
    +            childFragmentManager.commitTransaction {
                     replace(R.id.bottomSheetFragmentContainer,
                             fragmentClass.java,
                             bundle,
    @@ -230,14 +268,28 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
         }
     
         companion object {
    -        fun withArgs(roomId: String?, otherUserId: String, transactionId: String? = null, waitForIncomingRequest: Boolean = false): VerificationBottomSheet {
    +
    +        const val SECRET_REQUEST_CODE = 101
    +
    +        fun withArgs(roomId: String?, otherUserId: String, transactionId: String? = null): VerificationBottomSheet {
                 return VerificationBottomSheet().apply {
                     arguments = Bundle().apply {
                         putParcelable(MvRx.KEY_ARG, VerificationArgs(
                                 otherUserId = otherUserId,
                                 roomId = roomId,
                                 verificationId = transactionId,
    -                            waitForIncomingRequest = waitForIncomingRequest
    +                            selfVerificationMode = false
    +                    ))
    +                }
    +            }
    +        }
    +
    +        fun forSelfVerification(session: Session): VerificationBottomSheet {
    +            return VerificationBottomSheet().apply {
    +                arguments = Bundle().apply {
    +                    putParcelable(MvRx.KEY_ARG, VerificationArgs(
    +                            otherUserId = session.myUserId,
    +                            selfVerificationMode = true
                         ))
                     }
                 }
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewEvents.kt
    index 5509ecbe16..d7c02a8d3b 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewEvents.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewEvents.kt
    @@ -23,4 +23,6 @@ import im.vector.riotx.core.platform.VectorViewEvents
      */
     sealed class VerificationBottomSheetViewEvents : VectorViewEvents {
         object Dismiss : VerificationBottomSheetViewEvents()
    +    object AccessSecretStore : VerificationBottomSheetViewEvents()
    +    data class ModalError(val errorMessage: CharSequence) : VerificationBottomSheetViewEvents()
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt
    index 85b878fe16..c5fd167f39 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt
    @@ -28,20 +28,26 @@ import com.squareup.inject.assisted.Assisted
     import com.squareup.inject.assisted.AssistedInject
     import im.vector.matrix.android.api.MatrixCallback
     import im.vector.matrix.android.api.session.Session
    -import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction
    -import im.vector.matrix.android.api.session.crypto.sas.QrCodeVerificationTransaction
    -import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
    -import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod
    -import im.vector.matrix.android.api.session.crypto.sas.VerificationService
    -import im.vector.matrix.android.api.session.crypto.sas.VerificationTransaction
    -import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
    +import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
    +import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
    +import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
    +import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction
    +import im.vector.matrix.android.api.session.crypto.verification.QrCodeVerificationTransaction
    +import im.vector.matrix.android.api.session.crypto.verification.SasVerificationTransaction
    +import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod
    +import im.vector.matrix.android.api.session.crypto.verification.VerificationService
    +import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
    +import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
     import im.vector.matrix.android.api.session.events.model.LocalEcho
     import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
     import im.vector.matrix.android.api.util.MatrixItem
     import im.vector.matrix.android.api.util.toMatrixItem
    +import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64NoPadding
    +import im.vector.matrix.android.internal.crypto.crosssigning.isVerified
     import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest
     import im.vector.riotx.core.extensions.exhaustive
     import im.vector.riotx.core.platform.VectorViewModel
    +import timber.log.Timber
     
     data class VerificationBottomSheetViewState(
             val otherUserMxItem: MatrixItem? = null,
    @@ -52,7 +58,8 @@ data class VerificationBottomSheetViewState(
             val qrTransactionState: VerificationTxState? = null,
             val transactionId: String? = null,
             // true when we display the loading and we wait for the other (incoming request)
    -        val waitForOtherUserMode: Boolean = false,
    +        val selfVerificationMode: Boolean = false,
    +        val verifiedFromPrivateKeys: Boolean = false,
             val isMe: Boolean = false
     ) : MvRxState
     
    @@ -63,17 +70,17 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
             VerificationService.Listener {
     
         init {
    -        session.getVerificationService().addListener(this)
    +        session.cryptoService().verificationService().addListener(this)
     
             val userItem = session.getUser(args.otherUserId)
     
    -        val isWaitingForOtherMode = args.waitForIncomingRequest
    +        val selfVerificationMode = args.selfVerificationMode
     
             var autoReady = false
    -        val pr = if (isWaitingForOtherMode) {
    +        val pr = if (selfVerificationMode) {
                 // See if active tx for this user and take it
     
    -            session.getVerificationService().getExistingVerificationRequest(args.otherUserId)
    +            session.cryptoService().verificationService().getExistingVerificationRequest(args.otherUserId)
                         ?.lastOrNull { !it.isFinished }
                         ?.also { verificationRequest ->
                             if (verificationRequest.isIncoming && !verificationRequest.isReady) {
    @@ -82,15 +89,15 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
                             }
                         }
             } else {
    -            session.getVerificationService().getExistingVerificationRequest(args.otherUserId, args.verificationId)
    +            session.cryptoService().verificationService().getExistingVerificationRequest(args.otherUserId, args.verificationId)
             }
     
             val sasTx = (pr?.transactionId ?: args.verificationId)?.let {
    -            session.getVerificationService().getExistingTransaction(args.otherUserId, it) as? SasVerificationTransaction
    +            session.cryptoService().verificationService().getExistingTransaction(args.otherUserId, it) as? SasVerificationTransaction
             }
     
             val qrTx = (pr?.transactionId ?: args.verificationId)?.let {
    -            session.getVerificationService().getExistingTransaction(args.otherUserId, it) as? QrCodeVerificationTransaction
    +            session.cryptoService().verificationService().getExistingTransaction(args.otherUserId, it) as? QrCodeVerificationTransaction
             }
     
             setState {
    @@ -100,7 +107,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
                         qrTransactionState = qrTx?.state,
                         transactionId = pr?.transactionId ?: args.verificationId,
                         pendingRequest = if (pr != null) Success(pr) else Uninitialized,
    -                    waitForOtherUserMode = isWaitingForOtherMode,
    +                    selfVerificationMode = selfVerificationMode,
                         roomId = args.roomId,
                         isMe = args.otherUserId == session.myUserId
                 )
    @@ -108,7 +115,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
     
             if (autoReady) {
                 // TODO, can I be here in DM mode? in this case should test if roomID is null?
    -            session.getVerificationService()
    +            session.cryptoService().verificationService()
                         .readyPendingVerification(supportedVerificationMethods,
                                 pr!!.otherUserId,
                                 pr.transactionId ?: "")
    @@ -116,7 +123,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
         }
     
         override fun onCleared() {
    -        session.getVerificationService().removeListener(this)
    +        session.cryptoService().verificationService().removeListener(this)
             super.onCleared()
         }
     
    @@ -164,7 +171,8 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
                                             roomId = data,
                                             pendingRequest = Success(
                                                     session
    -                                                        .getVerificationService()
    +                                                        .cryptoService()
    +                                                        .verificationService()
                                                             .requestKeyVerificationInDMs(supportedVerificationMethods, otherUserId, data, pendingLocalId)
                                             )
                                     )
    @@ -181,7 +189,8 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
                         setState {
                             copy(
                                     pendingRequest = Success(session
    -                                        .getVerificationService()
    +                                        .cryptoService()
    +                                        .verificationService()
                                             .requestKeyVerificationInDMs(supportedVerificationMethods, otherUserId, roomId)
                                     )
                             )
    @@ -190,18 +199,18 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
                     Unit
                 }
                 is VerificationAction.StartSASVerification         -> {
    -                val request = session.getVerificationService().getExistingVerificationRequest(otherUserId, action.pendingRequestTransactionId)
    +                val request = session.cryptoService().verificationService().getExistingVerificationRequest(otherUserId, action.pendingRequestTransactionId)
                             ?: return@withState
                     val otherDevice = if (request.isIncoming) request.requestInfo?.fromDevice else request.readyInfo?.fromDevice
                     if (roomId == null) {
    -                    session.getVerificationService().beginKeyVerification(
    +                    session.cryptoService().verificationService().beginKeyVerification(
                                 VerificationMethod.SAS,
                                 otherUserId = request.otherUserId,
                                 otherDeviceId = otherDevice ?: "",
                                 transactionId = action.pendingRequestTransactionId
                         )
                     } else {
    -                    session.getVerificationService().beginKeyVerificationInDMs(
    +                    session.cryptoService().verificationService().beginKeyVerificationInDMs(
                                 VerificationMethod.SAS,
                                 transactionId = action.pendingRequestTransactionId,
                                 roomId = roomId,
    @@ -213,7 +222,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
                     Unit
                 }
                 is VerificationAction.RemoteQrCodeScanned          -> {
    -                val existingTransaction = session.getVerificationService()
    +                val existingTransaction = session.cryptoService().verificationService()
                             .getExistingTransaction(action.otherUserId, action.transactionId) as? QrCodeVerificationTransaction
                     existingTransaction
                             ?.userHasScannedOtherQrCode(action.scannedData)
    @@ -221,7 +230,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
                 is VerificationAction.OtherUserScannedSuccessfully -> {
                     val transactionId = state.transactionId ?: return@withState
     
    -                val existingTransaction = session.getVerificationService()
    +                val existingTransaction = session.cryptoService().verificationService()
                             .getExistingTransaction(otherUserId, transactionId) as? QrCodeVerificationTransaction
                     existingTransaction
                             ?.otherUserScannedMyQrCode()
    @@ -229,18 +238,18 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
                 is VerificationAction.OtherUserDidNotScanned       -> {
                     val transactionId = state.transactionId ?: return@withState
     
    -                val existingTransaction = session.getVerificationService()
    +                val existingTransaction = session.cryptoService().verificationService()
                             .getExistingTransaction(otherUserId, transactionId) as? QrCodeVerificationTransaction
                     existingTransaction
                             ?.otherUserDidNotScannedMyQrCode()
                 }
                 is VerificationAction.SASMatchAction               -> {
    -                (session.getVerificationService()
    +                (session.cryptoService().verificationService()
                             .getExistingTransaction(action.otherUserId, action.sasTransactionId)
                             as? SasVerificationTransaction)?.userHasVerifiedShortCode()
                 }
                 is VerificationAction.SASDoNotMatchAction          -> {
    -                (session.getVerificationService()
    +                (session.cryptoService().verificationService()
                             .getExistingTransaction(action.otherUserId, action.sasTransactionId)
                             as? SasVerificationTransaction)
                             ?.shortCodeDoesNotMatch()
    @@ -248,6 +257,46 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
                 is VerificationAction.GotItConclusion              -> {
                     _viewEvents.post(VerificationBottomSheetViewEvents.Dismiss)
                 }
    +            is VerificationAction.SkipVerification             -> {
    +                _viewEvents.post(VerificationBottomSheetViewEvents.Dismiss)
    +            }
    +            is VerificationAction.VerifyFromPassphrase         -> {
    +                _viewEvents.post(VerificationBottomSheetViewEvents.AccessSecretStore)
    +            }
    +            is VerificationAction.GotResultFromSsss            -> {
    +                try {
    +                    action.cypherData.fromBase64NoPadding().inputStream().use { ins ->
    +                        val res = session.loadSecureSecret>(ins, action.alias)
    +                        val trustResult = session.cryptoService().crossSigningService().checkTrustFromPrivateKeys(
    +                                res?.get(MASTER_KEY_SSSS_NAME),
    +                                res?.get(USER_SIGNING_KEY_SSSS_NAME),
    +                                res?.get(SELF_SIGNING_KEY_SSSS_NAME)
    +                        )
    +                        if (trustResult.isVerified()) {
    +                            // Sign this device and upload the signature
    +                            session.sessionParams.credentials.deviceId?.let { deviceId ->
    +                                session.cryptoService()
    +                                        .crossSigningService().trustDevice(deviceId, object : MatrixCallback {
    +                                            override fun onFailure(failure: Throwable) {
    +                                                Timber.w(failure, "Failed to sign my device after recovery")
    +                                            }
    +                                        })
    +                            }
    +
    +                            setState {
    +                                copy(verifiedFromPrivateKeys = true)
    +                            }
    +                        } else {
    +                            // POP UP something
    +                            _viewEvents.post(VerificationBottomSheetViewEvents.ModalError("Failed to import keys"))
    +                        }
    +                    }
    +                } catch (failure: Throwable) {
    +                    _viewEvents.post(VerificationBottomSheetViewEvents.ModalError(failure.localizedMessage))
    +                }
    +
    +                Unit
    +            }
             }.exhaustive
         }
     
    @@ -256,7 +305,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
         }
     
         override fun transactionUpdated(tx: VerificationTransaction) = withState { state ->
    -        if (state.waitForOtherUserMode && state.transactionId == null) {
    +        if (state.selfVerificationMode && state.transactionId == null) {
                 // is this an incoming with that user
                 if (tx.isIncoming && tx.otherUserId == state.otherUserMxItem?.id) {
                     // Also auto accept incoming if needed!
    @@ -306,13 +355,13 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
     
         override fun verificationRequestUpdated(pr: PendingVerificationRequest) = withState { state ->
     
    -        if (state.waitForOtherUserMode && state.pendingRequest.invoke() == null && state.transactionId == null) {
    +        if (state.selfVerificationMode && state.pendingRequest.invoke() == null && state.transactionId == null) {
                 // is this an incoming with that user
                 if (pr.isIncoming && pr.otherUserId == state.otherUserMxItem?.id) {
                     if (!pr.isReady) {
                         // auto ready in this case, as we are waiting
                         // TODO, can I be here in DM mode? in this case should test if roomID is null?
    -                    session.getVerificationService()
    +                    session.cryptoService().verificationService()
                                 .readyPendingVerification(supportedVerificationMethods,
                                         pr.otherUserId,
                                         pr.transactionId ?: "")
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt
    index bdb07ed0dc..7845c69b78 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt
    @@ -23,9 +23,9 @@ import com.squareup.inject.assisted.Assisted
     import com.squareup.inject.assisted.AssistedInject
     import im.vector.matrix.android.api.extensions.orFalse
     import im.vector.matrix.android.api.session.Session
    -import im.vector.matrix.android.api.session.crypto.sas.QrCodeVerificationTransaction
    -import im.vector.matrix.android.api.session.crypto.sas.VerificationService
    -import im.vector.matrix.android.api.session.crypto.sas.VerificationTransaction
    +import im.vector.matrix.android.api.session.crypto.verification.QrCodeVerificationTransaction
    +import im.vector.matrix.android.api.session.crypto.verification.VerificationService
    +import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
     import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest
     import im.vector.riotx.core.di.HasScreenInjector
     import im.vector.riotx.core.platform.EmptyAction
    @@ -62,7 +62,7 @@ class VerificationChooseMethodViewModel @AssistedInject constructor(
         }
     
         override fun verificationRequestUpdated(pr: PendingVerificationRequest) = withState { state ->
    -        val pvr = session.getVerificationService().getExistingVerificationRequest(state.otherUserId, state.transactionId)
    +        val pvr = session.cryptoService().verificationService().getExistingVerificationRequest(state.otherUserId, state.transactionId)
     
             setState {
                 copy(
    @@ -79,12 +79,12 @@ class VerificationChooseMethodViewModel @AssistedInject constructor(
         }
     
         init {
    -        session.getVerificationService().addListener(this)
    +        session.cryptoService().verificationService().addListener(this)
         }
     
         override fun onCleared() {
             super.onCleared()
    -        session.getVerificationService().removeListener(this)
    +        session.cryptoService().verificationService().removeListener(this)
         }
     
         companion object : MvRxViewModelFactory {
    @@ -96,10 +96,11 @@ class VerificationChooseMethodViewModel @AssistedInject constructor(
             override fun initialState(viewModelContext: ViewModelContext): VerificationChooseMethodViewState? {
                 val args: VerificationBottomSheet.VerificationArgs = viewModelContext.args()
                 val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
    -            val pvr = session.getVerificationService().getExistingVerificationRequest(args.otherUserId, args.verificationId)
    +            val verificationService = session.cryptoService().verificationService()
    +            val pvr = verificationService.getExistingVerificationRequest(args.otherUserId, args.verificationId)
     
                 // Get the QR code now, because transaction is already created, so transactionCreated() will not be called
    -            val qrCodeVerificationTransaction = session.getVerificationService().getExistingTransaction(args.otherUserId, args.verificationId ?: "")
    +            val qrCodeVerificationTransaction = verificationService.getExistingTransaction(args.otherUserId, args.verificationId ?: "")
     
                 return VerificationChooseMethodViewState(otherUserId = args.otherUserId,
                         transactionId = args.verificationId ?: "",
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/conclusion/VerificationConclusionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/conclusion/VerificationConclusionViewModel.kt
    index bdeb9e01dd..868e355150 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/conclusion/VerificationConclusionViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/conclusion/VerificationConclusionViewModel.kt
    @@ -18,8 +18,8 @@ package im.vector.riotx.features.crypto.verification.conclusion
     import com.airbnb.mvrx.MvRxState
     import com.airbnb.mvrx.MvRxViewModelFactory
     import com.airbnb.mvrx.ViewModelContext
    -import im.vector.matrix.android.api.session.crypto.sas.CancelCode
    -import im.vector.matrix.android.api.session.crypto.sas.safeValueOf
    +import im.vector.matrix.android.api.session.crypto.verification.CancelCode
    +import im.vector.matrix.android.api.session.crypto.verification.safeValueOf
     import im.vector.riotx.core.platform.EmptyAction
     import im.vector.riotx.core.platform.EmptyViewEvents
     import im.vector.riotx.core.platform.VectorViewModel
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/emoji/VerificationEmojiCodeViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/emoji/VerificationEmojiCodeViewModel.kt
    index 637b7d7cc9..be5df38353 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/emoji/VerificationEmojiCodeViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/emoji/VerificationEmojiCodeViewModel.kt
    @@ -27,11 +27,11 @@ import com.airbnb.mvrx.ViewModelContext
     import com.squareup.inject.assisted.Assisted
     import com.squareup.inject.assisted.AssistedInject
     import im.vector.matrix.android.api.session.Session
    -import im.vector.matrix.android.api.session.crypto.sas.EmojiRepresentation
    -import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
    -import im.vector.matrix.android.api.session.crypto.sas.VerificationService
    -import im.vector.matrix.android.api.session.crypto.sas.VerificationTransaction
    -import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
    +import im.vector.matrix.android.api.session.crypto.verification.EmojiRepresentation
    +import im.vector.matrix.android.api.session.crypto.verification.SasVerificationTransaction
    +import im.vector.matrix.android.api.session.crypto.verification.VerificationService
    +import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
    +import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
     import im.vector.matrix.android.api.util.MatrixItem
     import im.vector.matrix.android.api.util.toMatrixItem
     import im.vector.riotx.core.di.HasScreenInjector
    @@ -56,16 +56,16 @@ class VerificationEmojiCodeViewModel @AssistedInject constructor(
     
         init {
             withState { state ->
    -            refreshStateFromTx(session.getVerificationService()
    +            refreshStateFromTx(session.cryptoService().verificationService()
                         .getExistingTransaction(state.otherUser?.id ?: "", state.transactionId
                                 ?: "") as? SasVerificationTransaction)
             }
     
    -        session.getVerificationService().addListener(this)
    +        session.cryptoService().verificationService().addListener(this)
         }
     
         override fun onCleared() {
    -        session.getVerificationService().removeListener(this)
    +        session.cryptoService().verificationService().removeListener(this)
             super.onCleared()
         }
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/epoxy/BottomSheetVerificationEmojisItem.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/epoxy/BottomSheetVerificationEmojisItem.kt
    index 6f75d91d8f..7d72486ccd 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/epoxy/BottomSheetVerificationEmojisItem.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/epoxy/BottomSheetVerificationEmojisItem.kt
    @@ -20,7 +20,7 @@ import android.view.ViewGroup
     import android.widget.TextView
     import com.airbnb.epoxy.EpoxyAttribute
     import com.airbnb.epoxy.EpoxyModelClass
    -import im.vector.matrix.android.api.session.crypto.sas.EmojiRepresentation
    +import im.vector.matrix.android.api.session.crypto.verification.EmojiRepresentation
     import im.vector.riotx.R
     import im.vector.riotx.core.epoxy.VectorEpoxyHolder
     import im.vector.riotx.core.epoxy.VectorEpoxyModel
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt
    index 9c4a5a870f..05ed2f1799 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt
    @@ -50,7 +50,7 @@ class VerificationRequestController @Inject constructor(
             val state = viewState ?: return
             val matrixItem = viewState?.otherUserMxItem ?: return
     
    -        if (state.waitForOtherUserMode) {
    +        if (state.selfVerificationMode) {
                 bottomSheetVerificationNoticeItem {
                     id("notice")
                     notice(stringProvider.getString(R.string.verification_open_other_to_verify))
    @@ -62,7 +62,26 @@ class VerificationRequestController @Inject constructor(
     
                 bottomSheetVerificationWaitingItem {
                     id("waiting")
    -                title(stringProvider.getString(R.string.verification_request_waiting_for, matrixItem.getBestName()))
    +                title(stringProvider.getString(R.string.verification_request_waiting, matrixItem.getBestName()))
    +            }
    +
    +            bottomSheetVerificationActionItem {
    +                id("passphrase")
    +                title(stringProvider.getString(R.string.verification_cannot_access_other_session))
    +                titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
    +                subTitle(stringProvider.getString(R.string.verification_use_passphrase))
    +                iconRes(R.drawable.ic_arrow_right)
    +                iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
    +                listener { listener?.onClickRecoverFromPassphrase() }
    +            }
    +            bottomSheetVerificationActionItem {
    +                id("skip")
    +                title(stringProvider.getString(R.string.skip))
    +                titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
    +//                subTitle(stringProvider.getString(R.string.verification_use_passphrase))
    +                iconRes(R.drawable.ic_arrow_right)
    +                iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
    +                listener { listener?.onClickDismiss() }
                 }
             } else {
                 val styledText = matrixItem.let {
    @@ -112,5 +131,7 @@ class VerificationRequestController @Inject constructor(
     
         interface Listener {
             fun onClickOnVerificationStart()
    +        fun onClickRecoverFromPassphrase()
    +        fun onClickDismiss()
         }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestFragment.kt
    index 8231242d08..64000d07a1 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestFragment.kt
    @@ -61,4 +61,12 @@ class VerificationRequestFragment @Inject constructor(
                 viewModel.handle(VerificationAction.RequestVerificationByDM(otherUserId, state.roomId))
             }
         }
    +
    +    override fun onClickRecoverFromPassphrase() {
    +        viewModel.handle(VerificationAction.VerifyFromPassphrase)
    +    }
    +
    +    override fun onClickDismiss() {
    +        viewModel.handle(VerificationAction.SkipVerification)
    +    }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt
    index 94e14ab214..2ebe1aebe1 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt
    @@ -130,7 +130,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
             if (sharedActionViewModel.hasDisplayedCompleteSecurityPrompt) return
     
             // ensure keys are downloaded
    -        session.downloadKeys(listOf(session.myUserId), true, object : MatrixCallback> {
    +        session.cryptoService().downloadKeys(listOf(session.myUserId), true, object : MatrixCallback> {
                 override fun onSuccess(data: MXUsersDevicesMap) {
                     runOnUiThread {
                         alertCompleteSecurity(session)
    @@ -140,7 +140,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
         }
     
         private fun alertCompleteSecurity(session: Session) {
    -        val myCrossSigningKeys = session.getCrossSigningService()
    +        val myCrossSigningKeys = session.cryptoService().crossSigningService()
                     .getMyCrossSigningKeys()
             val crossSigningEnabledOnAccount = myCrossSigningKeys != null
     
    @@ -150,34 +150,17 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
                 PopupAlertManager.postVectorAlert(
                         PopupAlertManager.VectorAlert(
                                 uid = "completeSecurity",
    -                            title = getString(R.string.crosssigning_verify_this_session),
    -                            description = getString(R.string.crosssigning_other_user_not_trust),
    +                            title = getString(R.string.new_signin),
    +                            description = getString(R.string.complete_security),
                                 iconId = R.drawable.ic_shield_warning
                         ).apply {
    -                        colorInt = ContextCompat.getColor(this@HomeActivity, R.color.riotx_positive_accent)
    +                        colorInt = ContextCompat.getColor(this@HomeActivity, R.color.riotx_destructive_accent)
                             contentAction = Runnable {
    -                            Runnable {
    -                                (weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
    -                                    it.navigator.waitSessionVerification(it)
    -                                }
    +                            (weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
    +                                it.navigator.waitSessionVerification(it)
                                 }
                             }
    -                        dismissedAction = Runnable {
    -                            //                            tx.cancel()
    -                        }
    -                        addButton(
    -                                getString(R.string.later),
    -                                Runnable {
    -                                }
    -                        )
    -                        addButton(
    -                                getString(R.string.verification_profile_verify),
    -                                Runnable {
    -                                    (weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
    -                                        it.navigator.waitSessionVerification(it)
    -                                    }
    -                                }
    -                        )
    +                        dismissedAction = Runnable {}
                         }
                 )
             }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/RoomListDisplayMode.kt b/vector/src/main/java/im/vector/riotx/features/home/RoomListDisplayMode.kt
    index 18b901a967..6d7f49750d 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/RoomListDisplayMode.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/RoomListDisplayMode.kt
    @@ -23,6 +23,5 @@ enum class RoomListDisplayMode(@StringRes val titleRes: Int) {
             HOME(R.string.bottom_action_home),
             PEOPLE(R.string.bottom_action_people_x),
             ROOMS(R.string.bottom_action_rooms),
    -        FILTERED(/* Not used */ 0),
    -        SHARE(/* Not used */ 0)
    +        FILTERED(/* Not used */ 0)
         }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt
    index 3061c5c9e2..2ef7b11b0e 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt
    @@ -27,7 +27,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
         data class UserIsTyping(val isTyping: Boolean) : RoomDetailAction()
         data class SaveDraft(val draft: String) : RoomDetailAction()
         data class SendMessage(val text: CharSequence, val autoMarkdown: Boolean) : RoomDetailAction()
    -    data class SendMedia(val attachments: List) : RoomDetailAction()
    +    data class SendMedia(val attachments: List, val compressBeforeSending: Boolean) : RoomDetailAction()
         data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailAction()
         data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailAction()
         data class LoadMoreTimelineEvents(val direction: Timeline.Direction) : RoomDetailAction()
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    index d744c7f443..c2eb61b3ca 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    @@ -27,12 +27,10 @@ import android.os.Bundle
     import android.os.Parcelable
     import android.text.Spannable
     import android.view.HapticFeedbackConstants
    -import android.view.LayoutInflater
     import android.view.Menu
     import android.view.MenuItem
     import android.view.View
     import android.view.Window
    -import android.widget.TextView
     import android.widget.Toast
     import androidx.annotation.DrawableRes
     import androidx.annotation.StringRes
    @@ -96,6 +94,7 @@ import im.vector.riotx.core.extensions.showKeyboard
     import im.vector.riotx.core.files.addEntryToDownloadManager
     import im.vector.riotx.core.glide.GlideApp
     import im.vector.riotx.core.platform.VectorBaseFragment
    +import im.vector.riotx.core.resources.ColorProvider
     import im.vector.riotx.core.ui.views.JumpToReadMarkerView
     import im.vector.riotx.core.ui.views.NotificationAreaView
     import im.vector.riotx.core.utils.Debouncer
    @@ -110,12 +109,16 @@ import im.vector.riotx.core.utils.checkPermissions
     import im.vector.riotx.core.utils.copyToClipboard
     import im.vector.riotx.core.utils.createUIHandler
     import im.vector.riotx.core.utils.getColorFromUserId
    +import im.vector.riotx.core.utils.jsonViewerStyler
     import im.vector.riotx.core.utils.openUrlInExternalBrowser
     import im.vector.riotx.core.utils.shareMedia
     import im.vector.riotx.core.utils.toast
     import im.vector.riotx.features.attachments.AttachmentTypeSelectorView
     import im.vector.riotx.features.attachments.AttachmentsHelper
     import im.vector.riotx.features.attachments.ContactAttachment
    +import im.vector.riotx.features.attachments.preview.AttachmentsPreviewActivity
    +import im.vector.riotx.features.attachments.preview.AttachmentsPreviewArgs
    +import im.vector.riotx.features.attachments.toGroupedContentAttachmentData
     import im.vector.riotx.features.command.Command
     import im.vector.riotx.features.crypto.util.toImageRes
     import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
    @@ -154,6 +157,7 @@ import kotlinx.android.parcel.Parcelize
     import kotlinx.android.synthetic.main.fragment_room_detail.*
     import kotlinx.android.synthetic.main.merge_composer_layout.view.*
     import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
    +import org.billcarsonfr.jsonviewer.JSonViewerDialog
     import org.commonmark.parser.Parser
     import timber.log.Timber
     import java.io.File
    @@ -178,8 +182,8 @@ class RoomDetailFragment @Inject constructor(
             private val notificationDrawerManager: NotificationDrawerManager,
             val roomDetailViewModelFactory: RoomDetailViewModel.Factory,
             private val eventHtmlRenderer: EventHtmlRenderer,
    -        private val vectorPreferences: VectorPreferences
    -) :
    +        private val vectorPreferences: VectorPreferences,
    +        private val colorProvider: ColorProvider) :
             VectorBaseFragment(),
             TimelineEventController.Callback,
             VectorInviteView.Callback,
    @@ -298,10 +302,16 @@ class RoomDetailFragment @Inject constructor(
             super.onActivityCreated(savedInstanceState)
             if (savedInstanceState == null) {
                 when (val sharedData = roomDetailArgs.sharedData) {
    -                is SharedData.Text        -> roomDetailViewModel.handle(RoomDetailAction.SendMessage(sharedData.text, false))
    -                is SharedData.Attachments -> roomDetailViewModel.handle(RoomDetailAction.SendMedia(sharedData.attachmentData))
    +                is SharedData.Text        -> {
    +                    // Save a draft to set the shared text to the composer
    +                    roomDetailViewModel.handle(RoomDetailAction.SaveDraft(sharedData.text))
    +                }
    +                is SharedData.Attachments -> {
    +                    // open share edition
    +                    onContentAttachmentsReady(sharedData.attachmentData)
    +                }
                     null                      -> Timber.v("No share data to process")
    -            }
    +            }.exhaustive
             }
         }
     
    @@ -426,7 +436,7 @@ class RoomDetailFragment @Inject constructor(
             composerLayout.collapse()
     
             updateComposerText(text)
    -        composerLayout.sendButton.setContentDescription(getString(R.string.send))
    +        composerLayout.sendButton.contentDescription = getString(R.string.send)
         }
     
         private fun renderSpecialMode(event: TimelineEvent,
    @@ -497,7 +507,12 @@ class RoomDetailFragment @Inject constructor(
             val hasBeenHandled = attachmentsHelper.onActivityResult(requestCode, resultCode, data)
             if (!hasBeenHandled && resultCode == RESULT_OK && data != null) {
                 when (requestCode) {
    -                REACTION_SELECT_REQUEST_CODE -> {
    +                AttachmentsPreviewActivity.REQUEST_CODE -> {
    +                    val sendData = AttachmentsPreviewActivity.getOutput(data)
    +                    val keepOriginalSize = AttachmentsPreviewActivity.getKeepOriginalSize(data)
    +                    roomDetailViewModel.handle(RoomDetailAction.SendMedia(sendData, !keepOriginalSize))
    +                }
    +                REACTION_SELECT_REQUEST_CODE            -> {
                         val (eventId, reaction) = EmojiReactionPickerActivity.getOutput(data) ?: return
                         roomDetailViewModel.handle(RoomDetailAction.SendReaction(eventId, reaction))
                     }
    @@ -636,9 +651,11 @@ class RoomDetailFragment @Inject constructor(
         }
     
         private fun sendUri(uri: Uri): Boolean {
    +        roomDetailViewModel.preventAttachmentPreview = true
             val shareIntent = Intent(Intent.ACTION_SEND, uri)
             val isHandled = attachmentsHelper.handleShareIntent(shareIntent)
             if (!isHandled) {
    +            roomDetailViewModel.preventAttachmentPreview = false
                 Toast.makeText(requireContext(), R.string.error_handling_incoming_share, Toast.LENGTH_SHORT).show()
             }
             return isHandled
    @@ -1159,26 +1176,18 @@ class RoomDetailFragment @Inject constructor(
                     onEditedDecorationClicked(action.messageInformationData)
                 }
                 is EventSharedAction.ViewSource                 -> {
    -                val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null)
    -                view.findViewById(R.id.event_content_text_view)?.let {
    -                    it.text = action.content
    -                }
    -
    -                AlertDialog.Builder(requireActivity())
    -                        .setView(view)
    -                        .setPositiveButton(R.string.ok, null)
    -                        .show()
    +                JSonViewerDialog.newInstance(
    +                        action.content,
    +                        -1,
    +                        jsonViewerStyler(colorProvider)
    +                ).show(childFragmentManager, "JSON_VIEWER")
                 }
                 is EventSharedAction.ViewDecryptedSource        -> {
    -                val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null)
    -                view.findViewById(R.id.event_content_text_view)?.let {
    -                    it.text = action.content
    -                }
    -
    -                AlertDialog.Builder(requireActivity())
    -                        .setView(view)
    -                        .setPositiveButton(R.string.ok, null)
    -                        .show()
    +                JSonViewerDialog.newInstance(
    +                        action.content,
    +                        -1,
    +                        jsonViewerStyler(colorProvider)
    +                ).show(childFragmentManager, "JSON_VIEWER")
                 }
                 is EventSharedAction.QuickReact                 -> {
                     // eventId,ClickedOn,Add
    @@ -1341,10 +1350,24 @@ class RoomDetailFragment @Inject constructor(
         // AttachmentsHelper.Callback
     
         override fun onContentAttachmentsReady(attachments: List) {
    -        roomDetailViewModel.handle(RoomDetailAction.SendMedia(attachments))
    +        if (roomDetailViewModel.preventAttachmentPreview) {
    +            roomDetailViewModel.preventAttachmentPreview = false
    +            roomDetailViewModel.handle(RoomDetailAction.SendMedia(attachments, false))
    +        } else {
    +            val grouped = attachments.toGroupedContentAttachmentData()
    +            if (grouped.notPreviewables.isNotEmpty()) {
    +                // Send the not previewable attachments right now (?)
    +                roomDetailViewModel.handle(RoomDetailAction.SendMedia(grouped.notPreviewables, false))
    +            }
    +            if (grouped.previewables.isNotEmpty()) {
    +                val intent = AttachmentsPreviewActivity.newIntent(requireContext(), AttachmentsPreviewArgs(grouped.previewables))
    +                startActivityForResult(intent, AttachmentsPreviewActivity.REQUEST_CODE)
    +            }
    +        }
         }
     
         override fun onAttachmentsProcessFailed() {
    +        roomDetailViewModel.preventAttachmentPreview = false
             Toast.makeText(requireContext(), R.string.error_attachment, Toast.LENGTH_SHORT).show()
         }
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    index 2ca372707f..8a231fb25d 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    @@ -115,6 +115,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
         var pendingAction: RoomDetailAction? = null
         // Slot to keep a pending uri during permission request
         var pendingUri: Uri? = null
    +    // Slot to store if we want to prevent preview of attachment
    +    var preventAttachmentPreview = false
     
         private var trackUnreadMessages = AtomicBoolean(false)
         private var mostRecentDisplayedEvent: TimelineEvent? = null
    @@ -241,7 +243,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
                     is SendMode.REPLY   -> room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback())
                     is SendMode.QUOTE   -> room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback())
                     is SendMode.EDIT    -> room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback())
    -            }
    +            }.exhaustive
             }
         }
     
    @@ -290,20 +292,16 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
         private fun handleTombstoneEvent(action: RoomDetailAction.HandleTombstoneEvent) {
             val tombstoneContent = action.event.getClearContent().toModel() ?: return
     
    -        val roomId = tombstoneContent.replacementRoom ?: ""
    +        val roomId = tombstoneContent.replacementRoomId ?: ""
             val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN
             if (isRoomJoined) {
                 setState { copy(tombstoneEventHandling = Success(roomId)) }
             } else {
    -            val viaServer = MatrixPatterns.extractServerNameFromId(action.event.senderId).let {
    -                if (it.isNullOrBlank()) {
    -                    emptyList()
    -                } else {
    -                    listOf(it)
    -                }
    -            }
    +            val viaServers = MatrixPatterns.extractServerNameFromId(action.event.senderId)
    +                    ?.let { listOf(it) }
    +                    .orEmpty()
                 session.rx()
    -                    .joinRoom(roomId, viaServers = viaServer)
    +                    .joinRoom(roomId, viaServers = viaServers)
                         .map { roomId }
                         .execute {
                             copy(tombstoneEventHandling = it)
    @@ -420,7 +418,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
                                 popDraft()
                             }
                             is ParsedCommand.VerifyUser               -> {
    -                            session.getVerificationService().requestKeyVerificationInDMs(supportedVerificationMethods, slashCommandResult.userId, room.roomId)
    +                            session
    +                                    .cryptoService()
    +                                    .verificationService()
    +                                    .requestKeyVerificationInDMs(supportedVerificationMethods, slashCommandResult.userId, room.roomId)
                                 _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
                                 popDraft()
                             }
    @@ -579,10 +580,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
     
             if (maxUploadFileSize == HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN) {
                 // Unknown limitation
    -            room.sendMedias(attachments)
    +            room.sendMedias(attachments, action.compressBeforeSending, emptySet())
             } else {
                 when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) {
    -                null -> room.sendMedias(attachments)
    +                null -> room.sendMedias(attachments, action.compressBeforeSending, emptySet())
                     else -> _viewEvents.post(RoomDetailViewEvents.FileTooBigError(
                             tooBigFile.name ?: tooBigFile.path,
                             tooBigFile.size,
    @@ -826,7 +827,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
     
         private fun handleAcceptVerification(action: RoomDetailAction.AcceptVerificationRequest) {
             Timber.v("## SAS handleAcceptVerification ${action.otherUserId},  roomId:${room.roomId}, txId:${action.transactionId}")
    -        if (session.getVerificationService().readyPendingVerificationInDMs(
    +        if (session.cryptoService().verificationService().readyPendingVerificationInDMs(
                             supportedVerificationMethods,
                             action.otherUserId,
                             room.roomId,
    @@ -838,7 +839,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
         }
     
         private fun handleDeclineVerification(action: RoomDetailAction.DeclineVerificationRequest) {
    -        session.getVerificationService().declineVerificationRequestInDMs(
    +        session.cryptoService().verificationService().declineVerificationRequestInDMs(
                     action.otherUserId,
                     action.transactionId,
                     room.roomId)
    @@ -851,7 +852,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
     
         private fun handleResumeRequestVerification(action: RoomDetailAction.ResumeVerification) {
             // Check if this request is still active and handled by me
    -        session.getVerificationService().getExistingVerificationRequestInRoom(room.roomId, action.transactionId)?.let {
    +        session.cryptoService().verificationService().getExistingVerificationRequestInRoom(room.roomId, action.transactionId)?.let {
                 if (it.handledByOtherSession) return
                 if (!it.isFinished) {
                     _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action.copy(
    @@ -949,8 +950,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
         private fun observeSummaryState() {
             asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary ->
                 if (summary.membership == Membership.INVITE) {
    -                summary.latestPreviewableEvent?.root?.senderId?.let { senderId ->
    -                    session.getUser(senderId)
    +                summary.inviterId?.let { inviterId ->
    +                    session.getUser(inviterId)
                     }?.also {
                         setState { copy(asyncInviter = Success(it)) }
                     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt
    index 8a67d7900d..f741023630 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt
    @@ -21,7 +21,6 @@ import androidx.recyclerview.widget.RecyclerView
     import im.vector.matrix.android.api.session.room.timeline.Timeline
     import im.vector.riotx.core.platform.DefaultListUpdateCallback
     import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
    -import kotlinx.android.synthetic.main.fragment_room_detail.*
     import timber.log.Timber
     import java.util.concurrent.atomic.AtomicReference
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt
    index c76948a557..a8c9cf679b 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt
    @@ -25,7 +25,11 @@ import androidx.recyclerview.widget.RecyclerView
     import com.airbnb.epoxy.EpoxyController
     import com.airbnb.epoxy.EpoxyModel
     import com.airbnb.epoxy.VisibilityState
    -import im.vector.matrix.android.api.session.room.model.message.*
    +import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
    +import im.vector.matrix.android.api.session.room.model.message.MessageContent
    +import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
    +import im.vector.matrix.android.api.session.room.model.message.MessageImageInfoContent
    +import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
     import im.vector.matrix.android.api.session.room.timeline.Timeline
     import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
     import im.vector.riotx.core.date.VectorDateFormatter
    @@ -36,8 +40,19 @@ import im.vector.riotx.features.home.room.detail.RoomDetailViewState
     import im.vector.riotx.features.home.room.detail.UnreadState
     import im.vector.riotx.features.home.room.detail.timeline.factory.MergedHeaderItemFactory
     import im.vector.riotx.features.home.room.detail.timeline.factory.TimelineItemFactory
    -import im.vector.riotx.features.home.room.detail.timeline.helper.*
    -import im.vector.riotx.features.home.room.detail.timeline.item.*
    +import im.vector.riotx.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
    +import im.vector.riotx.features.home.room.detail.timeline.helper.ReadMarkerVisibilityStateChangedListener
    +import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback
    +import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener
    +import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
    +import im.vector.riotx.features.home.room.detail.timeline.helper.nextOrNull
    +import im.vector.riotx.features.home.room.detail.timeline.item.BaseEventItem
    +import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem
    +import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem_
    +import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem
    +import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
    +import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
    +import im.vector.riotx.features.home.room.detail.timeline.item.TimelineReadMarkerItem_
     import im.vector.riotx.features.media.ImageContentRenderer
     import im.vector.riotx.features.media.VideoContentRenderer
     import org.threeten.bp.LocalDateTime
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
    index a36215007d..fa9bdbc29c 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
    @@ -28,7 +28,12 @@ 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.events.model.isTextMessage
     import im.vector.matrix.android.api.session.events.model.toModel
    -import im.vector.matrix.android.api.session.room.model.message.*
    +import im.vector.matrix.android.api.session.room.model.message.MessageContent
    +import im.vector.matrix.android.api.session.room.model.message.MessageFormat
    +import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
    +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.api.session.room.model.message.MessageVerificationRequestContent
     import im.vector.matrix.android.api.session.room.send.SendState
     import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
     import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
    @@ -169,7 +174,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
     
         private fun computeMessageBody(timelineEvent: TimelineEvent): CharSequence {
             if (timelineEvent.root.isRedacted()) {
    -            return getRedactionReason(timelineEvent)
    +            return noticeEventFormatter.formatRedactedEvent(timelineEvent.root)
             }
     
             return when (timelineEvent.root.getClearType()) {
    @@ -204,31 +209,6 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
             } ?: ""
         }
     
    -    private fun getRedactionReason(timelineEvent: TimelineEvent): String {
    -            return (timelineEvent
    -                    .root
    -                    .unsignedData
    -                    ?.redactedEvent
    -                    ?.content
    -                    ?.get("reason") as? String)
    -                    ?.takeIf { it.isNotBlank() }
    -                    .let { reason ->
    -                        if (reason == null) {
    -                            if (timelineEvent.root.isRedactedBySameUser()) {
    -                                stringProvider.getString(R.string.event_redacted_by_user_reason)
    -                            } else {
    -                                stringProvider.getString(R.string.event_redacted_by_admin_reason)
    -                            }
    -                        } else {
    -                            if (timelineEvent.root.isRedactedBySameUser()) {
    -                                stringProvider.getString(R.string.event_redacted_by_user_reason_with_reason, reason)
    -                            } else {
    -                                stringProvider.getString(R.string.event_redacted_by_admin_reason_with_reason, reason)
    -                            }
    -                        }
    -                    }
    -    }
    -
         private fun actionsForEvent(timelineEvent: TimelineEvent): List {
             val messageContent: MessageContent? = timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
                     ?: timelineEvent.root.getClearContent().toModel()
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt
    index 1a5c6db270..7ac0c8b1e8 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt
    @@ -37,7 +37,7 @@ import im.vector.riotx.core.ui.list.genericLoaderItem
     import im.vector.riotx.features.html.EventHtmlRenderer
     import me.gujun.android.span.span
     import name.fraser.neil.plaintext.diff_match_patch
    -import java.util.*
    +import java.util.Calendar
     
     /**
      * Epoxy controller for edit history list
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt
    index 3c98d24ccf..f03a1c8704 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt
    @@ -100,7 +100,7 @@ class ViewEditHistoryViewModel @AssistedInject constructor(@Assisted
                             if (it.isEncrypted() && it.mxDecryptionResult == null) {
                                 // for now decrypt sync
                                 try {
    -                                val result = session.decryptEvent(it, timelineID)
    +                                val result = session.cryptoService().decryptEvent(it, timelineID)
                                     it.mxDecryptionResult = OlmDecryptionResult(
                                             payload = result.clearEvent,
                                             senderKey = result.senderCurve25519Key,
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt
    index a2e979a08d..42dc4e07eb 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt
    @@ -20,7 +20,10 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
     import im.vector.riotx.core.di.ActiveSessionHolder
     import im.vector.riotx.features.home.AvatarRenderer
     import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
    -import im.vector.riotx.features.home.room.detail.timeline.helper.*
    +import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
    +import im.vector.riotx.features.home.room.detail.timeline.helper.MergedTimelineEventVisibilityStateChangedListener
    +import im.vector.riotx.features.home.room.detail.timeline.helper.canBeMerged
    +import im.vector.riotx.features.home.room.detail.timeline.helper.prevSameTypeEvents
     import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem
     import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem_
     import javax.inject.Inject
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt
    index dc5bd740dd..87a0d2af75 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt
    @@ -16,8 +16,8 @@
     package im.vector.riotx.features.home.room.detail.timeline.factory
     
     import im.vector.matrix.android.api.session.Session
    -import im.vector.matrix.android.api.session.crypto.sas.CancelCode
    -import im.vector.matrix.android.api.session.crypto.sas.safeValueOf
    +import im.vector.matrix.android.api.session.crypto.verification.CancelCode
    +import im.vector.matrix.android.api.session.crypto.verification.safeValueOf
     import im.vector.matrix.android.api.session.events.model.EventType
     import im.vector.matrix.android.api.session.events.model.RelationType
     import im.vector.matrix.android.api.session.events.model.toModel
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
    index 294429368a..2f7b52de62 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
    @@ -36,6 +36,10 @@ class DisplayableEventFormatter @Inject constructor(
     ) {
     
         fun format(timelineEvent: TimelineEvent, appendAuthor: Boolean): CharSequence {
    +        if (timelineEvent.root.isRedacted()) {
    +            return noticeEventFormatter.formatRedactedEvent(timelineEvent.root)
    +        }
    +
             if (timelineEvent.root.isEncrypted()
                     && timelineEvent.root.mxDecryptionResult == null) {
                 return stringProvider.getString(R.string.encrypted_message)
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
    index 8d70279fce..39e17b7c35 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
    @@ -317,4 +317,28 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
                 else                 -> null
             }
         }
    +
    +    fun formatRedactedEvent(event: Event): String {
    +        return (event
    +                .unsignedData
    +                ?.redactedEvent
    +                ?.content
    +                ?.get("reason") as? String)
    +                ?.takeIf { it.isNotBlank() }
    +                .let { reason ->
    +                    if (reason == null) {
    +                        if (event.isRedactedBySameUser()) {
    +                            sp.getString(R.string.event_redacted_by_user_reason)
    +                        } else {
    +                            sp.getString(R.string.event_redacted_by_admin_reason)
    +                        }
    +                    } else {
    +                        if (event.isRedactedBySameUser()) {
    +                            sp.getString(R.string.event_redacted_by_user_reason_with_reason, reason)
    +                        } else {
    +                            sp.getString(R.string.event_redacted_by_admin_reason_with_reason, reason)
    +                        }
    +                    }
    +                }
    +    }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
    index c300b8e1c3..2a7261665a 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
    @@ -104,11 +104,9 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
                             }
                             .toList(),
                     referencesInfoData = event.annotations?.referencesAggregatedSummary?.let { referencesAggregatedSummary ->
    -                    val stateStr = referencesAggregatedSummary.content.toModel()?.verificationSummary
    -                    ReferencesInfoData(
    -                            VerificationState.values().firstOrNull { stateStr == it.name }
    -                                    ?: VerificationState.REQUEST
    -                    )
    +                    val verificationState = referencesAggregatedSummary.content.toModel()?.verificationState
    +                            ?: VerificationState.REQUEST
    +                    ReferencesInfoData(verificationState)
                     },
                     sentByMe = event.root.senderId == session.myUserId
             )
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestItem.kt
    index 853e40c516..309a4a436f 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestItem.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestItem.kt
    @@ -28,9 +28,10 @@ import androidx.core.view.isVisible
     import androidx.core.view.updateLayoutParams
     import com.airbnb.epoxy.EpoxyAttribute
     import com.airbnb.epoxy.EpoxyModelClass
    -import im.vector.matrix.android.api.session.crypto.sas.VerificationService
    +import im.vector.matrix.android.api.session.crypto.verification.VerificationService
     import im.vector.matrix.android.internal.session.room.VerificationState
     import im.vector.riotx.R
    +import im.vector.riotx.core.extensions.exhaustive
     import im.vector.riotx.core.resources.ColorProvider
     import im.vector.riotx.core.utils.DebouncedClickListener
     import im.vector.riotx.features.home.AvatarRenderer
    @@ -102,12 +103,7 @@ abstract class VerificationRequestItem : AbsBaseMessageItem {
    -                holder.buttonBar.isVisible = false
    -                holder.statusTextView.text = null
    -                holder.statusTextView.isVisible = false
    -            }
    -        }
    +        }.exhaustive
     
             // Always hide buttons if request is too old
             if (!VerificationService.isValidRequest(attributes.informationData.ageLocalTS)) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/tools/EventRenderingTools.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/tools/EventRenderingTools.kt
    index 043763fd8e..4e9959eda6 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/tools/EventRenderingTools.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/tools/EventRenderingTools.kt
    @@ -25,7 +25,10 @@ import im.vector.riotx.core.linkify.VectorLinkify
     import im.vector.riotx.core.utils.isValidUrl
     import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
     import im.vector.riotx.features.html.PillImageSpan
    -import kotlinx.coroutines.*
    +import kotlinx.coroutines.CoroutineScope
    +import kotlinx.coroutines.Dispatchers
    +import kotlinx.coroutines.launch
    +import kotlinx.coroutines.withContext
     import me.saket.bettermovementmethod.BetterLinkMovementMethod
     
     fun CharSequence.findPillsAndProcess(scope: CoroutineScope, processBlock: (PillImageSpan) -> Unit) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListDisplayModeFilter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListDisplayModeFilter.kt
    index 8cae1fd4e8..9b5f74c9e6 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListDisplayModeFilter.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListDisplayModeFilter.kt
    @@ -33,7 +33,6 @@ class RoomListDisplayModeFilter(private val displayMode: RoomListDisplayMode) :
                 RoomListDisplayMode.PEOPLE   -> roomSummary.isDirect && roomSummary.membership == Membership.JOIN
                 RoomListDisplayMode.ROOMS    -> !roomSummary.isDirect && roomSummary.membership == Membership.JOIN
                 RoomListDisplayMode.FILTERED -> roomSummary.membership == Membership.JOIN
    -            RoomListDisplayMode.SHARE    -> roomSummary.membership == Membership.JOIN
             }
         }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt
    index 5a32f4b8b3..9c606de3b4 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt
    @@ -50,15 +50,13 @@ import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsShare
     import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
     import im.vector.riotx.features.home.room.list.widget.FabMenuView
     import im.vector.riotx.features.notifications.NotificationDrawerManager
    -import im.vector.riotx.features.share.SharedData
     import kotlinx.android.parcel.Parcelize
     import kotlinx.android.synthetic.main.fragment_room_list.*
     import javax.inject.Inject
     
     @Parcelize
     data class RoomListParams(
    -        val displayMode: RoomListDisplayMode,
    -        val sharedData: SharedData? = null
    +        val displayMode: RoomListDisplayMode
     ) : Parcelable
     
     class RoomListFragment @Inject constructor(
    @@ -106,7 +104,8 @@ class RoomListFragment @Inject constructor(
                 when (it) {
                     is RoomListViewEvents.Loading    -> showLoading(it.message)
                     is RoomListViewEvents.Failure    -> showFailure(it.throwable)
    -                is RoomListViewEvents.SelectRoom -> openSelectedRoom(it)
    +                is RoomListViewEvents.SelectRoom -> handleSelectRoom(it)
    +                is RoomListViewEvents.Done       -> Unit
                 }.exhaustive
             }
     
    @@ -131,13 +130,8 @@ class RoomListFragment @Inject constructor(
             super.onDestroyView()
         }
     
    -    private fun openSelectedRoom(event: RoomListViewEvents.SelectRoom) {
    -        if (roomListParams.displayMode == RoomListDisplayMode.SHARE) {
    -            val sharedData = roomListParams.sharedData ?: return
    -            navigator.openRoomForSharing(requireActivity(), event.roomId, sharedData)
    -        } else {
    -            navigator.openRoom(requireActivity(), event.roomId)
    -        }
    +    private fun handleSelectRoom(event: RoomListViewEvents.SelectRoom) {
    +        navigator.openRoom(requireActivity(), event.roomSummary.roomId)
         }
     
         private fun setupCreateRoomButton() {
    @@ -256,7 +250,6 @@ class RoomListFragment @Inject constructor(
                 is Fail       -> renderFailure(state.asyncFilteredRooms.error)
             }
             roomController.update(state)
    -
             // Mark all as read menu
             when (roomListParams.displayMode) {
                 RoomListDisplayMode.HOME,
    @@ -265,7 +258,7 @@ class RoomListFragment @Inject constructor(
                     val newValue = state.hasUnread
                     if (hasUnreadRooms != newValue) {
                         hasUnreadRooms = newValue
    -                    requireActivity().invalidateOptionsMenu()
    +                    invalidateOptionsMenu()
                     }
                 }
                 else                      -> Unit
    @@ -338,7 +331,6 @@ class RoomListFragment @Inject constructor(
             if (createChatFabMenu.onBackPressed()) {
                 return true
             }
    -
             return false
         }
     
    @@ -350,7 +342,6 @@ class RoomListFragment @Inject constructor(
     
         override fun onRoomLongClicked(room: RoomSummary): Boolean {
             roomController.onRoomLongClicked()
    -
             RoomListQuickActionsBottomSheet
                     .newInstance(room.roomId, RoomListActionsArgs.Mode.FULL)
                     .show(childFragmentManager, "ROOM_LIST_QUICK_ACTIONS")
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewEvents.kt
    index 2e147293ec..ea7393b2fc 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewEvents.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewEvents.kt
    @@ -17,6 +17,7 @@
     
     package im.vector.riotx.features.home.room.list
     
    +import im.vector.matrix.android.api.session.room.model.RoomSummary
     import im.vector.riotx.core.platform.VectorViewEvents
     
     /**
    @@ -26,5 +27,6 @@ sealed class RoomListViewEvents : VectorViewEvents {
         data class Loading(val message: CharSequence? = null) : RoomListViewEvents()
         data class Failure(val throwable: Throwable) : RoomListViewEvents()
     
    -    data class SelectRoom(val roomId: String) : RoomListViewEvents()
    +    data class SelectRoom(val roomSummary: RoomSummary) : RoomListViewEvents()
    +    object Done : RoomListViewEvents()
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt
    index 22c18e9134..81c75ed1d7 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt
    @@ -25,9 +25,9 @@ import im.vector.matrix.android.api.session.Session
     import im.vector.matrix.android.api.session.room.model.Membership
     import im.vector.matrix.android.api.session.room.model.RoomSummary
     import im.vector.matrix.android.api.session.room.model.tag.RoomTag
    +import im.vector.riotx.core.extensions.exhaustive
     import im.vector.riotx.core.platform.VectorViewModel
     import im.vector.riotx.core.utils.DataSource
    -import im.vector.riotx.features.home.RoomListDisplayMode
     import io.reactivex.schedulers.Schedulers
     import timber.log.Timber
     import javax.inject.Inject
    @@ -67,13 +67,13 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
                 is RoomListAction.MarkAllRoomsRead            -> handleMarkAllRoomsRead()
                 is RoomListAction.LeaveRoom                   -> handleLeaveRoom(action)
                 is RoomListAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action)
    -        }
    +        }.exhaustive
         }
     
         // PRIVATE METHODS *****************************************************************************
     
    -    private fun handleSelectRoom(action: RoomListAction.SelectRoom) {
    -        _viewEvents.post(RoomListViewEvents.SelectRoom(action.roomSummary.roomId))
    +    private fun handleSelectRoom(action: RoomListAction.SelectRoom) = withState {
    +        _viewEvents.post(RoomListViewEvents.SelectRoom(action.roomSummary))
         }
     
         private fun handleToggleCategory(action: RoomListAction.ToggleCategory) = setState {
    @@ -101,7 +101,18 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
                     .observeOn(Schedulers.computation())
                     .map { buildRoomSummaries(it) }
                     .execute { async ->
    -                    copy(asyncFilteredRooms = async)
    +                    val invitedRooms = async()?.get(RoomCategory.INVITE)?.map { it.roomId }.orEmpty()
    +                    val remainingJoining = joiningRoomsIds.intersect(invitedRooms)
    +                    val remainingJoinErrors = joiningErrorRoomsIds.intersect(invitedRooms)
    +                    val remainingRejecting = rejectingRoomsIds.intersect(invitedRooms)
    +                    val remainingRejectErrors = rejectingErrorRoomsIds.intersect(invitedRooms)
    +                    copy(
    +                            asyncFilteredRooms = async,
    +                            joiningRoomsIds = remainingJoining,
    +                            joiningErrorRoomsIds = remainingJoinErrors,
    +                            rejectingRoomsIds = remainingRejecting,
    +                            rejectingErrorRoomsIds = remainingRejectErrors
    +                    )
                     }
         }
     
    @@ -197,6 +208,10 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
         private fun handleLeaveRoom(action: RoomListAction.LeaveRoom) {
             _viewEvents.post(RoomListViewEvents.Loading(null))
             session.getRoom(action.roomId)?.leave(null, object : MatrixCallback {
    +            override fun onSuccess(data: Unit) {
    +                _viewEvents.post(RoomListViewEvents.Done)
    +            }
    +
                 override fun onFailure(failure: Throwable) {
                     _viewEvents.post(RoomListViewEvents.Failure(failure))
                 }
    @@ -204,54 +219,34 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
         }
     
         private fun buildRoomSummaries(rooms: List): RoomSummaries {
    -        if (displayMode == RoomListDisplayMode.SHARE) {
    -            val recentRooms = ArrayList(20)
    -            val otherRooms = ArrayList(rooms.size)
    +        // Set up init size on directChats and groupRooms as they are the biggest ones
    +        val invites = ArrayList()
    +        val favourites = ArrayList()
    +        val directChats = ArrayList(rooms.size)
    +        val groupRooms = ArrayList(rooms.size)
    +        val lowPriorities = ArrayList()
    +        val serverNotices = ArrayList()
     
    -            rooms
    -                    .filter { roomListDisplayModeFilter.test(it) }
    -                    .forEach { room ->
    -                        when (room.breadcrumbsIndex) {
    -                            RoomSummary.NOT_IN_BREADCRUMBS -> otherRooms.add(room)
    -                            else                           -> recentRooms.add(room)
    -                        }
    +        rooms
    +                .filter { roomListDisplayModeFilter.test(it) }
    +                .forEach { room ->
    +                    val tags = room.tags.map { it.name }
    +                    when {
    +                        room.membership == Membership.INVITE          -> invites.add(room)
    +                        tags.contains(RoomTag.ROOM_TAG_SERVER_NOTICE) -> serverNotices.add(room)
    +                        tags.contains(RoomTag.ROOM_TAG_FAVOURITE)     -> favourites.add(room)
    +                        tags.contains(RoomTag.ROOM_TAG_LOW_PRIORITY)  -> lowPriorities.add(room)
    +                        room.isDirect                                 -> directChats.add(room)
    +                        else                                          -> groupRooms.add(room)
                         }
    -
    -            return RoomSummaries().apply {
    -                put(RoomCategory.RECENT_ROOMS, recentRooms)
    -                put(RoomCategory.OTHER_ROOMS, otherRooms)
    -            }
    -        } else {
    -            // Set up init size on directChats and groupRooms as they are the biggest ones
    -            val invites = ArrayList()
    -            val favourites = ArrayList()
    -            val directChats = ArrayList(rooms.size)
    -            val groupRooms = ArrayList(rooms.size)
    -            val lowPriorities = ArrayList()
    -            val serverNotices = ArrayList()
    -
    -            rooms
    -                    .filter { roomListDisplayModeFilter.test(it) }
    -                    .forEach { room ->
    -                        val tags = room.tags.map { it.name }
    -                        when {
    -                            room.membership == Membership.INVITE          -> invites.add(room)
    -                            tags.contains(RoomTag.ROOM_TAG_SERVER_NOTICE) -> serverNotices.add(room)
    -                            tags.contains(RoomTag.ROOM_TAG_FAVOURITE)     -> favourites.add(room)
    -                            tags.contains(RoomTag.ROOM_TAG_LOW_PRIORITY)  -> lowPriorities.add(room)
    -                            room.isDirect                                 -> directChats.add(room)
    -                            else                                          -> groupRooms.add(room)
    -                        }
    -                    }
    -
    -            return RoomSummaries().apply {
    -                put(RoomCategory.INVITE, invites)
    -                put(RoomCategory.FAVOURITE, favourites)
    -                put(RoomCategory.DIRECT, directChats)
    -                put(RoomCategory.GROUP, groupRooms)
    -                put(RoomCategory.LOW_PRIORITY, lowPriorities)
    -                put(RoomCategory.SERVER_NOTICE, serverNotices)
    -            }
    +                }
    +        return RoomSummaries().apply {
    +            put(RoomCategory.INVITE, invites)
    +            put(RoomCategory.FAVOURITE, favourites)
    +            put(RoomCategory.DIRECT, directChats)
    +            put(RoomCategory.GROUP, groupRooms)
    +            put(RoomCategory.LOW_PRIORITY, lowPriorities)
    +            put(RoomCategory.SERVER_NOTICE, serverNotices)
             }
         }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModelFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModelFactory.kt
    index 60ec92d8cf..5b823d6444 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModelFactory.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModelFactory.kt
    @@ -18,21 +18,18 @@ package im.vector.riotx.features.home.room.list
     
     import im.vector.matrix.android.api.session.Session
     import im.vector.riotx.features.home.HomeRoomListDataSource
    -import im.vector.riotx.features.home.RoomListDisplayMode
    -import im.vector.riotx.features.share.ShareRoomListDataSource
     import javax.inject.Inject
     import javax.inject.Provider
     
     class RoomListViewModelFactory @Inject constructor(private val session: Provider,
    -                                                   private val homeRoomListDataSource: Provider,
    -                                                   private val shareRoomListDataSource: Provider)
    +                                                   private val homeRoomListDataSource: Provider)
         : RoomListViewModel.Factory {
     
         override fun create(initialState: RoomListViewState): RoomListViewModel {
             return RoomListViewModel(
                     initialState,
                     session.get(),
    -                if (initialState.displayMode == RoomListDisplayMode.SHARE) shareRoomListDataSource.get() else homeRoomListDataSource.get()
    +                homeRoomListDataSource.get()
             )
         }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewState.kt
    index c127fa10e2..b41b4b9eeb 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewState.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewState.kt
    @@ -43,10 +43,7 @@ data class RoomListViewState(
             val isDirectRoomsExpanded: Boolean = true,
             val isGroupRoomsExpanded: Boolean = true,
             val isLowPriorityRoomsExpanded: Boolean = true,
    -        val isServerNoticeRoomsExpanded: Boolean = true,
    -        // For sharing
    -        val isRecentExpanded: Boolean = true,
    -        val isOtherExpanded: Boolean = true
    +        val isServerNoticeRoomsExpanded: Boolean = true
     ) : MvRxState {
     
         constructor(args: RoomListParams) : this(displayMode = args.displayMode)
    @@ -59,8 +56,6 @@ data class RoomListViewState(
                 RoomCategory.GROUP         -> isGroupRoomsExpanded
                 RoomCategory.LOW_PRIORITY  -> isLowPriorityRoomsExpanded
                 RoomCategory.SERVER_NOTICE -> isServerNoticeRoomsExpanded
    -            RoomCategory.RECENT_ROOMS  -> isRecentExpanded
    -            RoomCategory.OTHER_ROOMS   -> isOtherExpanded
             }
         }
     
    @@ -72,8 +67,6 @@ data class RoomListViewState(
                 RoomCategory.GROUP         -> copy(isGroupRoomsExpanded = !isGroupRoomsExpanded)
                 RoomCategory.LOW_PRIORITY  -> copy(isLowPriorityRoomsExpanded = !isLowPriorityRoomsExpanded)
                 RoomCategory.SERVER_NOTICE -> copy(isServerNoticeRoomsExpanded = !isServerNoticeRoomsExpanded)
    -            RoomCategory.RECENT_ROOMS  -> copy(isRecentExpanded = !isRecentExpanded)
    -            RoomCategory.OTHER_ROOMS   -> copy(isOtherExpanded = !isOtherExpanded)
             }
         }
     
    @@ -93,11 +86,7 @@ enum class RoomCategory(@StringRes val titleRes: Int) {
         DIRECT(R.string.bottom_action_people_x),
         GROUP(R.string.bottom_action_rooms),
         LOW_PRIORITY(R.string.low_priority_header),
    -    SERVER_NOTICE(R.string.system_alerts_header),
    -
    -    // For Sharing
    -    RECENT_ROOMS(R.string.room_list_sharing_header_recent_rooms),
    -    OTHER_ROOMS(R.string.room_list_sharing_header_other_rooms)
    +    SERVER_NOTICE(R.string.system_alerts_header)
     }
     
     fun RoomSummaries?.isNullOrEmpty(): Boolean {
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt
    index c4afd442ab..c9724bf971 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt
    @@ -22,7 +22,6 @@ import im.vector.matrix.android.api.session.room.model.Membership
     import im.vector.matrix.android.api.session.room.model.RoomSummary
     import im.vector.riotx.R
     import im.vector.riotx.core.epoxy.helpFooterItem
    -import im.vector.riotx.core.epoxy.noResultItem
     import im.vector.riotx.core.resources.StringProvider
     import im.vector.riotx.core.resources.UserPreferencesProvider
     import im.vector.riotx.features.home.RoomListDisplayMode
    @@ -60,7 +59,6 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
             val nonNullViewState = viewState ?: return
             when (nonNullViewState.displayMode) {
                 RoomListDisplayMode.FILTERED -> buildFilteredRooms(nonNullViewState)
    -            RoomListDisplayMode.SHARE    -> buildShareRooms(nonNullViewState)
                 else                         -> buildRooms(nonNullViewState)
             }
         }
    @@ -77,44 +75,12 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
                     viewState.joiningRoomsIds,
                     viewState.joiningErrorRoomsIds,
                     viewState.rejectingRoomsIds,
    -                viewState.rejectingErrorRoomsIds)
    +                viewState.rejectingErrorRoomsIds,
    +                emptySet())
     
             addFilterFooter(viewState)
         }
     
    -    private fun buildShareRooms(viewState: RoomListViewState) {
    -        var hasResult = false
    -        val roomSummaries = viewState.asyncFilteredRooms()
    -
    -        roomListNameFilter.filter = viewState.roomFilter
    -
    -        roomSummaries?.forEach { (category, summaries) ->
    -            val filteredSummaries = summaries
    -                    .filter { it.membership == Membership.JOIN && roomListNameFilter.test(it) }
    -
    -            if (filteredSummaries.isEmpty()) {
    -                return@forEach
    -            } else {
    -                hasResult = true
    -                val isExpanded = viewState.isCategoryExpanded(category)
    -                buildRoomCategory(viewState, emptyList(), category.titleRes, viewState.isCategoryExpanded(category)) {
    -                    listener?.onToggleRoomCategory(category)
    -                }
    -                if (isExpanded) {
    -                    buildRoomModels(filteredSummaries,
    -                            emptySet(),
    -                            emptySet(),
    -                            emptySet(),
    -                            emptySet()
    -                    )
    -                }
    -            }
    -        }
    -        if (!hasResult) {
    -            addNoResultItem()
    -        }
    -    }
    -
         private fun buildRooms(viewState: RoomListViewState) {
             var showHelp = false
             val roomSummaries = viewState.asyncFilteredRooms()
    @@ -131,7 +97,8 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
                                 viewState.joiningRoomsIds,
                                 viewState.joiningErrorRoomsIds,
                                 viewState.rejectingRoomsIds,
    -                            viewState.rejectingErrorRoomsIds)
    +                            viewState.rejectingErrorRoomsIds,
    +                            emptySet())
                         // Never set showHelp to true for invitation
                         if (category != RoomCategory.INVITE) {
                             showHelp = userPreferencesProvider.shouldShowLongClickOnRoomHelp()
    @@ -160,13 +127,6 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
             }
         }
     
    -    private fun addNoResultItem() {
    -        noResultItem {
    -            id("no_result")
    -            text(stringProvider.getString(R.string.no_result_placeholder))
    -        }
    -    }
    -
         private fun buildRoomCategory(viewState: RoomListViewState,
                                       summaries: List,
                                       @StringRes titleRes: Int,
    @@ -196,10 +156,17 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
                                     joiningRoomsIds: Set,
                                     joiningErrorRoomsIds: Set,
                                     rejectingRoomsIds: Set,
    -                                rejectingErrorRoomsIds: Set) {
    +                                rejectingErrorRoomsIds: Set,
    +                                selectedRoomIds: Set) {
             summaries.forEach { roomSummary ->
                 roomSummaryItemFactory
    -                    .create(roomSummary, joiningRoomsIds, joiningErrorRoomsIds, rejectingRoomsIds, rejectingErrorRoomsIds, listener)
    +                    .create(roomSummary,
    +                            joiningRoomsIds,
    +                            joiningErrorRoomsIds,
    +                            rejectingRoomsIds,
    +                            rejectingErrorRoomsIds,
    +                            selectedRoomIds,
    +                            listener)
                         .addTo(this)
             }
         }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt
    index 652740c0b7..f91b3356d0 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt
    @@ -16,14 +16,17 @@
     
     package im.vector.riotx.features.home.room.list
     
    +import android.view.HapticFeedbackConstants
     import android.view.View
     import android.view.ViewGroup
     import android.widget.ImageView
     import android.widget.TextView
    +import androidx.core.content.ContextCompat
     import androidx.core.view.isInvisible
     import androidx.core.view.isVisible
     import com.airbnb.epoxy.EpoxyAttribute
     import com.airbnb.epoxy.EpoxyModelClass
    +import com.amulyakhare.textdrawable.TextDrawable
     import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
     import im.vector.matrix.android.api.util.MatrixItem
     import im.vector.riotx.R
    @@ -48,11 +51,15 @@ abstract class RoomSummaryItem : VectorEpoxyModel() {
         @EpoxyAttribute var showHighlighted: Boolean = false
         @EpoxyAttribute var itemLongClickListener: View.OnLongClickListener? = null
         @EpoxyAttribute var itemClickListener: View.OnClickListener? = null
    +    @EpoxyAttribute var showSelected: Boolean = false
     
         override fun bind(holder: Holder) {
             super.bind(holder)
             holder.rootView.setOnClickListener(itemClickListener)
    -        holder.rootView.setOnLongClickListener(itemLongClickListener)
    +        holder.rootView.setOnLongClickListener {
    +            it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
    +            itemLongClickListener?.onLongClick(it) ?: false
    +        }
             holder.titleView.text = matrixItem.getBestName()
             holder.lastEventTimeView.text = lastEventTime
             holder.lastEventView.text = lastFormattedEvent
    @@ -64,6 +71,19 @@ abstract class RoomSummaryItem : VectorEpoxyModel() {
             avatarRenderer.render(matrixItem, holder.avatarImageView)
             holder.roomAvatarDecorationImageView.isVisible = encryptionTrustLevel != null
             holder.roomAvatarDecorationImageView.setImageResource(encryptionTrustLevel.toImageRes())
    +        renderSelection(holder, showSelected)
    +    }
    +
    +    private fun renderSelection(holder: Holder, isSelected: Boolean) {
    +        if (isSelected) {
    +            holder.avatarCheckedImageView.visibility = View.VISIBLE
    +            val backgroundColor = ContextCompat.getColor(holder.view.context, R.color.riotx_accent)
    +            val backgroundDrawable = TextDrawable.builder().buildRound("", backgroundColor)
    +            holder.avatarImageView.setImageDrawable(backgroundDrawable)
    +        } else {
    +            holder.avatarCheckedImageView.visibility = View.GONE
    +            avatarRenderer.render(matrixItem, holder.avatarImageView)
    +        }
         }
     
         class Holder : VectorEpoxyHolder() {
    @@ -74,6 +94,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel() {
             val typingView by bind(R.id.roomTypingView)
             val draftView by bind(R.id.roomDraftBadge)
             val lastEventTimeView by bind(R.id.roomLastEventTimeView)
    +        val avatarCheckedImageView by bind(R.id.roomAvatarCheckedImageView)
             val avatarImageView by bind(R.id.roomAvatarImageView)
             val roomAvatarDecorationImageView by bind(R.id.roomAvatarDecorationImageView)
             val rootView by bind(R.id.itemRoomLayout)
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt
    index d224ccec47..7d8cf0b6c1 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt
    @@ -25,7 +25,6 @@ import im.vector.riotx.R
     import im.vector.riotx.core.date.VectorDateFormatter
     import im.vector.riotx.core.epoxy.VectorEpoxyModel
     import im.vector.riotx.core.extensions.localDateTime
    -import im.vector.riotx.core.resources.ColorProvider
     import im.vector.riotx.core.resources.DateProvider
     import im.vector.riotx.core.resources.StringProvider
     import im.vector.riotx.core.utils.DebouncedClickListener
    @@ -36,7 +35,6 @@ import javax.inject.Inject
     
     class RoomSummaryItemFactory @Inject constructor(private val displayableEventFormatter: DisplayableEventFormatter,
                                                      private val dateFormatter: VectorDateFormatter,
    -                                                 private val colorProvider: ColorProvider,
                                                      private val stringProvider: StringProvider,
                                                      private val typingHelper: TypingHelper,
                                                      private val session: Session,
    @@ -47,23 +45,24 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
                    joiningErrorRoomsIds: Set,
                    rejectingRoomsIds: Set,
                    rejectingErrorRoomsIds: Set,
    +               selectedRoomIds: Set,
                    listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
             return when (roomSummary.membership) {
                 Membership.INVITE -> createInvitationItem(roomSummary, joiningRoomsIds, joiningErrorRoomsIds, rejectingRoomsIds, rejectingErrorRoomsIds, listener)
    -            else              -> createRoomItem(roomSummary, listener)
    +            else              -> createRoomItem(roomSummary, selectedRoomIds, listener?.let { it::onRoomClicked }, listener?.let { it::onRoomLongClicked })
             }
         }
     
    -    private fun createInvitationItem(roomSummary: RoomSummary,
    -                                     joiningRoomsIds: Set,
    -                                     joiningErrorRoomsIds: Set,
    -                                     rejectingRoomsIds: Set,
    -                                     rejectingErrorRoomsIds: Set,
    -                                     listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
    +    fun createInvitationItem(roomSummary: RoomSummary,
    +                             joiningRoomsIds: Set,
    +                             joiningErrorRoomsIds: Set,
    +                             rejectingRoomsIds: Set,
    +                             rejectingErrorRoomsIds: Set,
    +                             listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
             val secondLine = if (roomSummary.isDirect) {
    -            roomSummary.latestPreviewableEvent?.root?.senderId
    +            roomSummary.inviterId
             } else {
    -            roomSummary.latestPreviewableEvent?.root?.senderId?.let {
    +            roomSummary.inviterId?.let {
                     stringProvider.getString(R.string.invited_by, it)
                 }
             }
    @@ -82,10 +81,15 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
                     .listener { listener?.onRoomClicked(roomSummary) }
         }
     
    -    private fun createRoomItem(roomSummary: RoomSummary, listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
    +    fun createRoomItem(
    +            roomSummary: RoomSummary,
    +            selectedRoomIds: Set,
    +            onClick: ((RoomSummary) -> Unit)?,
    +            onLongClick: ((RoomSummary) -> Boolean)?
    +    ): VectorEpoxyModel<*> {
             val unreadCount = roomSummary.notificationCount
             val showHighlighted = roomSummary.highlightCount > 0
    -
    +        val showSelected = selectedRoomIds.contains(roomSummary.roomId)
             var latestFormattedEvent: CharSequence = ""
             var latestEventTime: CharSequence = ""
             val latestEvent = roomSummary.latestPreviewableEvent
    @@ -119,15 +123,16 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
                     .typingString(typingString)
                     .lastFormattedEvent(latestFormattedEvent)
                     .showHighlighted(showHighlighted)
    +                .showSelected(showSelected)
                     .unreadNotificationCount(unreadCount)
                     .hasUnreadMessage(roomSummary.hasUnreadMessages)
                     .hasDraft(roomSummary.userDrafts.isNotEmpty())
                     .itemLongClickListener { _ ->
    -                    listener?.onRoomLongClicked(roomSummary) ?: false
    +                    onLongClick?.invoke(roomSummary) ?: false
                     }
                     .itemClickListener(
                             DebouncedClickListener(View.OnClickListener { _ ->
    -                            listener?.onRoomClicked(roomSummary)
    +                            onClick?.invoke(roomSummary)
                             })
                     )
         }
    diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt
    index e3bb539172..b11cd57329 100644
    --- a/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt
    @@ -23,7 +23,11 @@ import android.net.http.SslError
     import android.os.Build
     import android.os.Parcelable
     import android.view.KeyEvent
    -import android.webkit.*
    +import android.webkit.SslErrorHandler
    +import android.webkit.WebResourceRequest
    +import android.webkit.WebResourceResponse
    +import android.webkit.WebView
    +import android.webkit.WebViewClient
     import androidx.appcompat.app.AlertDialog
     import androidx.core.view.isVisible
     import com.airbnb.mvrx.args
    @@ -34,7 +38,7 @@ import kotlinx.android.parcel.Parcelize
     import kotlinx.android.synthetic.main.fragment_login_captcha.*
     import timber.log.Timber
     import java.net.URLDecoder
    -import java.util.*
    +import java.util.Formatter
     import javax.inject.Inject
     
     @Parcelize
    diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt
    index 93b1b1b525..3e45eeb406 100644
    --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt
    @@ -40,7 +40,7 @@ import javax.inject.Inject
     
     /**
      * In this screen, in signin mode:
    - * - the user is asked for login and password to sign in to a homeserver.
    + * - the user is asked for login (or email) and password to sign in to a homeserver.
      * - He also can reset his password
      * In signup mode:
      * - the user is asked for login and password
    @@ -97,6 +97,12 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
                 SignMode.SignIn  -> R.string.login_connect_to
             }
     
    +        loginFieldTil.hint = getString(when (state.signMode) {
    +            SignMode.Unknown -> error("developer error")
    +            SignMode.SignUp  -> R.string.login_signup_username_hint
    +            SignMode.SignIn  -> R.string.login_signin_username_hint
    +        })
    +
             when (state.serverType) {
                 ServerType.MatrixOrg -> {
                     loginServerIcon.isVisible = true
    diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt
    index 2887dd04f0..3f81fa8f4b 100644
    --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt
    @@ -16,7 +16,12 @@
     
     package im.vector.riotx.features.login
     
    -import com.airbnb.mvrx.*
    +import com.airbnb.mvrx.Async
    +import com.airbnb.mvrx.Loading
    +import com.airbnb.mvrx.MvRxState
    +import com.airbnb.mvrx.PersistState
    +import com.airbnb.mvrx.Success
    +import com.airbnb.mvrx.Uninitialized
     
     data class LoginViewState(
             val asyncLoginAction: Async = Uninitialized,
    diff --git a/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt
    new file mode 100644
    index 0000000000..24b595d182
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt
    @@ -0,0 +1,71 @@
    +/*
    + * Copyright (c) 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.riotx.features.media
    +
    +import android.content.Context
    +import android.content.Intent
    +import android.os.Bundle
    +import androidx.core.net.toUri
    +import im.vector.riotx.R
    +import im.vector.riotx.core.di.ActiveSessionHolder
    +import im.vector.riotx.core.di.ScreenComponent
    +import im.vector.riotx.core.platform.VectorBaseActivity
    +import kotlinx.android.synthetic.main.activity_big_image_viewer.*
    +import javax.inject.Inject
    +
    +class BigImageViewerActivity : VectorBaseActivity() {
    +    @Inject lateinit var sessionHolder: ActiveSessionHolder
    +
    +    override fun injectWith(injector: ScreenComponent) {
    +        injector.inject(this)
    +    }
    +
    +    override fun onCreate(savedInstanceState: Bundle?) {
    +        super.onCreate(savedInstanceState)
    +        setContentView(R.layout.activity_big_image_viewer)
    +
    +        setSupportActionBar(bigImageViewerToolbar)
    +        supportActionBar?.apply {
    +            title = intent.getStringExtra(EXTRA_TITLE)
    +            setHomeButtonEnabled(true)
    +            setDisplayHomeAsUpEnabled(true)
    +        }
    +
    +        val uri = sessionHolder.getSafeActiveSession()
    +                ?.contentUrlResolver()
    +                ?.resolveFullSize(intent.getStringExtra(EXTRA_IMAGE_URL))
    +                ?.toUri()
    +
    +        if (uri == null) {
    +            finish()
    +        } else {
    +            bigImageViewerImageView.showImage(uri)
    +        }
    +    }
    +
    +    companion object {
    +        private const val EXTRA_TITLE = "EXTRA_TITLE"
    +        private const val EXTRA_IMAGE_URL = "EXTRA_IMAGE_URL"
    +
    +        fun newIntent(context: Context, title: String?, imageUrl: String): Intent {
    +            return Intent(context, BigImageViewerActivity::class.java).apply {
    +                putExtra(EXTRA_TITLE, title)
    +                putExtra(EXTRA_IMAGE_URL, imageUrl)
    +            }
    +        }
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt
    index 909fd5b8eb..e497d9ce04 100644
    --- a/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt
    @@ -25,6 +25,7 @@ import com.bumptech.glide.load.engine.GlideException
     import com.bumptech.glide.load.resource.bitmap.RoundedCorners
     import com.bumptech.glide.request.RequestListener
     import com.bumptech.glide.request.target.Target
    +import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.ORIENTATION_USE_EXIF
     import com.github.piasy.biv.view.BigImageView
     import im.vector.matrix.android.api.session.content.ContentUrlResolver
     import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
    @@ -36,6 +37,7 @@ import im.vector.riotx.core.utils.DimensionConverter
     import im.vector.riotx.core.utils.isLocalFile
     import kotlinx.android.parcel.Parcelize
     import timber.log.Timber
    +import java.io.File
     import javax.inject.Inject
     import kotlin.math.min
     
    @@ -142,6 +144,12 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
                 return
             }
     
    +        imageView.setImageLoaderCallback(object : DefaultImageLoaderCallback {
    +            override fun onSuccess(image: File?) {
    +                imageView.ssiv?.orientation = ORIENTATION_USE_EXIF
    +            }
    +        })
    +
             imageView.showImage(
                     Uri.parse(thumbnail),
                     Uri.parse(fullSize)
    diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt
    index ecc806c798..63fab290be 100644
    --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt
    @@ -19,10 +19,14 @@ package im.vector.riotx.features.navigation
     import android.app.Activity
     import android.content.Context
     import android.content.Intent
    +import android.view.View
    +import androidx.core.app.ActivityOptionsCompat
     import androidx.core.app.TaskStackBuilder
    -import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction
    -import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod
    +import androidx.core.view.ViewCompat
    +import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction
    +import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod
     import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
    +import im.vector.matrix.android.api.util.MatrixItem
     import im.vector.riotx.R
     import im.vector.riotx.core.di.ActiveSessionHolder
     import im.vector.riotx.core.error.fatalError
    @@ -36,6 +40,7 @@ import im.vector.riotx.features.debug.DebugMenuActivity
     import im.vector.riotx.features.home.room.detail.RoomDetailActivity
     import im.vector.riotx.features.home.room.detail.RoomDetailArgs
     import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
    +import im.vector.riotx.features.media.BigImageViewerActivity
     import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
     import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
     import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity
    @@ -64,25 +69,25 @@ class DefaultNavigator @Inject constructor(
             startActivity(context, intent, buildTask)
         }
     
    -    override fun performDeviceVerification(context: Context, otherUserId: String, sasTransationId: String) {
    +    override fun performDeviceVerification(context: Context, otherUserId: String, sasTransactionId: String) {
             val session = sessionHolder.getSafeActiveSession() ?: return
    -        val tx = session.getVerificationService().getExistingTransaction(otherUserId, sasTransationId) ?: return
    +        val tx = session.cryptoService().verificationService().getExistingTransaction(otherUserId, sasTransactionId) ?: return
             (tx as? IncomingSasVerificationTransaction)?.performAccept()
             if (context is VectorBaseActivity) {
                 VerificationBottomSheet.withArgs(
                         roomId = null,
                         otherUserId = otherUserId,
    -                    transactionId = sasTransationId
    +                    transactionId = sasTransactionId
                 ).show(context.supportFragmentManager, "REQPOP")
             }
         }
     
         override fun requestSessionVerification(context: Context) {
             val session = sessionHolder.getSafeActiveSession() ?: return
    -        val pr = session.getVerificationService().requestKeyVerification(
    +        val pr = session.cryptoService().verificationService().requestKeyVerification(
                     listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
                     session.myUserId,
    -                session.getUserDevices(session.myUserId).map { it.deviceId })
    +                session.cryptoService().getUserDevices(session.myUserId).map { it.deviceId })
             if (context is VectorBaseActivity) {
                 VerificationBottomSheet.withArgs(
                         roomId = null,
    @@ -95,12 +100,8 @@ class DefaultNavigator @Inject constructor(
         override fun waitSessionVerification(context: Context) {
             val session = sessionHolder.getSafeActiveSession() ?: return
             if (context is VectorBaseActivity) {
    -            VerificationBottomSheet.withArgs(
    -                    roomId = null,
    -                    otherUserId = session.myUserId,
    -                    waitForIncomingRequest = true
    -
    -            ).show(context.supportFragmentManager, VerificationBottomSheet.WAITING_SELF_VERIF_TAG)
    +            VerificationBottomSheet.forSelfVerification(session)
    +                    .show(context.supportFragmentManager, VerificationBottomSheet.WAITING_SELF_VERIF_TAG)
             }
         }
     
    @@ -126,7 +127,7 @@ class DefaultNavigator @Inject constructor(
             startActivity(context, intent, buildTask)
         }
     
    -    override fun openRoomForSharing(activity: Activity, roomId: String, sharedData: SharedData) {
    +    override fun openRoomForSharingAndFinish(activity: Activity, roomId: String, sharedData: SharedData) {
             val args = RoomDetailArgs(roomId, null, sharedData)
             val intent = RoomDetailActivity.newIntent(activity, args)
             activity.startActivity(intent)
    @@ -179,6 +180,18 @@ class DefaultNavigator @Inject constructor(
             context.startActivity(RoomProfileActivity.newIntent(context, roomId))
         }
     
    +    override fun openBigImageViewer(activity: Activity, sharedElement: View?, matrixItem: MatrixItem) {
    +        matrixItem.avatarUrl
    +                ?.takeIf { it.isNotBlank() }
    +                ?.let { avatarUrl ->
    +                    val intent = BigImageViewerActivity.newIntent(activity, matrixItem.getBestName(), avatarUrl)
    +                    val options = sharedElement?.let {
    +                        ActivityOptionsCompat.makeSceneTransitionAnimation(activity, it, ViewCompat.getTransitionName(it) ?: "")
    +                    }
    +                    activity.startActivity(intent, options?.toBundle())
    +                }
    +    }
    +
         private fun startActivity(context: Context, intent: Intent, buildTask: Boolean) {
             if (buildTask) {
                 val stackBuilder = TaskStackBuilder.create(context)
    diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt
    index 0f368362e8..fcb3d7bb44 100644
    --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt
    @@ -18,7 +18,9 @@ package im.vector.riotx.features.navigation
     
     import android.app.Activity
     import android.content.Context
    +import android.view.View
     import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
    +import im.vector.matrix.android.api.util.MatrixItem
     import im.vector.riotx.features.settings.VectorSettingsActivity
     import im.vector.riotx.features.share.SharedData
     
    @@ -26,11 +28,13 @@ interface Navigator {
     
         fun openRoom(context: Context, roomId: String, eventId: String? = null, buildTask: Boolean = false)
     
    -    fun performDeviceVerification(context: Context, otherUserId: String, sasTransationId: String)
    +    fun performDeviceVerification(context: Context, otherUserId: String, sasTransactionId: String)
    +
         fun requestSessionVerification(context: Context)
    +
         fun waitSessionVerification(context: Context)
     
    -    fun openRoomForSharing(activity: Activity, roomId: String, sharedData: SharedData)
    +    fun openRoomForSharingAndFinish(activity: Activity, roomId: String, sharedData: SharedData)
     
         fun openNotJoinedRoom(context: Context, roomIdOrAlias: String?, eventId: String? = null, buildTask: Boolean = false)
     
    @@ -57,4 +61,6 @@ interface Navigator {
         fun openRoomMemberProfile(userId: String, roomId: String?, context: Context, buildTask: Boolean = false)
     
         fun openRoomProfile(context: Context, roomId: String)
    +
    +    fun openBigImageViewer(activity: Activity, sharedElement: View?, matrixItem: MatrixItem)
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt
    index 11d770adc4..1f9f54127b 100644
    --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt
    @@ -26,11 +26,11 @@ import im.vector.matrix.android.api.session.room.model.Membership
     import im.vector.matrix.android.api.session.room.model.RoomMemberContent
     import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
     import im.vector.matrix.android.api.session.room.timeline.getEditedEventId
    -import im.vector.matrix.android.api.session.room.timeline.getLastMessageBody
     import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
     import im.vector.riotx.BuildConfig
     import im.vector.riotx.R
     import im.vector.riotx.core.resources.StringProvider
    +import im.vector.riotx.features.home.room.detail.timeline.format.DisplayableEventFormatter
     import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter
     import timber.log.Timber
     import java.util.UUID
    @@ -43,27 +43,28 @@ import javax.inject.Inject
      * this pattern allow decoupling between the object responsible of displaying notifications and the matrix sdk.
      */
     class NotifiableEventResolver @Inject constructor(private val stringProvider: StringProvider,
    -                                                  private val noticeEventFormatter: NoticeEventFormatter) {
    +                                                  private val noticeEventFormatter: NoticeEventFormatter,
    +                                                  private val displayableEventFormatter: DisplayableEventFormatter) {
     
         // private val eventDisplay = RiotEventDisplay(context)
     
         fun resolveEvent(event: Event/*, roomState: RoomState?, bingRule: PushRule?*/, session: Session): NotifiableEvent? {
             val roomID = event.roomId ?: return null
             val eventId = event.eventId ?: return null
    +        if (event.getClearType() == EventType.STATE_ROOM_MEMBER) {
    +            return resolveStateRoomEvent(event, session)
    +        }
             val timelineEvent = session.getRoom(roomID)?.getTimeLineEvent(eventId) ?: return null
             when (event.getClearType()) {
    -            EventType.MESSAGE           -> {
    +            EventType.MESSAGE   -> {
                     return resolveMessageEvent(timelineEvent, session)
                 }
    -            EventType.ENCRYPTED         -> {
    +            EventType.ENCRYPTED -> {
                     val messageEvent = resolveMessageEvent(timelineEvent, session)
                     messageEvent?.lockScreenVisibility = NotificationCompat.VISIBILITY_PRIVATE
                     return messageEvent
                 }
    -            EventType.STATE_ROOM_MEMBER -> {
    -                return resolveStateRoomEvent(event, session)
    -            }
    -            else                        -> {
    +            else                -> {
                     // If the event can be displayed, display it as is
                     Timber.w("NotifiableEventResolver Received an unsupported event matching a bing rule")
                     // TODO Better event text display
    @@ -90,9 +91,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
             if (room == null) {
                 Timber.e("## Unable to resolve room for eventId [$event]")
                 // Ok room is not known in store, but we can still display something
    -            val body =
    -                    event.getLastMessageBody()
    -                            ?: stringProvider.getString(R.string.notification_unknown_new_event)
    +            val body = displayableEventFormatter.format(event, false)
                 val roomName = stringProvider.getString(R.string.notification_unknown_room_name)
                 val senderDisplayName = event.getDisambiguatedDisplayName()
     
    @@ -103,7 +102,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
                         noisy = false, // will be updated
                         senderName = senderDisplayName,
                         senderId = event.root.senderId,
    -                    body = body,
    +                    body = body.toString(),
                         roomId = event.root.roomId!!,
                         roomName = roomName)
     
    @@ -114,7 +113,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
                     // TODO use a global event decryptor? attache to session and that listen to new sessionId?
                     // for now decrypt sync
                     try {
    -                    val result = session.decryptEvent(event.root, event.root.roomId + UUID.randomUUID().toString())
    +                    val result = session.cryptoService().decryptEvent(event.root, event.root.roomId + UUID.randomUUID().toString())
                         event.root.mxDecryptionResult = OlmDecryptionResult(
                                 payload = result.clearEvent,
                                 senderKey = result.senderCurve25519Key,
    @@ -125,8 +124,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
                     }
                 }
     
    -            val body = event.getLastMessageBody()
    -                    ?: stringProvider.getString(R.string.notification_unknown_new_event)
    +            val body = displayableEventFormatter.format(event, false).toString()
                 val roomName = room.roomSummary()?.displayName ?: ""
                 val senderDisplayName = event.getDisambiguatedDisplayName()
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt
    index 24ae0e722a..0f1d5f466e 100644
    --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt
    @@ -130,9 +130,9 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
     
         fun onEventRedacted(eventId: String) {
             synchronized(eventList) {
    -            eventList.filter { it.eventId == eventId }.map { notifiableEvent ->
    -                notifiableEvent.isRedacted = true
    -                notifiableEvent.hasBeenDisplayed = false
    +            eventList.find { it.eventId == eventId }?.apply {
    +                isRedacted = true
    +                hasBeenDisplayed = false
                 }
             }
         }
    @@ -182,7 +182,6 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
                     e is InviteNotifiableEvent && e.roomId == roomId
                 }
             }
    -
             notificationUtils.cancelNotificationMessage(roomId, ROOM_INVITATION_NOTIFICATION_ID)
         }
     
    @@ -204,7 +203,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
     
             val user = session.getUser(session.myUserId)
             // myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash
    -        val myUserDisplayName = user?.displayName?.takeIf { it.isNotBlank() } ?: session.myUserId
    +        val myUserDisplayName = user?.getBestName() ?: session.myUserId
             val myUserAvatarUrl = session.contentUrlResolver().resolveThumbnail(user?.avatarUrl, avatarSize, avatarSize, ContentUrlResolver.ThumbnailMethod.SCALE)
             synchronized(eventList) {
                 Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER ")
    diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt
    index 7d8e43d0be..ff0947598f 100755
    --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt
    @@ -515,7 +515,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
                         val joinIntent = Intent(context, NotificationBroadcastReceiver::class.java)
                         joinIntent.action = JOIN_ACTION
                         joinIntent.data = Uri.parse("foobar://$roomId&$matrixId")
    -                    rejectIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId)
    +                    joinIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId)
                         val joinIntentPendingIntent = PendingIntent.getBroadcast(context, System.currentTimeMillis().toInt(), joinIntent,
                                 PendingIntent.FLAG_UPDATE_CURRENT)
                         addAction(
    diff --git a/vector/src/main/java/im/vector/riotx/features/permalink/PermalinkHandler.kt b/vector/src/main/java/im/vector/riotx/features/permalink/PermalinkHandler.kt
    index 84a25060cc..bcf981fc92 100644
    --- a/vector/src/main/java/im/vector/riotx/features/permalink/PermalinkHandler.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/permalink/PermalinkHandler.kt
    @@ -20,16 +20,16 @@ import android.content.Context
     import android.net.Uri
     import im.vector.matrix.android.api.permalinks.PermalinkData
     import im.vector.matrix.android.api.permalinks.PermalinkParser
    -import im.vector.matrix.android.api.session.Session
     import im.vector.matrix.android.api.util.Optional
     import im.vector.matrix.rx.rx
    +import im.vector.riotx.core.di.ActiveSessionHolder
     import im.vector.riotx.features.navigation.Navigator
     import io.reactivex.Single
     import io.reactivex.android.schedulers.AndroidSchedulers
     import io.reactivex.schedulers.Schedulers
     import javax.inject.Inject
     
    -class PermalinkHandler @Inject constructor(private val session: Session,
    +class PermalinkHandler @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
                                                private val navigator: Navigator) {
     
         fun launch(
    @@ -80,7 +80,8 @@ class PermalinkHandler @Inject constructor(private val session: Session,
         }
     
         private fun PermalinkData.RoomLink.getRoomId(): Single> {
    -        return if (isRoomAlias) {
    +        val session = activeSessionHolder.getSafeActiveSession()
    +        return if (isRoomAlias && session != null) {
                 // At the moment we are not fetching on the server as we don't handle not join room
                 session.rx().getRoomIdByAlias(roomIdOrAlias, false).subscribeOn(Schedulers.io())
             } else {
    @@ -92,6 +93,7 @@ class PermalinkHandler @Inject constructor(private val session: Session,
          * Open room either joined, or not
          */
         private fun openRoom(context: Context, roomId: String?, eventId: String?, buildTask: Boolean) {
    +        val session = activeSessionHolder.getSafeActiveSession() ?: return
             return if (roomId != null && session.getRoom(roomId) != null) {
                 navigator.openRoom(context, roomId, eventId, buildTask)
             } else {
    diff --git a/vector/src/main/java/im/vector/riotx/features/qrcode/QrCodeScannerActivity.kt b/vector/src/main/java/im/vector/riotx/features/qrcode/QrCodeScannerActivity.kt
    index bb6f1be03b..f9c92ba3ad 100644
    --- a/vector/src/main/java/im/vector/riotx/features/qrcode/QrCodeScannerActivity.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/qrcode/QrCodeScannerActivity.kt
    @@ -22,6 +22,7 @@ import android.os.Bundle
     import androidx.fragment.app.Fragment
     import com.google.zxing.BarcodeFormat
     import com.google.zxing.Result
    +import com.google.zxing.ResultMetadataType
     import im.vector.riotx.R
     import im.vector.riotx.core.di.ScreenComponent
     import im.vector.riotx.core.extensions.replaceFragment
    @@ -43,15 +44,33 @@ class QrCodeScannerActivity : VectorBaseActivity() {
         }
     
         fun setResultAndFinish(result: Result?) {
    -        result?.let {
    +        if (result != null) {
    +            val rawBytes = getRawBytes(result)
    +            val rawBytesStr = rawBytes?.toString(Charsets.ISO_8859_1)
    +
                 setResult(RESULT_OK, Intent().apply {
    -                putExtra(EXTRA_OUT_TEXT, it.text)
    -                putExtra(EXTRA_OUT_IS_QR_CODE, it.barcodeFormat == BarcodeFormat.QR_CODE)
    +                putExtra(EXTRA_OUT_TEXT, rawBytesStr ?: result.text)
    +                putExtra(EXTRA_OUT_IS_QR_CODE, result.barcodeFormat == BarcodeFormat.QR_CODE)
                 })
             }
             finish()
         }
     
    +    // Copied from https://github.com/markusfisch/BinaryEye/blob/
    +    // 9d57889b810dcaa1a91d7278fc45c262afba1284/app/src/main/kotlin/de/markusfisch/android/binaryeye/activity/CameraActivity.kt#L434
    +    private fun getRawBytes(result: Result): ByteArray? {
    +        val metadata = result.resultMetadata ?: return null
    +        val segments = metadata[ResultMetadataType.BYTE_SEGMENTS] ?: return null
    +        var bytes = ByteArray(0)
    +        @Suppress("UNCHECKED_CAST")
    +        for (seg in segments as Iterable) {
    +            bytes += seg
    +        }
    +        // byte segments can never be shorter than the text.
    +        // Zxing cuts off content prefixes like "WIFI:"
    +        return if (bytes.size >= result.text.length) bytes else null
    +    }
    +
         companion object {
             private const val EXTRA_OUT_TEXT = "EXTRA_OUT_TEXT"
             private const val EXTRA_OUT_IS_QR_CODE = "EXTRA_OUT_IS_QR_CODE"
    diff --git a/vector/src/main/java/im/vector/riotx/features/rageshake/BugReporter.kt b/vector/src/main/java/im/vector/riotx/features/rageshake/BugReporter.kt
    index dc353363d5..7d58c4aacc 100755
    --- a/vector/src/main/java/im/vector/riotx/features/rageshake/BugReporter.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/rageshake/BugReporter.kt
    @@ -49,11 +49,10 @@ import java.io.File
     import java.io.IOException
     import java.io.OutputStreamWriter
     import java.net.HttpURLConnection
    -import java.util.*
    +import java.util.Locale
     import java.util.zip.GZIPOutputStream
     import javax.inject.Inject
     import javax.inject.Singleton
    -import kotlin.collections.ArrayList
     
     /**
      * BugReporter creates and sends the bug reports.
    @@ -211,7 +210,7 @@ class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSes
                     activeSessionHolder.getSafeActiveSession()?.let { session ->
                         userId = session.myUserId
                         deviceId = session.sessionParams.credentials.deviceId ?: "undefined"
    -                    olmVersion = session.getCryptoVersion(context, true)
    +                    olmVersion = session.cryptoService().getCryptoVersion(context, true)
                     }
     
                     if (!mIsCancelled) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/rageshake/VectorFileLogger.kt b/vector/src/main/java/im/vector/riotx/features/rageshake/VectorFileLogger.kt
    index 6049db6180..16e231491a 100644
    --- a/vector/src/main/java/im/vector/riotx/features/rageshake/VectorFileLogger.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/rageshake/VectorFileLogger.kt
    @@ -24,12 +24,16 @@ import java.io.File
     import java.io.PrintWriter
     import java.io.StringWriter
     import java.text.SimpleDateFormat
    -import java.util.*
    -import java.util.logging.*
    +import java.util.Date
    +import java.util.Locale
    +import java.util.TimeZone
    +import java.util.logging.FileHandler
     import java.util.logging.Formatter
    +import java.util.logging.Level
    +import java.util.logging.LogRecord
    +import java.util.logging.Logger
     import javax.inject.Inject
     import javax.inject.Singleton
    -import kotlin.collections.ArrayList
     
     private const val LOG_SIZE_BYTES = 20 * 1024 * 1024 // 20MB
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/widget/CircleView.kt b/vector/src/main/java/im/vector/riotx/features/reactions/widget/CircleView.kt
    index 486f17ed63..1555c72119 100644
    --- a/vector/src/main/java/im/vector/riotx/features/reactions/widget/CircleView.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/reactions/widget/CircleView.kt
    @@ -17,7 +17,11 @@ package im.vector.riotx.features.reactions.widget
     
     import android.animation.ArgbEvaluator
     import android.content.Context
    -import android.graphics.*
    +import android.graphics.Bitmap
    +import android.graphics.Canvas
    +import android.graphics.Paint
    +import android.graphics.PorterDuff
    +import android.graphics.PorterDuffXfermode
     import android.util.AttributeSet
     import android.util.Property
     import android.view.View
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsController.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsController.kt
    index 83a1768843..aaacb2a170 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsController.kt
    @@ -85,7 +85,7 @@ class PublicRoomsController @Inject constructor(private val stringProvider: Stri
                 avatarRenderer(avatarRenderer)
                 id(publicRoom.roomId)
                 matrixItem(publicRoom.toMatrixItem())
    -            roomAlias(publicRoom.canonicalAlias)
    +            roomAlias(publicRoom.getPrimaryAlias())
                 roomTopic(publicRoom.topic)
                 nbOfMembers(publicRoom.numJoinedMembers)
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsFragment.kt
    index 0cb2a7feca..e466c2311f 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsFragment.kt
    @@ -134,7 +134,7 @@ class PublicRoomsFragment @Inject constructor(
     
         override fun onPublicRoomJoin(publicRoom: PublicRoom) {
             Timber.v("PublicRoomJoinClicked: $publicRoom")
    -        viewModel.handle(RoomDirectoryAction.JoinRoom(publicRoom.roomId))
    +        viewModel.handle(RoomDirectoryAction.JoinRoom(publicRoom.getPrimaryAlias(), publicRoom.roomId))
         }
     
         override fun loadMore() {
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryAction.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryAction.kt
    index 8b32726370..598f26fc3b 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryAction.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryAction.kt
    @@ -23,5 +23,5 @@ sealed class RoomDirectoryAction : VectorViewModelAction {
         data class SetRoomDirectoryData(val roomDirectoryData: RoomDirectoryData) : RoomDirectoryAction()
         data class FilterWith(val filter: String) : RoomDirectoryAction()
         object LoadMore : RoomDirectoryAction()
    -    data class JoinRoom(val roomId: String) : RoomDirectoryAction()
    +    data class JoinRoom(val roomAlias: String?, val roomId: String) : RoomDirectoryAction()
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt
    index 9f81e8b076..53661b075a 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt
    @@ -216,7 +216,7 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
                 )
             }
     
    -        session.joinRoom(action.roomId, callback = object : MatrixCallback {
    +        session.joinRoom(action.roomAlias ?: action.roomId, callback = object : MatrixCallback {
                 override fun onSuccess(data: Unit) {
                     // We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data.
                     // Instead, we wait for the room to be joined
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomViewModel.kt
    index 6c750af5ac..cfe50bb2f7 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomViewModel.kt
    @@ -30,7 +30,6 @@ import im.vector.matrix.android.api.session.Session
     import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
     import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
     import im.vector.matrix.android.api.session.room.model.create.CreateRoomPreset
    -import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
     import im.vector.riotx.core.platform.EmptyViewEvents
     import im.vector.riotx.core.platform.VectorViewModel
     import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
    @@ -91,10 +90,9 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
                     visibility = if (state.isInRoomDirectory) RoomDirectoryVisibility.PUBLIC else RoomDirectoryVisibility.PRIVATE,
                     // Public room
                     preset = if (state.isPublic) CreateRoomPreset.PRESET_PUBLIC_CHAT else CreateRoomPreset.PRESET_PRIVATE_CHAT
    -        ).let {
    -            // Encryption
    -            if (state.isEncrypted) it.enableEncryptionWithAlgorithm(MXCRYPTO_ALGORITHM_MEGOLM) else it
    -        }
    +        )
    +                // Encryption
    +                .enableEncryptionWithAlgorithm(state.isEncrypted)
     
             session.createRoom(createRoomParams, object : MatrixCallback {
                 override fun onSuccess(data: String) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewAction.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewAction.kt
    index 426078fa3d..6b83ada90e 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewAction.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewAction.kt
    @@ -19,5 +19,5 @@ package im.vector.riotx.features.roomdirectory.roompreview
     import im.vector.riotx.core.platform.VectorViewModelAction
     
     sealed class RoomPreviewAction : VectorViewModelAction {
    -    object Join : RoomPreviewAction()
    +    data class Join(val roomAlias: String?) : RoomPreviewAction()
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewActivity.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewActivity.kt
    index 0fdb504c23..a7cb8f2f68 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewActivity.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewActivity.kt
    @@ -32,12 +32,13 @@ import kotlinx.android.parcel.Parcelize
     data class RoomPreviewData(
             val roomId: String,
             val roomName: String?,
    +        val roomAlias: String?,
             val topic: String?,
             val worldReadable: Boolean,
             val avatarUrl: String?
     ) : Parcelable {
         val matrixItem: MatrixItem
    -        get() = MatrixItem.RoomItem(roomId, roomName, avatarUrl)
    +        get() = MatrixItem.RoomItem(roomId, roomName ?: roomAlias, avatarUrl)
     }
     
     class RoomPreviewActivity : VectorBaseActivity(), ToolbarConfigurable {
    @@ -50,6 +51,7 @@ class RoomPreviewActivity : VectorBaseActivity(), ToolbarConfigurable {
                     putExtra(ARG, RoomPreviewData(
                             roomId = publicRoom.roomId,
                             roomName = publicRoom.name,
    +                        roomAlias = publicRoom.getPrimaryAlias(),
                             topic = publicRoom.topic,
                             worldReadable = publicRoom.worldReadable,
                             avatarUrl = publicRoom.avatarUrl
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt
    index 8999b88aba..04ecdb2305 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt
    @@ -50,11 +50,11 @@ class RoomPreviewNoPreviewFragment @Inject constructor(
             setupToolbar(roomPreviewNoPreviewToolbar)
             // Toolbar
             avatarRenderer.render(roomPreviewData.matrixItem, roomPreviewNoPreviewToolbarAvatar)
    -        roomPreviewNoPreviewToolbarTitle.text = roomPreviewData.roomName
    +        roomPreviewNoPreviewToolbarTitle.text = roomPreviewData.roomName ?: roomPreviewData.roomAlias
     
             // Screen
             avatarRenderer.render(roomPreviewData.matrixItem, roomPreviewNoPreviewAvatar)
    -        roomPreviewNoPreviewName.text = roomPreviewData.roomName
    +        roomPreviewNoPreviewName.text = roomPreviewData.roomName ?: roomPreviewData.roomAlias
             roomPreviewNoPreviewTopic.setTextOrHide(roomPreviewData.topic)
     
             if (roomPreviewData.worldReadable) {
    @@ -65,7 +65,7 @@ class RoomPreviewNoPreviewFragment @Inject constructor(
     
             roomPreviewNoPreviewJoin.callback = object : ButtonStateView.Callback {
                 override fun onButtonClicked() {
    -                roomPreviewViewModel.handle(RoomPreviewAction.Join)
    +                roomPreviewViewModel.handle(RoomPreviewAction.Join(roomPreviewData.roomAlias))
                 }
     
                 override fun onRetryClicked() {
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt
    index b4aae0d116..3f8ae03029 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt
    @@ -26,6 +26,7 @@ import im.vector.matrix.android.api.session.Session
     import im.vector.matrix.android.api.session.room.model.Membership
     import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
     import im.vector.matrix.rx.rx
    +import im.vector.riotx.core.extensions.exhaustive
     import im.vector.riotx.core.platform.EmptyViewEvents
     import im.vector.riotx.core.platform.VectorViewModel
     import im.vector.riotx.features.roomdirectory.JoinState
    @@ -82,11 +83,11 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted initialState: R
     
         override fun handle(action: RoomPreviewAction) {
             when (action) {
    -            RoomPreviewAction.Join -> joinRoom()
    -        }
    +            is RoomPreviewAction.Join -> handleJoinRoom(action)
    +        }.exhaustive
         }
     
    -    private fun joinRoom() = withState { state ->
    +    private fun handleJoinRoom(action: RoomPreviewAction.Join) = withState { state ->
             if (state.roomJoinState == JoinState.JOINING) {
                 // Request already sent, should not happen
                 Timber.w("Try to join an already joining room. Should not happen")
    @@ -100,7 +101,7 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted initialState: R
                 )
             }
     
    -        session.joinRoom(state.roomId, callback = object : MatrixCallback {
    +        session.joinRoom(action.roomAlias ?: state.roomId, callback = object : MatrixCallback {
                 override fun onSuccess(data: Unit) {
                     // We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data.
                     // Instead, we wait for the room to be joined
    diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileAction.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileAction.kt
    index e352683841..1dc2459538 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileAction.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileAction.kt
    @@ -23,4 +23,5 @@ sealed class RoomMemberProfileAction : VectorViewModelAction {
         object RetryFetchingInfo : RoomMemberProfileAction()
         object IgnoreUser : RoomMemberProfileAction()
         object VerifyUser : RoomMemberProfileAction()
    +    object ShareRoomMemberProfile : RoomMemberProfileAction()
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileFragment.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileFragment.kt
    index 89b37400f1..28734af0ad 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileFragment.kt
    @@ -19,6 +19,7 @@ package im.vector.riotx.features.roommemberprofile
     
     import android.os.Bundle
     import android.os.Parcelable
    +import android.view.MenuItem
     import android.view.View
     import androidx.appcompat.app.AlertDialog
     import androidx.core.view.isVisible
    @@ -38,6 +39,7 @@ import im.vector.riotx.core.extensions.exhaustive
     import im.vector.riotx.core.extensions.setTextOrHide
     import im.vector.riotx.core.platform.StateView
     import im.vector.riotx.core.platform.VectorBaseFragment
    +import im.vector.riotx.core.utils.startSharePlainTextIntent
     import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
     import im.vector.riotx.features.home.AvatarRenderer
     import im.vector.riotx.features.roommemberprofile.devices.DeviceListBottomSheet
    @@ -65,6 +67,8 @@ class RoomMemberProfileFragment @Inject constructor(
     
         override fun getLayoutResId() = R.layout.fragment_matrix_profile
     
    +    override fun getMenuRes() = R.menu.vector_room_member_profile
    +
         override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
             super.onViewCreated(view, savedInstanceState)
             setupToolbar(matrixProfileToolbar)
    @@ -90,14 +94,25 @@ class RoomMemberProfileFragment @Inject constructor(
             matrixProfileAppBarLayout.addOnOffsetChangedListener(appBarStateChangeListener)
             viewModel.observeViewEvents {
                 when (it) {
    -                is RoomMemberProfileViewEvents.Loading               -> showLoading(it.message)
    -                is RoomMemberProfileViewEvents.Failure               -> showFailure(it.throwable)
    -                is RoomMemberProfileViewEvents.OnIgnoreActionSuccess -> Unit
    -                is RoomMemberProfileViewEvents.StartVerification     -> handleStartVerification(it)
    +                is RoomMemberProfileViewEvents.Loading                -> showLoading(it.message)
    +                is RoomMemberProfileViewEvents.Failure                -> showFailure(it.throwable)
    +                is RoomMemberProfileViewEvents.OnIgnoreActionSuccess  -> Unit
    +                is RoomMemberProfileViewEvents.StartVerification      -> handleStartVerification(it)
    +                is RoomMemberProfileViewEvents.ShareRoomMemberProfile -> handleShareRoomMemberProfile(it.permalink)
                 }.exhaustive
             }
         }
     
    +    override fun onOptionsItemSelected(item: MenuItem): Boolean {
    +        when (item.itemId) {
    +            R.id.roomMemberProfileShareAction -> {
    +                viewModel.handle(RoomMemberProfileAction.ShareRoomMemberProfile)
    +                return true
    +            }
    +        }
    +        return super.onOptionsItemSelected(item)
    +    }
    +
         private fun handleStartVerification(startVerification: RoomMemberProfileViewEvents.StartVerification) {
             if (startVerification.canCrossSign) {
                 VerificationBottomSheet
    @@ -177,6 +192,13 @@ class RoomMemberProfileFragment @Inject constructor(
                     } else {
                         memberProfileDecorationImageView.isVisible = false
                     }
    +
    +                memberProfileAvatarView.setOnClickListener { view ->
    +                    onAvatarClicked(view, userMatrixItem)
    +                }
    +                matrixProfileToolbarAvatarImageView.setOnClickListener { view ->
    +                    onAvatarClicked(view, userMatrixItem)
    +                }
                 }
             }
             memberProfilePowerLevelView.setTextOrHide(state.userPowerLevelString())
    @@ -208,4 +230,12 @@ class RoomMemberProfileFragment @Inject constructor(
         override fun onMentionClicked() {
             vectorBaseActivity.notImplemented("Mention")
         }
    +
    +    private fun handleShareRoomMemberProfile(permalink: String) {
    +        startSharePlainTextIntent(fragment = this, chooserTitle = null, text = permalink)
    +    }
    +
    +    private fun onAvatarClicked(view: View, userMatrixItem: MatrixItem) {
    +        navigator.openBigImageViewer(requireActivity(), view, userMatrixItem)
    +    }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewEvents.kt
    index 5b7f5bf54c..5d8757a337 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewEvents.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewEvents.kt
    @@ -31,4 +31,6 @@ sealed class RoomMemberProfileViewEvents : VectorViewEvents {
                 val userId: String,
                 val canCrossSign: Boolean
         ) : RoomMemberProfileViewEvents()
    +
    +    data class ShareRoomMemberProfile(val permalink: String) : RoomMemberProfileViewEvents()
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewModel.kt
    index e3dd53104c..44c214bc99 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewModel.kt
    @@ -26,6 +26,7 @@ import com.airbnb.mvrx.ViewModelContext
     import com.squareup.inject.assisted.Assisted
     import com.squareup.inject.assisted.AssistedInject
     import im.vector.matrix.android.api.MatrixCallback
    +import im.vector.matrix.android.api.permalinks.PermalinkFactory
     import im.vector.matrix.android.api.query.QueryStringValue
     import im.vector.matrix.android.api.session.Session
     import im.vector.matrix.android.api.session.events.model.EventType
    @@ -135,9 +136,10 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
     
         override fun handle(action: RoomMemberProfileAction) {
             when (action) {
    -            is RoomMemberProfileAction.RetryFetchingInfo -> fetchProfileInfo()
    -            is RoomMemberProfileAction.IgnoreUser        -> handleIgnoreAction()
    -            is RoomMemberProfileAction.VerifyUser        -> prepareVerification()
    +            is RoomMemberProfileAction.RetryFetchingInfo      -> fetchProfileInfo()
    +            is RoomMemberProfileAction.IgnoreUser             -> handleIgnoreAction()
    +            is RoomMemberProfileAction.VerifyUser             -> prepareVerification()
    +            is RoomMemberProfileAction.ShareRoomMemberProfile -> handleShareRoomMemberProfile()
             }
         }
     
    @@ -148,7 +150,7 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
                     // ok, let's find or create the DM room
                     _viewEvents.post(RoomMemberProfileViewEvents.StartVerification(
                             userId = state.userId,
    -                        canCrossSign = session.getCrossSigningService().canCrossSign()
    +                        canCrossSign = session.cryptoService().crossSigningService().canCrossSign()
                     ))
                 }
             }
    @@ -234,4 +236,10 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
                 session.ignoreUserIds(listOf(state.userId), ignoreActionCallback)
             }
         }
    +
    +    private fun handleShareRoomMemberProfile() {
    +        PermalinkFactory.createPermalink(initialState.userId)?.let { permalink ->
    +            _viewEvents.post(RoomMemberProfileViewEvents.ShareRoomMemberProfile(permalink))
    +        }
    +    }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt
    index b12ed58a73..036c9269df 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt
    @@ -26,7 +26,7 @@ import com.squareup.inject.assisted.Assisted
     import com.squareup.inject.assisted.AssistedInject
     import im.vector.matrix.android.api.session.Session
     import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
    -import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod
    +import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod
     import im.vector.matrix.android.api.util.MatrixItem
     import im.vector.matrix.android.api.util.toMatrixItem
     import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
    @@ -100,7 +100,7 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted priva
         }
     
         private fun manuallyVerify(action: DeviceListAction.ManuallyVerify) {
    -        session.getVerificationService().beginKeyVerification(VerificationMethod.SAS, userId, action.deviceId, null)?.let { txID ->
    +        session.cryptoService().verificationService().beginKeyVerification(VerificationMethod.SAS, userId, action.deviceId, null)?.let { txID ->
                 _viewEvents.post(DeviceListBottomSheetViewEvents.Verify(userId, txID))
             }
         }
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileAction.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileAction.kt
    index d3852e34c8..545d67c314 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileAction.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileAction.kt
    @@ -23,4 +23,5 @@ import im.vector.riotx.core.platform.VectorViewModelAction
     sealed class RoomProfileAction: VectorViewModelAction {
         object LeaveRoom: RoomProfileAction()
         data class ChangeRoomNotificationState(val notificationState: RoomNotificationState) : RoomProfileAction()
    +    object ShareRoomProfile : RoomProfileAction()
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileController.kt
    index ab1774233a..5f8c000497 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileController.kt
    @@ -25,10 +25,12 @@ import im.vector.riotx.core.epoxy.profiles.buildProfileSection
     import im.vector.riotx.core.resources.ColorProvider
     import im.vector.riotx.core.resources.StringProvider
     import im.vector.riotx.core.ui.list.genericFooterItem
    +import im.vector.riotx.features.settings.VectorPreferences
     import javax.inject.Inject
     
     class RoomProfileController @Inject constructor(
             private val stringProvider: StringProvider,
    +        private val vectorPreferences: VectorPreferences,
             colorProvider: ColorProvider
     ) : TypedEpoxyController() {
     
    @@ -43,6 +45,7 @@ class RoomProfileController @Inject constructor(
             fun onUploadsClicked()
             fun onSettingsClicked()
             fun onLeaveRoomClicked()
    +        fun onRoomIdClicked()
         }
     
         override fun buildModels(data: RoomProfileViewState?) {
    @@ -105,5 +108,19 @@ class RoomProfileController @Inject constructor(
                     editable = false,
                     action = { callback?.onLeaveRoomClicked() }
             )
    +
    +        // Advanced
    +        if (vectorPreferences.developerMode()) {
    +            buildProfileSection(stringProvider.getString(R.string.room_settings_category_advanced_title))
    +            buildProfileAction(
    +                    id = "roomId",
    +                    title = stringProvider.getString(R.string.room_settings_room_internal_id),
    +                    subtitle = roomSummary.roomId,
    +                    dividerColor = dividerColor,
    +                    divider = false,
    +                    editable = false,
    +                    action = { callback?.onRoomIdClicked() }
    +            )
    +        }
         }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt
    index 1eafc8af3f..58c8fead32 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt
    @@ -19,6 +19,7 @@ package im.vector.riotx.features.roomprofile
     
     import android.os.Bundle
     import android.os.Parcelable
    +import android.view.MenuItem
     import android.view.View
     import androidx.appcompat.app.AlertDialog
     import androidx.core.view.isVisible
    @@ -26,6 +27,7 @@ import com.airbnb.mvrx.args
     import com.airbnb.mvrx.fragmentViewModel
     import com.airbnb.mvrx.withState
     import im.vector.matrix.android.api.session.room.notification.RoomNotificationState
    +import im.vector.matrix.android.api.util.MatrixItem
     import im.vector.matrix.android.api.util.toMatrixItem
     import im.vector.riotx.R
     import im.vector.riotx.core.animations.AppBarStateChangeListener
    @@ -35,6 +37,8 @@ import im.vector.riotx.core.extensions.configureWith
     import im.vector.riotx.core.extensions.exhaustive
     import im.vector.riotx.core.extensions.setTextOrHide
     import im.vector.riotx.core.platform.VectorBaseFragment
    +import im.vector.riotx.core.utils.copyToClipboard
    +import im.vector.riotx.core.utils.startSharePlainTextIntent
     import im.vector.riotx.features.crypto.util.toImageRes
     import im.vector.riotx.features.home.AvatarRenderer
     import im.vector.riotx.features.home.room.list.actions.RoomListActionsArgs
    @@ -67,6 +71,8 @@ class RoomProfileFragment @Inject constructor(
     
         override fun getLayoutResId() = R.layout.fragment_matrix_profile
     
    +    override fun getMenuRes() = R.menu.vector_room_profile
    +
         override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
             super.onViewCreated(view, savedInstanceState)
             roomListQuickActionsSharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java)
    @@ -89,6 +95,7 @@ class RoomProfileFragment @Inject constructor(
                     is RoomProfileViewEvents.Loading            -> showLoading(it.message)
                     is RoomProfileViewEvents.Failure            -> showFailure(it.throwable)
                     is RoomProfileViewEvents.OnLeaveRoomSuccess -> onLeaveRoom()
    +                is RoomProfileViewEvents.ShareRoomProfile   -> onShareRoomProfile(it.permalink)
                 }.exhaustive
             }
             roomListQuickActionsSharedActionViewModel
    @@ -97,6 +104,16 @@ class RoomProfileFragment @Inject constructor(
                     .disposeOnDestroyView()
         }
     
    +    override fun onOptionsItemSelected(item: MenuItem): Boolean {
    +        when (item.itemId) {
    +            R.id.roomProfileShareAction -> {
    +                roomProfileViewModel.handle(RoomProfileAction.ShareRoomProfile)
    +                return true
    +            }
    +        }
    +        return super.onOptionsItemSelected(item)
    +    }
    +
         private fun handleQuickActions(action: RoomListQuickActionsSharedAction) = when (action) {
             is RoomListQuickActionsSharedAction.NotificationsAllNoisy     -> {
                 roomProfileViewModel.handle(RoomProfileAction.ChangeRoomNotificationState(RoomNotificationState.ALL_MESSAGES_NOISY))
    @@ -148,6 +165,13 @@ class RoomProfileFragment @Inject constructor(
                     roomProfileDecorationImageView.isVisible = it.roomEncryptionTrustLevel != null
                     roomProfileDecorationImageView.setImageResource(it.roomEncryptionTrustLevel.toImageRes())
                     matrixProfileDecorationToolbarAvatarImageView.setImageResource(it.roomEncryptionTrustLevel.toImageRes())
    +
    +                roomProfileAvatarView.setOnClickListener { view ->
    +                    onAvatarClicked(view, matrixItem)
    +                }
    +                matrixProfileToolbarAvatarImageView.setOnClickListener { view ->
    +                    onAvatarClicked(view, matrixItem)
    +                }
                 }
             }
             roomProfileController.setData(state)
    @@ -187,4 +211,16 @@ class RoomProfileFragment @Inject constructor(
                     .setNegativeButton(R.string.cancel, null)
                     .show()
         }
    +
    +    override fun onRoomIdClicked() {
    +        copyToClipboard(requireContext(), roomProfileArgs.roomId)
    +    }
    +
    +    private fun onShareRoomProfile(permalink: String) {
    +        startSharePlainTextIntent(fragment = this, chooserTitle = null, text = permalink)
    +    }
    +
    +    private fun onAvatarClicked(view: View, matrixItem: MatrixItem.RoomItem) {
    +        navigator.openBigImageViewer(requireActivity(), view, matrixItem)
    +    }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewEvents.kt
    index 50b3c136e5..7a08a08126 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewEvents.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewEvents.kt
    @@ -26,4 +26,5 @@ sealed class RoomProfileViewEvents : VectorViewEvents {
         data class Failure(val throwable: Throwable) : RoomProfileViewEvents()
     
         object OnLeaveRoomSuccess : RoomProfileViewEvents()
    +    data class ShareRoomProfile(val permalink: String) : RoomProfileViewEvents()
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt
    index 6c66ac67b2..d49727d12d 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt
    @@ -23,6 +23,7 @@ import com.airbnb.mvrx.ViewModelContext
     import com.squareup.inject.assisted.Assisted
     import com.squareup.inject.assisted.AssistedInject
     import im.vector.matrix.android.api.MatrixCallback
    +import im.vector.matrix.android.api.permalinks.PermalinkFactory
     import im.vector.matrix.android.api.session.Session
     import im.vector.matrix.rx.rx
     import im.vector.matrix.rx.unwrap
    @@ -30,7 +31,7 @@ import im.vector.riotx.R
     import im.vector.riotx.core.platform.VectorViewModel
     import im.vector.riotx.core.resources.StringProvider
     
    -class RoomProfileViewModel @AssistedInject constructor(@Assisted initialState: RoomProfileViewState,
    +class RoomProfileViewModel @AssistedInject constructor(@Assisted private val initialState: RoomProfileViewState,
                                                            private val stringProvider: StringProvider,
                                                            private val session: Session)
         : VectorViewModel(initialState) {
    @@ -66,6 +67,7 @@ class RoomProfileViewModel @AssistedInject constructor(@Assisted initialState: R
         override fun handle(action: RoomProfileAction) = when (action) {
             RoomProfileAction.LeaveRoom                      -> handleLeaveRoom()
             is RoomProfileAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action)
    +        is RoomProfileAction.ShareRoomProfile            -> handleShareRoomProfile()
         }
     
         private fun handleChangeNotificationMode(action: RoomProfileAction.ChangeRoomNotificationState) {
    @@ -88,4 +90,10 @@ class RoomProfileViewModel @AssistedInject constructor(@Assisted initialState: R
                 }
             })
         }
    +
    +    private fun handleShareRoomProfile() {
    +        PermalinkFactory.createPermalink(initialState.roomId)?.let { permalink ->
    +            _viewEvents.post(RoomProfileViewEvents.ShareRoomProfile(permalink))
    +        }
    +    }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewModel.kt
    index 72b4af4474..81b2809c4f 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewModel.kt
    @@ -95,7 +95,7 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState
                 room.rx().liveRoomMembers(roomMemberQueryParams)
                         .observeOn(AndroidSchedulers.mainThread())
                         .switchMap { membersSummary ->
    -                        session.getLiveCryptoDeviceInfo(membersSummary.map { it.userId })
    +                        session.cryptoService().getLiveCryptoDeviceInfo(membersSummary.map { it.userId })
                                     .asObservable()
                                     .doOnError { Timber.e(it) }
                                     .map { deviceList ->
    @@ -104,7 +104,7 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState
                                             val allDeviceTrusted = it.value.fold(it.value.isNotEmpty()) { prev, next ->
                                                 prev && next.trustLevel?.isCrossSigningVerified().orFalse()
                                             }
    -                                        if (session.getCrossSigningService().getUserCrossSigningKeys(it.key)?.isTrusted().orFalse()) {
    +                                        if (session.cryptoService().crossSigningService().getUserCrossSigningKeys(it.key)?.isTrusted().orFalse()) {
                                                 if (allDeviceTrusted) RoomEncryptionTrustLevel.Trusted else RoomEncryptionTrustLevel.Warning
                                             } else {
                                                 RoomEncryptionTrustLevel.Default
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt
    index 93931fe71d..805fa53e96 100644
    --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt
    @@ -28,9 +28,6 @@ import kotlinx.coroutines.GlobalScope
     import kotlinx.coroutines.launch
     import timber.log.Timber
     import java.util.Locale
    -import kotlin.Comparator
    -import kotlin.collections.ArrayList
    -import kotlin.collections.HashSet
     
     /**
      * Object to manage the Locale choice of the user
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt
    index 17f440c3dc..0a670e2c5a 100644
    --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt
    @@ -41,7 +41,12 @@ import im.vector.riotx.core.extensions.showPassword
     import im.vector.riotx.core.platform.SimpleTextWatcher
     import im.vector.riotx.core.preference.UserAvatarPreference
     import im.vector.riotx.core.preference.VectorPreference
    -import im.vector.riotx.core.utils.*
    +import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA
    +import im.vector.riotx.core.utils.TextUtils
    +import im.vector.riotx.core.utils.allGranted
    +import im.vector.riotx.core.utils.copyToClipboard
    +import im.vector.riotx.core.utils.getSizeOfFiles
    +import im.vector.riotx.core.utils.toast
     import im.vector.riotx.features.MainActivity
     import im.vector.riotx.features.MainActivityArgs
     import im.vector.riotx.features.themes.ThemeUtils
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsHelpAboutFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsHelpAboutFragment.kt
    index 6c10b8695d..7b892ec88b 100644
    --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsHelpAboutFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsHelpAboutFragment.kt
    @@ -77,7 +77,7 @@ class VectorSettingsHelpAboutFragment @Inject constructor(
     
             // olm version
             findPreference(VectorPreferences.SETTINGS_OLM_VERSION_PREFERENCE_KEY)!!
    -                .summary = session.getCryptoVersion(requireContext(), false)
    +                .summary = session.cryptoService().getCryptoVersion(requireContext(), false)
     
             // copyright
             findPreference(VectorPreferences.SETTINGS_COPYRIGHT_PREFERENCE_KEY)!!
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsLabsFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsLabsFragment.kt
    index 8b1a2dba31..66c56455b9 100644
    --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsLabsFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsLabsFragment.kt
    @@ -37,7 +37,7 @@ class VectorSettingsLabsFragment @Inject constructor(
     //        val useCryptoPref = findPreference(VectorPreferences.SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY) as SwitchPreference
     //        val cryptoIsEnabledPref = findPreference(VectorPreferences.SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY)
     
    -        if (session.isCryptoEnabled()) {
    +        if (session.cryptoService().isCryptoEnabled()) {
     //            mLabsCategory.removePreference(useCryptoPref)
     //
     //            cryptoIsEnabledPref.isEnabled = false
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsNotificationPreferenceFragment.kt
    index b397cd1cf6..5e7bc9d95b 100644
    --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsNotificationPreferenceFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsNotificationPreferenceFragment.kt
    @@ -118,6 +118,10 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor(
                                     }
     
                                     override fun onFailure(failure: Throwable) {
    +                                    if (!isAdded) {
    +                                        return
    +                                    }
    +
                                         // revert the check box
                                         switchPref.isChecked = !switchPref.isChecked
                                         Toast.makeText(activity, R.string.unknown_error, Toast.LENGTH_SHORT).show()
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    index 0637d6d545..4324c79dfd 100644
    --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    @@ -134,10 +134,10 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
     
         private fun refreshXSigningStatus() {
             if (vectorPreferences.developerMode()) {
    -            val crossSigningKeys = session.getCrossSigningService().getMyCrossSigningKeys()
    +            val crossSigningKeys = session.cryptoService().crossSigningService().getMyCrossSigningKeys()
                 val xSigningIsEnableInAccount = crossSigningKeys != null
    -            val xSigningKeysAreTrusted = session.getCrossSigningService().checkUserTrust(session.myUserId).isVerified()
    -            val xSigningKeyCanSign = session.getCrossSigningService().canCrossSign()
    +            val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified()
    +            val xSigningKeyCanSign = session.cryptoService().crossSigningService().canCrossSign()
     
                 if (xSigningKeyCanSign) {
                     mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_trusted)
    @@ -412,10 +412,10 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
     
             sendToUnverifiedDevicesPref.isChecked = false
     
    -        sendToUnverifiedDevicesPref.isChecked = session.getGlobalBlacklistUnverifiedDevices()
    +        sendToUnverifiedDevicesPref.isChecked = session.cryptoService().getGlobalBlacklistUnverifiedDevices()
     
             sendToUnverifiedDevicesPref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
    -            session.setGlobalBlacklistUnverifiedDevices(sendToUnverifiedDevicesPref.isChecked)
    +            session.cryptoService().setGlobalBlacklistUnverifiedDevices(sendToUnverifiedDevicesPref.isChecked)
     
                 true
             }
    @@ -426,7 +426,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
         // ==============================================================================================================
     
         private fun refreshMyDevice() {
    -        session.getUserDevices(session.myUserId).map {
    +        session.cryptoService().getUserDevices(session.myUserId).map {
                 DeviceInfo(
                         user_id = session.myUserId,
                         deviceId = it.deviceId,
    @@ -436,7 +436,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
                 refreshCryptographyPreference(it)
             }
             // TODO Move to a ViewModel...
    -        session.getDevicesList(object : MatrixCallback {
    +        session.cryptoService().getDevicesList(object : MatrixCallback {
                 override fun onSuccess(data: DevicesListResponse) {
                     if (isAdded) {
                         refreshCryptographyPreference(data.devices ?: emptyList())
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningEpoxyController.kt
    index 681b6030bf..cf74e83b1f 100644
    --- a/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningEpoxyController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningEpoxyController.kt
    @@ -36,6 +36,7 @@ class CrossSigningEpoxyController @Inject constructor(
         interface InteractionListener {
             fun onInitializeCrossSigningKeys()
             fun onResetCrossSigningKeys()
    +        fun verifySession()
         }
     
         var interactionListener: InteractionListener? = null
    @@ -77,21 +78,31 @@ class CrossSigningEpoxyController @Inject constructor(
                             interactionListener?.onResetCrossSigningKeys()
                         }
                     }
    -            } else if (data.xSigningIsEnableInAccount) {
    -                genericItem {
    -                    id("enable")
    -                    titleIconResourceId(R.drawable.ic_shield_black)
    -                    title(stringProvider.getString(R.string.encryption_information_dg_xsigning_not_trusted))
    +            }
    +        } else if (data.xSigningIsEnableInAccount) {
    +            genericItem {
    +                id("enable")
    +                titleIconResourceId(R.drawable.ic_shield_black)
    +                title(stringProvider.getString(R.string.encryption_information_dg_xsigning_not_trusted))
    +            }
    +            bottomSheetVerificationActionItem {
    +                id("verify")
    +                title(stringProvider.getString(R.string.complete_security))
    +                titleColor(colorProvider.getColor(R.color.riotx_positive_accent))
    +                iconRes(R.drawable.ic_arrow_right)
    +                iconColor(colorProvider.getColor(R.color.riotx_positive_accent))
    +                listener {
    +                    interactionListener?.verifySession()
                     }
    -                bottomSheetVerificationActionItem {
    -                    id("resetkeys")
    -                    title("Reset keys")
    -                    titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
    -                    iconRes(R.drawable.ic_arrow_right)
    -                    iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
    -                    listener {
    -                        interactionListener?.onResetCrossSigningKeys()
    -                    }
    +            }
    +            bottomSheetVerificationActionItem {
    +                id("resetkeys")
    +                title("Reset keys")
    +                titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
    +                iconRes(R.drawable.ic_arrow_right)
    +                iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
    +                listener {
    +                    interactionListener?.onResetCrossSigningKeys()
                     }
                 }
             } else {
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsFragment.kt
    index ec56929002..76835211cb 100644
    --- a/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsFragment.kt
    @@ -54,6 +54,11 @@ class CrossSigningSettingsFragment @Inject constructor(
                     is CrossSigningSettingsViewEvents.RequestPassword -> {
                         requestPassword()
                     }
    +                CrossSigningSettingsViewEvents.VerifySession      -> {
    +                    (requireActivity() as? VectorBaseActivity)?.let { activity ->
    +                        activity.navigator.waitSessionVerification(activity)
    +                    }
    +                }
                 }.exhaustive
             }
         }
    @@ -93,6 +98,10 @@ class CrossSigningSettingsFragment @Inject constructor(
             viewModel.handle(CrossSigningAction.InitializeCrossSigning)
         }
     
    +    override fun verifySession() {
    +        viewModel.handle(CrossSigningAction.VerifySession)
    +    }
    +
         override fun onResetCrossSigningKeys() {
             AlertDialog.Builder(requireContext())
                     .setTitle(R.string.dialog_title_confirmation)
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsViewEvents.kt
    index b03707b363..65a4a18485 100644
    --- a/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsViewEvents.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsViewEvents.kt
    @@ -25,4 +25,5 @@ sealed class CrossSigningSettingsViewEvents : VectorViewEvents {
         data class Failure(val throwable: Throwable) : CrossSigningSettingsViewEvents()
     
         object RequestPassword : CrossSigningSettingsViewEvents()
    +    object VerifySession : CrossSigningSettingsViewEvents()
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsViewModel.kt
    index 2ad2e8c1ca..f18e0b3cc7 100644
    --- a/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsViewModel.kt
    @@ -45,6 +45,7 @@ data class CrossSigningSettingsViewState(
     
     sealed class CrossSigningAction : VectorViewModelAction {
         object InitializeCrossSigning : CrossSigningAction()
    +    object VerifySession : CrossSigningAction()
         data class PasswordEntered(val password: String) : CrossSigningAction()
     }
     
    @@ -57,8 +58,8 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted privat
                     .execute {
                         val crossSigningKeys = it.invoke()?.getOrNull()
                         val xSigningIsEnableInAccount = crossSigningKeys != null
    -                    val xSigningKeysAreTrusted = session.getCrossSigningService().checkUserTrust(session.myUserId).isVerified()
    -                    val xSigningKeyCanSign = session.getCrossSigningService().canCrossSign()
    +                    val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified()
    +                    val xSigningKeyCanSign = session.cryptoService().crossSigningService().canCrossSign()
                         copy(
                                 crossSigningInfo = crossSigningKeys,
                                 xSigningIsEnableInAccount = xSigningIsEnableInAccount,
    @@ -88,6 +89,9 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted privat
                             password = action.password
                     ))
                 }
    +            CrossSigningAction.VerifySession             -> {
    +                _viewEvents.post(CrossSigningSettingsViewEvents.VerifySession)
    +            }
             }.exhaustive
         }
     
    @@ -97,7 +101,7 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted privat
             setState {
                 copy(isUploadingKeys = true)
             }
    -        session.getCrossSigningService().initializeCrossSigning(auth, object : MatrixCallback {
    +        session.cryptoService().crossSigningService().initializeCrossSigning(auth, object : MatrixCallback {
                 override fun onSuccess(data: Unit) {
                     _pendingSession = null
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt
    index f2625cbc37..7ee79a279f 100644
    --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt
    @@ -63,7 +63,7 @@ class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@As
             setState {
                 copy(deviceInfo = Loading())
             }
    -        session.getDeviceInfo(deviceId, object : MatrixCallback {
    +        session.cryptoService().getDeviceInfo(deviceId, object : MatrixCallback {
                 override fun onSuccess(data: DeviceInfo) {
                     setState {
                         copy(deviceInfo = Success(data))
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt
    index b931f5d66f..ce6e388af6 100644
    --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt
    @@ -30,9 +30,9 @@ import com.squareup.inject.assisted.AssistedInject
     import im.vector.matrix.android.api.MatrixCallback
     import im.vector.matrix.android.api.failure.Failure
     import im.vector.matrix.android.api.session.Session
    -import im.vector.matrix.android.api.session.crypto.sas.VerificationService
    -import im.vector.matrix.android.api.session.crypto.sas.VerificationTransaction
    -import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
    +import im.vector.matrix.android.api.session.crypto.verification.VerificationService
    +import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
    +import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
     import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
     import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
     import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
    @@ -74,7 +74,7 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
     
         init {
             refreshDevicesList()
    -        session.getVerificationService().addListener(this)
    +        session.cryptoService().verificationService().addListener(this)
     
             session.rx().liveUserCryptoDevices(session.myUserId)
                     .execute {
    @@ -85,7 +85,7 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
         }
     
         override fun onCleared() {
    -        session.getVerificationService().removeListener(this)
    +        session.cryptoService().verificationService().removeListener(this)
             super.onCleared()
         }
     
    @@ -103,7 +103,7 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
         private fun refreshDevicesList() {
             if (!session.sessionParams.credentials.deviceId.isNullOrEmpty()) {
                 // display something asap
    -            val localKnown = session.getUserDevices(session.myUserId).map {
    +            val localKnown = session.cryptoService().getUserDevices(session.myUserId).map {
                     DeviceInfo(
                             user_id = session.myUserId,
                             deviceId = it.deviceId,
    @@ -118,7 +118,7 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
                     )
                 }
     
    -            session.getDevicesList(object : MatrixCallback {
    +            session.cryptoService().getDevicesList(object : MatrixCallback {
                     override fun onSuccess(data: DevicesListResponse) {
                         setState {
                             copy(
    @@ -141,16 +141,16 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
                 setState {
                     copy(
                             myDeviceId = session.sessionParams.credentials.deviceId ?: "",
    -                        cryptoDevices = Success(session.getUserDevices(session.myUserId))
    +                        cryptoDevices = Success(session.cryptoService().getUserDevices(session.myUserId))
                     )
                 }
     
                 // then force download
    -            session.downloadKeys(listOf(session.myUserId), true, object : MatrixCallback> {
    +            session.cryptoService().downloadKeys(listOf(session.myUserId), true, object : MatrixCallback> {
                     override fun onSuccess(data: MXUsersDevicesMap) {
                         setState {
                             copy(
    -                                cryptoDevices = Success(session.getUserDevices(session.myUserId))
    +                                cryptoDevices = Success(session.cryptoService().getUserDevices(session.myUserId))
                             )
                         }
                     }
    @@ -172,7 +172,7 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
         }
     
         private fun handleVerify(action: DevicesAction.VerifyMyDevice) {
    -        val txID = session.getVerificationService().requestKeyVerification(supportedVerificationMethods, session.myUserId, listOf(action.deviceId))
    +        val txID = session.cryptoService().verificationService().requestKeyVerification(supportedVerificationMethods, session.myUserId, listOf(action.deviceId))
             _viewEvents.post(DevicesViewEvents.ShowVerifyDevice(
                     session.myUserId,
                     txID.transactionId
    @@ -187,7 +187,7 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
         }
     
         private fun handleRename(action: DevicesAction.Rename) {
    -        session.setDeviceName(action.deviceId, action.newName, object : MatrixCallback {
    +        session.cryptoService().setDeviceName(action.deviceId, action.newName, object : MatrixCallback {
                 override fun onSuccess(data: Unit) {
                     setState {
                         copy(
    @@ -222,7 +222,7 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
                 )
             }
     
    -        session.deleteDevice(deviceId, object : MatrixCallback {
    +        session.cryptoService().deleteDevice(deviceId, object : MatrixCallback {
                 override fun onFailure(failure: Throwable) {
                     var isPasswordRequestFound = false
     
    @@ -284,7 +284,7 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
                 )
             }
     
    -        session.deleteDeviceWithUserPassword(currentDeviceId, _currentSession, action.password, object : MatrixCallback {
    +        session.cryptoService().deleteDeviceWithUserPassword(currentDeviceId, _currentSession, action.password, object : MatrixCallback {
                 override fun onSuccess(data: Unit) {
                     _currentDeviceId = null
                     _currentSession = null
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devtools/AccountDataEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/settings/devtools/AccountDataEpoxyController.kt
    new file mode 100644
    index 0000000000..c8a09bfb64
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/devtools/AccountDataEpoxyController.kt
    @@ -0,0 +1,79 @@
    +/*
    + * Copyright (c) 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.riotx.features.settings.devtools
    +
    +import android.view.View
    +import com.airbnb.epoxy.TypedEpoxyController
    +import com.airbnb.mvrx.Fail
    +import com.airbnb.mvrx.Loading
    +import com.airbnb.mvrx.Success
    +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
    +import im.vector.riotx.R
    +import im.vector.riotx.core.epoxy.loadingItem
    +import im.vector.riotx.core.resources.StringProvider
    +import im.vector.riotx.core.ui.list.genericFooterItem
    +import im.vector.riotx.core.ui.list.genericItemWithValue
    +import im.vector.riotx.core.utils.DebouncedClickListener
    +import javax.inject.Inject
    +
    +class AccountDataEpoxyController @Inject constructor(
    +        private val stringProvider: StringProvider
    +) : TypedEpoxyController() {
    +
    +    interface InteractionListener {
    +        fun didTap(data: UserAccountData)
    +    }
    +
    +    var interactionListener: InteractionListener? = null
    +
    +    override fun buildModels(data: AccountDataViewState?) {
    +        if (data == null) return
    +        when (data.accountData) {
    +            is Loading -> {
    +                loadingItem {
    +                    id("loading")
    +                    loadingText(stringProvider.getString(R.string.loading))
    +                }
    +            }
    +            is Fail    -> {
    +                genericFooterItem {
    +                    id("fail")
    +                    text(data.accountData.error.localizedMessage)
    +                }
    +            }
    +            is Success -> {
    +                val dataList = data.accountData.invoke()
    +                if (dataList.isEmpty()) {
    +                    genericFooterItem {
    +                        id("noResults")
    +                        text(stringProvider.getString(R.string.no_result_placeholder))
    +                    }
    +                } else {
    +                    dataList.forEach { accountData ->
    +                        genericItemWithValue {
    +                            id(accountData.type)
    +                            title(accountData.type)
    +                            itemClickAction(DebouncedClickListener(View.OnClickListener {
    +                                interactionListener?.didTap(accountData)
    +                            }))
    +                        }
    +                    }
    +                }
    +            }
    +        }
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devtools/AccountDataFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/devtools/AccountDataFragment.kt
    new file mode 100644
    index 0000000000..7a57a03deb
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/devtools/AccountDataFragment.kt
    @@ -0,0 +1,79 @@
    +/*
    + * Copyright (c) 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.riotx.features.settings.devtools
    +
    +import android.os.Bundle
    +import android.view.View
    +import com.airbnb.mvrx.fragmentViewModel
    +import com.airbnb.mvrx.withState
    +import im.vector.matrix.android.internal.di.MoshiProvider
    +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
    +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
    +import im.vector.riotx.R
    +import im.vector.riotx.core.extensions.cleanup
    +import im.vector.riotx.core.extensions.configureWith
    +import im.vector.riotx.core.platform.VectorBaseActivity
    +import im.vector.riotx.core.platform.VectorBaseFragment
    +import im.vector.riotx.core.resources.ColorProvider
    +import im.vector.riotx.core.utils.jsonViewerStyler
    +import kotlinx.android.synthetic.main.fragment_generic_recycler.*
    +import org.billcarsonfr.jsonviewer.JSonViewerDialog
    +import javax.inject.Inject
    +
    +class AccountDataFragment @Inject constructor(
    +        val viewModelFactory: AccountDataViewModel.Factory,
    +        private val epoxyController: AccountDataEpoxyController,
    +        private val colorProvider: ColorProvider
    +) : VectorBaseFragment(), AccountDataEpoxyController.InteractionListener {
    +
    +    override fun getLayoutResId() = R.layout.fragment_generic_recycler
    +
    +    private val viewModel: AccountDataViewModel by fragmentViewModel(AccountDataViewModel::class)
    +
    +    override fun onResume() {
    +        super.onResume()
    +        (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_account_data)
    +    }
    +
    +    override fun invalidate() = withState(viewModel) { state ->
    +        epoxyController.setData(state)
    +    }
    +
    +    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    +        super.onViewCreated(view, savedInstanceState)
    +        recyclerView.configureWith(epoxyController, showDivider = true)
    +        epoxyController.interactionListener = this
    +    }
    +
    +    override fun onDestroyView() {
    +        super.onDestroyView()
    +        recyclerView.cleanup()
    +        epoxyController.interactionListener = null
    +    }
    +
    +    override fun didTap(data: UserAccountData) {
    +        val fb = data as? UserAccountDataEvent ?: return
    +        val jsonString = MoshiProvider.providesMoshi()
    +                .adapter(UserAccountDataEvent::class.java)
    +                .toJson(fb)
    +        JSonViewerDialog.newInstance(
    +                jsonString,
    +                -1, // open All
    +                jsonViewerStyler(colorProvider)
    +        ).show(childFragmentManager, "JSON_VIEWER")
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devtools/AccountDataViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/devtools/AccountDataViewModel.kt
    new file mode 100644
    index 0000000000..32ce17c660
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/devtools/AccountDataViewModel.kt
    @@ -0,0 +1,64 @@
    +/*
    + * Copyright (c) 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.riotx.features.settings.devtools
    +
    +import com.airbnb.mvrx.Async
    +import com.airbnb.mvrx.FragmentViewModelContext
    +import com.airbnb.mvrx.MvRxState
    +import com.airbnb.mvrx.MvRxViewModelFactory
    +import com.airbnb.mvrx.Uninitialized
    +import com.airbnb.mvrx.ViewModelContext
    +import com.squareup.inject.assisted.Assisted
    +import com.squareup.inject.assisted.AssistedInject
    +import im.vector.matrix.android.api.session.Session
    +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
    +import im.vector.matrix.rx.rx
    +import im.vector.riotx.core.platform.EmptyAction
    +import im.vector.riotx.core.platform.EmptyViewEvents
    +import im.vector.riotx.core.platform.VectorViewModel
    +
    +data class AccountDataViewState(
    +        val accountData: Async> = Uninitialized
    +) : MvRxState
    +
    +class AccountDataViewModel @AssistedInject constructor(@Assisted initialState: AccountDataViewState,
    +                                                       private val session: Session)
    +    : VectorViewModel(initialState) {
    +
    +    init {
    +        session.rx().liveAccountData(emptySet())
    +                .execute {
    +                    copy(accountData = it)
    +                }
    +    }
    +
    +    override fun handle(action: EmptyAction) {}
    +
    +    @AssistedInject.Factory
    +    interface Factory {
    +        fun create(initialState: AccountDataViewState): AccountDataViewModel
    +    }
    +
    +    companion object : MvRxViewModelFactory {
    +
    +        @JvmStatic
    +        override fun create(viewModelContext: ViewModelContext, state: AccountDataViewState): AccountDataViewModel? {
    +            val fragment: AccountDataFragment = (viewModelContext as FragmentViewModelContext).fragment()
    +            return fragment.viewModelFactory.create(state)
    +        }
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareAction.kt b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareAction.kt
    new file mode 100644
    index 0000000000..0379c4980f
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareAction.kt
    @@ -0,0 +1,28 @@
    +/*
    + * Copyright (c) 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.riotx.features.share
    +
    +import im.vector.matrix.android.api.session.room.model.RoomSummary
    +import im.vector.riotx.core.platform.VectorViewModelAction
    +
    +sealed class IncomingShareAction : VectorViewModelAction {
    +    data class SelectRoom(val roomSummary: RoomSummary, val enableMultiSelect: Boolean) : IncomingShareAction()
    +    object ShareToSelectedRooms : IncomingShareAction()
    +    data class ShareMedia(val keepOriginalSize: Boolean) : IncomingShareAction()
    +    data class FilterWith(val filter: String) : IncomingShareAction()
    +    data class UpdateSharedData(val sharedData: SharedData) : IncomingShareAction()
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt
    index 3669a51937..5948ea089d 100644
    --- a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt
    @@ -9,126 +9,30 @@
      *
      * 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.
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.V
      * See the License for the specific language governing permissions and
      * limitations under the License.
      */
     
     package im.vector.riotx.features.share
     
    -import android.content.ClipDescription
    -import android.content.Intent
    -import android.os.Bundle
    -import android.widget.Toast
    -import androidx.appcompat.widget.SearchView
    -import com.airbnb.mvrx.viewModel
    -import com.kbeanie.multipicker.utils.IntentUtils
    -import im.vector.matrix.android.api.session.content.ContentAttachmentData
    +import androidx.appcompat.widget.Toolbar
     import im.vector.riotx.R
    -import im.vector.riotx.core.di.ActiveSessionHolder
    -import im.vector.riotx.core.di.ScreenComponent
    -import im.vector.riotx.core.extensions.replaceFragment
    +import im.vector.riotx.core.extensions.addFragment
    +import im.vector.riotx.core.platform.ToolbarConfigurable
     import im.vector.riotx.core.platform.VectorBaseActivity
    -import im.vector.riotx.features.attachments.AttachmentsHelper
    -import im.vector.riotx.features.home.LoadingFragment
    -import im.vector.riotx.features.home.RoomListDisplayMode
    -import im.vector.riotx.features.home.room.list.RoomListFragment
    -import im.vector.riotx.features.home.room.list.RoomListParams
    -import im.vector.riotx.features.login.LoginActivity
    -import kotlinx.android.synthetic.main.activity_incoming_share.*
    -import javax.inject.Inject
     
    -class IncomingShareActivity :
    -        VectorBaseActivity(), AttachmentsHelper.Callback {
    +class IncomingShareActivity : VectorBaseActivity(), ToolbarConfigurable {
     
    -    @Inject lateinit var sessionHolder: ActiveSessionHolder
    -    @Inject lateinit var incomingShareViewModelFactory: IncomingShareViewModel.Factory
    -    private lateinit var attachmentsHelper: AttachmentsHelper
    -    // Do not remove, even if not used, it instantiates the view model
    -    @Suppress("unused")
    -    private val viewModel: IncomingShareViewModel by viewModel()
    -    private val roomListFragment: RoomListFragment?
    -        get() {
    -            return supportFragmentManager.findFragmentById(R.id.shareRoomListFragmentContainer) as? RoomListFragment
    -        }
    +    override fun getLayoutRes() = R.layout.activity_simple
     
    -    override fun getLayoutRes() = R.layout.activity_incoming_share
    -
    -    override fun injectWith(injector: ScreenComponent) {
    -        injector.inject(this)
    -    }
    -
    -    override fun onCreate(savedInstanceState: Bundle?) {
    -        super.onCreate(savedInstanceState)
    -        // If we are not logged in, stop the sharing process and open login screen.
    -        // In the future, we might want to relaunch the sharing process after login.
    -        if (!sessionHolder.hasActiveSession()) {
    -            startLoginActivity()
    -            return
    -        }
    -        configureToolbar(incomingShareToolbar)
    +    override fun initUiAndData() {
             if (isFirstCreation()) {
    -            replaceFragment(R.id.shareRoomListFragmentContainer, LoadingFragment::class.java)
    +            addFragment(R.id.simpleFragmentContainer, IncomingShareFragment::class.java)
             }
    -        attachmentsHelper = AttachmentsHelper.create(this, this).register()
    -        if (intent?.action == Intent.ACTION_SEND || intent?.action == Intent.ACTION_SEND_MULTIPLE) {
    -            var isShareManaged = attachmentsHelper.handleShareIntent(
    -                    IntentUtils.getPickerIntentForSharing(intent)
    -            )
    -            if (!isShareManaged) {
    -                isShareManaged = handleTextShare(intent)
    -            }
    -            if (!isShareManaged) {
    -                cannotManageShare()
    -            }
    -        } else {
    -            cannotManageShare()
    -        }
    -
    -        incomingShareSearchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
    -            override fun onQueryTextSubmit(query: String): Boolean {
    -                return true
    -            }
    -
    -            override fun onQueryTextChange(newText: String): Boolean {
    -                roomListFragment?.filterRoomsWith(newText)
    -                return true
    -            }
    -        })
         }
     
    -    override fun onContentAttachmentsReady(attachments: List) {
    -        val roomListParams = RoomListParams(RoomListDisplayMode.SHARE, sharedData = SharedData.Attachments(attachments))
    -        replaceFragment(R.id.shareRoomListFragmentContainer, RoomListFragment::class.java, roomListParams)
    -    }
    -
    -    override fun onAttachmentsProcessFailed() {
    -        cannotManageShare()
    -    }
    -
    -    private fun cannotManageShare() {
    -        Toast.makeText(this, R.string.error_handling_incoming_share, Toast.LENGTH_LONG).show()
    -        finish()
    -    }
    -
    -    private fun handleTextShare(intent: Intent): Boolean {
    -        if (intent.type == ClipDescription.MIMETYPE_TEXT_PLAIN) {
    -            val sharedText = intent.getCharSequenceExtra(Intent.EXTRA_TEXT)?.toString()
    -            return if (sharedText.isNullOrEmpty()) {
    -                false
    -            } else {
    -                val roomListParams = RoomListParams(RoomListDisplayMode.SHARE, sharedData = SharedData.Text(sharedText))
    -                replaceFragment(R.id.shareRoomListFragmentContainer, RoomListFragment::class.java, roomListParams)
    -                true
    -            }
    -        }
    -        return false
    -    }
    -
    -    private fun startLoginActivity() {
    -        val intent = LoginActivity.newIntent(this, null)
    -        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
    -        startActivity(intent)
    -        finish()
    +    override fun configure(toolbar: Toolbar) {
    +        configureToolbar(toolbar, displayBack = false)
         }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareController.kt b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareController.kt
    new file mode 100644
    index 0000000000..4e7f585458
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareController.kt
    @@ -0,0 +1,60 @@
    +/*
    + * Copyright (c) 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.riotx.features.share
    +
    +import com.airbnb.epoxy.TypedEpoxyController
    +import com.airbnb.mvrx.Incomplete
    +import im.vector.matrix.android.api.session.room.model.RoomSummary
    +import im.vector.riotx.R
    +import im.vector.riotx.core.epoxy.loadingItem
    +import im.vector.riotx.core.epoxy.noResultItem
    +import im.vector.riotx.core.resources.StringProvider
    +import im.vector.riotx.features.home.room.list.RoomSummaryItemFactory
    +import javax.inject.Inject
    +
    +class IncomingShareController @Inject constructor(private val roomSummaryItemFactory: RoomSummaryItemFactory,
    +                                                  private val stringProvider: StringProvider) : TypedEpoxyController() {
    +
    +    interface Callback {
    +        fun onRoomClicked(roomSummary: RoomSummary)
    +        fun onRoomLongClicked(roomSummary: RoomSummary): Boolean
    +    }
    +
    +    var callback: Callback? = null
    +
    +    override fun buildModels(data: IncomingShareViewState) {
    +        if (data.sharedData == null || data.filteredRoomSummaries is Incomplete) {
    +            loadingItem {
    +                id("loading")
    +            }
    +            return
    +        }
    +        val roomSummaries = data.filteredRoomSummaries()
    +        if (roomSummaries.isNullOrEmpty()) {
    +            noResultItem {
    +                id("no_result")
    +                text(stringProvider.getString(R.string.no_result_placeholder))
    +            }
    +        } else {
    +            roomSummaries.forEach { roomSummary ->
    +                roomSummaryItemFactory
    +                        .createRoomItem(roomSummary, data.selectedRoomIds, callback?.let { it::onRoomClicked }, callback?.let { it::onRoomLongClicked })
    +                        .addTo(this)
    +            }
    +        }
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareFragment.kt b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareFragment.kt
    new file mode 100644
    index 0000000000..eb0f5128ba
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareFragment.kt
    @@ -0,0 +1,241 @@
    +/*
    + * Copyright (c) 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.riotx.features.share
    +
    +import android.app.Activity
    +import android.content.ClipDescription
    +import android.content.Intent
    +import android.os.Bundle
    +import android.view.View
    +import android.widget.Toast
    +import androidx.annotation.StringRes
    +import androidx.appcompat.app.AlertDialog
    +import androidx.appcompat.widget.SearchView
    +import androidx.core.view.isVisible
    +import com.airbnb.mvrx.fragmentViewModel
    +import com.airbnb.mvrx.withState
    +import com.kbeanie.multipicker.utils.IntentUtils
    +import im.vector.matrix.android.api.session.content.ContentAttachmentData
    +import im.vector.matrix.android.api.session.room.model.RoomSummary
    +import im.vector.riotx.R
    +import im.vector.riotx.core.di.ActiveSessionHolder
    +import im.vector.riotx.core.extensions.cleanup
    +import im.vector.riotx.core.extensions.configureWith
    +import im.vector.riotx.core.extensions.exhaustive
    +import im.vector.riotx.core.platform.VectorBaseFragment
    +import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
    +import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_PICK_ATTACHMENT
    +import im.vector.riotx.core.utils.allGranted
    +import im.vector.riotx.core.utils.checkPermissions
    +import im.vector.riotx.features.attachments.AttachmentsHelper
    +import im.vector.riotx.features.attachments.preview.AttachmentsPreviewActivity
    +import im.vector.riotx.features.attachments.preview.AttachmentsPreviewArgs
    +import im.vector.riotx.features.login.LoginActivity
    +import kotlinx.android.synthetic.main.fragment_incoming_share.*
    +import javax.inject.Inject
    +
    +/**
    + * Display the list of rooms
    + * The user can select multiple rooms to send the data to
    + */
    +class IncomingShareFragment @Inject constructor(
    +        val incomingShareViewModelFactory: IncomingShareViewModel.Factory,
    +        private val incomingShareController: IncomingShareController,
    +        private val sessionHolder: ActiveSessionHolder
    +) : VectorBaseFragment(), AttachmentsHelper.Callback, IncomingShareController.Callback {
    +
    +    private lateinit var attachmentsHelper: AttachmentsHelper
    +    private val viewModel: IncomingShareViewModel by fragmentViewModel()
    +
    +    override fun getLayoutResId() = R.layout.fragment_incoming_share
    +
    +    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    +        // If we are not logged in, stop the sharing process and open login screen.
    +        // In the future, we might want to relaunch the sharing process after login.
    +        if (!sessionHolder.hasActiveSession()) {
    +            startLoginActivity()
    +            return
    +        }
    +        super.onViewCreated(view, savedInstanceState)
    +        setupRecyclerView()
    +        setupToolbar(incomingShareToolbar)
    +        attachmentsHelper = AttachmentsHelper.create(this, this).register()
    +
    +        val intent = vectorBaseActivity.intent
    +        val isShareManaged = when (intent?.action) {
    +            Intent.ACTION_SEND          -> {
    +                var isShareManaged = attachmentsHelper.handleShareIntent(IntentUtils.getPickerIntentForSharing(intent))
    +                if (!isShareManaged) {
    +                    isShareManaged = handleTextShare(intent)
    +                }
    +                isShareManaged
    +            }
    +            Intent.ACTION_SEND_MULTIPLE -> attachmentsHelper.handleShareIntent(intent)
    +            else                        -> false
    +        }
    +
    +        if (!isShareManaged) {
    +            cannotManageShare(R.string.error_handling_incoming_share)
    +        }
    +
    +        incomingShareSearchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
    +            override fun onQueryTextSubmit(query: String): Boolean {
    +                return true
    +            }
    +
    +            override fun onQueryTextChange(newText: String): Boolean {
    +                viewModel.handle(IncomingShareAction.FilterWith(newText))
    +                return true
    +            }
    +        })
    +        sendShareButton.setOnClickListener { _ ->
    +            handleSendShare()
    +        }
    +        viewModel.observeViewEvents {
    +            when (it) {
    +                is IncomingShareViewEvents.ShareToRoom            -> handleShareToRoom(it)
    +                is IncomingShareViewEvents.EditMediaBeforeSending -> handleEditMediaBeforeSending(it)
    +                is IncomingShareViewEvents.MultipleRoomsShareDone -> handleMultipleRoomsShareDone(it)
    +            }.exhaustive
    +        }
    +    }
    +
    +    private fun handleMultipleRoomsShareDone(viewEvent: IncomingShareViewEvents.MultipleRoomsShareDone) {
    +        requireActivity().let {
    +            navigator.openRoom(it, viewEvent.roomId)
    +            it.finish()
    +        }
    +    }
    +
    +    private fun handleEditMediaBeforeSending(event: IncomingShareViewEvents.EditMediaBeforeSending) {
    +        val intent = AttachmentsPreviewActivity.newIntent(requireContext(), AttachmentsPreviewArgs(event.contentAttachmentData))
    +        startActivityForResult(intent, AttachmentsPreviewActivity.REQUEST_CODE)
    +    }
    +
    +    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    +        val hasBeenHandled = attachmentsHelper.onActivityResult(requestCode, resultCode, data)
    +        if (!hasBeenHandled && resultCode == Activity.RESULT_OK && data != null) {
    +            when (requestCode) {
    +                AttachmentsPreviewActivity.REQUEST_CODE -> {
    +                    val sendData = AttachmentsPreviewActivity.getOutput(data)
    +                    val keepOriginalSize = AttachmentsPreviewActivity.getKeepOriginalSize(data)
    +                    viewModel.handle(IncomingShareAction.UpdateSharedData(SharedData.Attachments(sendData)))
    +                    viewModel.handle(IncomingShareAction.ShareMedia(keepOriginalSize))
    +                }
    +            }
    +        }
    +    }
    +
    +    override fun onResume() {
    +        super.onResume()
    +
    +        // We need the read file permission
    +        checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_PICK_ATTACHMENT)
    +    }
    +
    +    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
    +        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    +
    +        if (requestCode == PERMISSION_REQUEST_CODE_PICK_ATTACHMENT && !allGranted(grantResults)) {
    +            // Permission is mandatory
    +            cannotManageShare(R.string.missing_permissions_error)
    +        }
    +    }
    +
    +    private fun handleShareToRoom(event: IncomingShareViewEvents.ShareToRoom) {
    +        if (event.showAlert) {
    +            showConfirmationDialog(event.roomSummary, event.sharedData)
    +        } else {
    +            navigator.openRoomForSharingAndFinish(requireActivity(), event.roomSummary.roomId, event.sharedData)
    +        }
    +    }
    +
    +    private fun handleSendShare() {
    +        viewModel.handle(IncomingShareAction.ShareToSelectedRooms)
    +    }
    +
    +    override fun onDestroyView() {
    +        incomingShareController.callback = null
    +        incomingShareRoomList.cleanup()
    +        super.onDestroyView()
    +    }
    +
    +    private fun setupRecyclerView() {
    +        incomingShareRoomList.configureWith(incomingShareController, hasFixedSize = true)
    +        incomingShareController.callback = this
    +    }
    +
    +    override fun onContentAttachmentsReady(attachments: List) {
    +        val sharedData = SharedData.Attachments(attachments)
    +        viewModel.handle(IncomingShareAction.UpdateSharedData(sharedData))
    +    }
    +
    +    override fun onAttachmentsProcessFailed() {
    +        cannotManageShare(R.string.error_handling_incoming_share)
    +    }
    +
    +    private fun cannotManageShare(@StringRes messageResId: Int) {
    +        Toast.makeText(requireContext(), messageResId, Toast.LENGTH_LONG).show()
    +        requireActivity().finish()
    +    }
    +
    +    private fun handleTextShare(intent: Intent): Boolean {
    +        if (intent.type == ClipDescription.MIMETYPE_TEXT_PLAIN) {
    +            val sharedText = intent.getCharSequenceExtra(Intent.EXTRA_TEXT)?.toString()
    +            return if (sharedText.isNullOrEmpty()) {
    +                false
    +            } else {
    +                val sharedData = SharedData.Text(sharedText)
    +                viewModel.handle(IncomingShareAction.UpdateSharedData(sharedData))
    +                true
    +            }
    +        }
    +        return false
    +    }
    +
    +    private fun showConfirmationDialog(roomSummary: RoomSummary, sharedData: SharedData) {
    +        AlertDialog.Builder(requireActivity())
    +                .setTitle(R.string.send_attachment)
    +                .setMessage(getString(R.string.share_confirm_room, roomSummary.displayName))
    +                .setPositiveButton(R.string.send) { _, _ ->
    +                    navigator.openRoomForSharingAndFinish(requireActivity(), roomSummary.roomId, sharedData)
    +                }
    +                .setNegativeButton(R.string.cancel, null)
    +                .show()
    +    }
    +
    +    private fun startLoginActivity() {
    +        val intent = LoginActivity.newIntent(requireActivity(), null)
    +        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
    +        startActivity(intent)
    +        requireActivity().finish()
    +    }
    +
    +    override fun invalidate() = withState(viewModel) {
    +        sendShareButton.isVisible = it.isInMultiSelectionMode
    +        incomingShareController.setData(it)
    +    }
    +
    +    override fun onRoomClicked(roomSummary: RoomSummary) {
    +        viewModel.handle(IncomingShareAction.SelectRoom(roomSummary, false))
    +    }
    +
    +    override fun onRoomLongClicked(roomSummary: RoomSummary): Boolean {
    +        viewModel.handle(IncomingShareAction.SelectRoom(roomSummary, true))
    +        return true
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewEvents.kt
    new file mode 100644
    index 0000000000..2940a1e750
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewEvents.kt
    @@ -0,0 +1,30 @@
    +/*
    + * Copyright (c) 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.riotx.features.share
    +
    +import im.vector.matrix.android.api.session.content.ContentAttachmentData
    +import im.vector.matrix.android.api.session.room.model.RoomSummary
    +import im.vector.riotx.core.platform.VectorViewEvents
    +
    +sealed class IncomingShareViewEvents : VectorViewEvents {
    +    data class ShareToRoom(val roomSummary: RoomSummary,
    +                           val sharedData: SharedData,
    +                           val showAlert: Boolean) : IncomingShareViewEvents()
    +
    +    data class EditMediaBeforeSending(val contentAttachmentData: List) : IncomingShareViewEvents()
    +    data class MultipleRoomsShareDone(val roomId: String) : IncomingShareViewEvents()
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewModel.kt b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewModel.kt
    index 8b791fbf1b..c135460ee4 100644
    --- a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewModel.kt
    @@ -16,71 +16,190 @@
     
     package im.vector.riotx.features.share
     
    -import com.airbnb.mvrx.ActivityViewModelContext
    -import com.airbnb.mvrx.MvRxState
    +import com.airbnb.mvrx.FragmentViewModelContext
     import com.airbnb.mvrx.MvRxViewModelFactory
     import com.airbnb.mvrx.ViewModelContext
    +import com.jakewharton.rxrelay2.BehaviorRelay
     import com.squareup.inject.assisted.Assisted
     import com.squareup.inject.assisted.AssistedInject
    +import im.vector.matrix.android.api.query.QueryStringValue
    +import im.vector.matrix.android.api.session.Session
    +import im.vector.matrix.android.api.session.content.ContentAttachmentData
    +import im.vector.matrix.android.api.session.room.model.Membership
     import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
     import im.vector.matrix.rx.rx
    -import im.vector.riotx.ActiveSessionDataSource
    -import im.vector.riotx.core.platform.EmptyAction
    -import im.vector.riotx.core.platform.EmptyViewEvents
    +import im.vector.riotx.core.extensions.exhaustive
    +import im.vector.riotx.core.extensions.toggle
     import im.vector.riotx.core.platform.VectorViewModel
    +import im.vector.riotx.features.attachments.isPreviewable
    +import im.vector.riotx.features.attachments.toGroupedContentAttachmentData
     import im.vector.riotx.features.home.room.list.BreadcrumbsRoomComparator
    -import io.reactivex.Observable
    -import io.reactivex.android.schedulers.AndroidSchedulers
     import java.util.concurrent.TimeUnit
     
    -data class IncomingShareState(private val dummy: Boolean = false) : MvRxState
    -
    -/**
    - * View model used to observe the room list and post update to the ShareRoomListObservableStore
    - */
    -class IncomingShareViewModel @AssistedInject constructor(@Assisted initialState: IncomingShareState,
    -                                                         private val sessionObservableStore: ActiveSessionDataSource,
    -                                                         private val shareRoomListObservableStore: ShareRoomListDataSource,
    -                                                         private val breadcrumbsRoomComparator: BreadcrumbsRoomComparator)
    -    : VectorViewModel(initialState) {
    +class IncomingShareViewModel @AssistedInject constructor(
    +        @Assisted initialState: IncomingShareViewState,
    +        private val session: Session,
    +        private val breadcrumbsRoomComparator: BreadcrumbsRoomComparator)
    +    : VectorViewModel(initialState) {
     
         @AssistedInject.Factory
         interface Factory {
    -        fun create(initialState: IncomingShareState): IncomingShareViewModel
    +        fun create(initialState: IncomingShareViewState): IncomingShareViewModel
         }
     
    -    companion object : MvRxViewModelFactory {
    +    companion object : MvRxViewModelFactory {
     
             @JvmStatic
    -        override fun create(viewModelContext: ViewModelContext, state: IncomingShareState): IncomingShareViewModel? {
    -            val activity: IncomingShareActivity = (viewModelContext as ActivityViewModelContext).activity()
    -            return activity.incomingShareViewModelFactory.create(state)
    +        override fun create(viewModelContext: ViewModelContext, state: IncomingShareViewState): IncomingShareViewModel? {
    +            val fragment: IncomingShareFragment = (viewModelContext as FragmentViewModelContext).fragment()
    +            return fragment.incomingShareViewModelFactory.create(state)
             }
         }
     
    +    private val filterStream: BehaviorRelay = BehaviorRelay.createDefault("")
    +
         init {
             observeRoomSummaries()
         }
     
         private fun observeRoomSummaries() {
    -        val queryParams = roomSummaryQueryParams()
    -        sessionObservableStore.observe()
    -                .observeOn(AndroidSchedulers.mainThread())
    -                .switchMap {
    -                    it.orNull()?.rx()?.liveRoomSummaries(queryParams)
    -                            ?: Observable.just(emptyList())
    +        val queryParams = roomSummaryQueryParams {
    +            memberships = listOf(Membership.JOIN)
    +        }
    +        session
    +                .rx().liveRoomSummaries(queryParams)
    +                .execute {
    +                    copy(roomSummaries = it)
    +                }
    +
    +        filterStream
    +                .switchMap { filter ->
    +                    val displayNameQuery = if (filter.isEmpty()) {
    +                        QueryStringValue.NoCondition
    +                    } else {
    +                        QueryStringValue.Contains(filter, QueryStringValue.Case.INSENSITIVE)
    +                    }
    +                    val filterQueryParams = roomSummaryQueryParams {
    +                        displayName = displayNameQuery
    +                        memberships = listOf(Membership.JOIN)
    +                    }
    +                    session.rx().liveRoomSummaries(filterQueryParams)
                     }
                     .throttleLast(300, TimeUnit.MILLISECONDS)
    -                .map {
    -                    it.sortedWith(breadcrumbsRoomComparator)
    +                .map { it.sortedWith(breadcrumbsRoomComparator) }
    +                .execute {
    +                    copy(filteredRoomSummaries = it)
                     }
    -                .subscribe {
    -                    shareRoomListObservableStore.post(it)
    -                }
    -                .disposeOnClear()
         }
     
    -    override fun handle(action: EmptyAction) {
    -        // No op
    +    override fun handle(action: IncomingShareAction) {
    +        when (action) {
    +            is IncomingShareAction.SelectRoom           -> handleSelectRoom(action)
    +            is IncomingShareAction.ShareToSelectedRooms -> handleShareToSelectedRooms()
    +            is IncomingShareAction.ShareMedia           -> handleShareMediaToSelectedRooms(action)
    +            is IncomingShareAction.FilterWith           -> handleFilter(action)
    +            is IncomingShareAction.UpdateSharedData     -> handleUpdateSharedData(action)
    +        }.exhaustive
    +    }
    +
    +    private fun handleUpdateSharedData(action: IncomingShareAction.UpdateSharedData) {
    +        setState { copy(sharedData = action.sharedData) }
    +    }
    +
    +    private fun handleFilter(action: IncomingShareAction.FilterWith) {
    +        filterStream.accept(action.filter)
    +    }
    +
    +    private fun handleShareToSelectedRooms() = withState { state ->
    +        val sharedData = state.sharedData ?: return@withState
    +        if (state.selectedRoomIds.size == 1) {
    +            // In this case the edition of the media will be handled by the RoomDetailFragment
    +            val selectedRoomId = state.selectedRoomIds.first()
    +            val selectedRoom = state.roomSummaries()?.find { it.roomId == selectedRoomId } ?: return@withState
    +            _viewEvents.post(IncomingShareViewEvents.ShareToRoom(selectedRoom, sharedData, showAlert = false))
    +        } else {
    +            when (sharedData) {
    +                is SharedData.Text        -> {
    +                    state.selectedRoomIds.forEach { roomId ->
    +                        val room = session.getRoom(roomId)
    +                        room?.sendTextMessage(sharedData.text)
    +                    }
    +                    // This is it, pass the first roomId to let the screen open it
    +                    _viewEvents.post(IncomingShareViewEvents.MultipleRoomsShareDone(state.selectedRoomIds.first()))
    +                }
    +                is SharedData.Attachments -> {
    +                    shareAttachments(sharedData.attachmentData, state.selectedRoomIds, proposeMediaEdition = true, compressMediaBeforeSending = false)
    +                }
    +            }.exhaustive
    +        }
    +    }
    +
    +    private fun handleShareMediaToSelectedRooms(action: IncomingShareAction.ShareMedia) = withState { state ->
    +        (state.sharedData as? SharedData.Attachments)?.let {
    +            shareAttachments(it.attachmentData, state.selectedRoomIds, proposeMediaEdition = false, compressMediaBeforeSending = !action.keepOriginalSize)
    +        }
    +    }
    +
    +    private fun shareAttachments(attachmentData: List,
    +                                 selectedRoomIds: Set,
    +                                 proposeMediaEdition: Boolean,
    +                                 compressMediaBeforeSending: Boolean) {
    +        if (proposeMediaEdition) {
    +            val grouped = attachmentData.toGroupedContentAttachmentData()
    +            if (grouped.notPreviewables.isNotEmpty()) {
    +                // Send the not previewable attachments right now (?)
    +                // Pick the first room to send the media
    +                selectedRoomIds.firstOrNull()
    +                        ?.let { roomId -> session.getRoom(roomId) }
    +                        ?.sendMedias(grouped.notPreviewables, compressMediaBeforeSending, selectedRoomIds)
    +
    +                // Ensure they will not be sent twice
    +                setState {
    +                    copy(
    +                            sharedData = SharedData.Attachments(grouped.previewables)
    +                    )
    +                }
    +            }
    +            if (grouped.previewables.isNotEmpty()) {
    +                // In case of multiple share of media, edit them first
    +                _viewEvents.post(IncomingShareViewEvents.EditMediaBeforeSending(grouped.previewables))
    +            } else {
    +                // This is it, pass the first roomId to let the screen open it
    +                _viewEvents.post(IncomingShareViewEvents.MultipleRoomsShareDone(selectedRoomIds.first()))
    +            }
    +        } else {
    +            // Pick the first room to send the media
    +            selectedRoomIds.firstOrNull()
    +                    ?.let { roomId -> session.getRoom(roomId) }
    +                    ?.sendMedias(attachmentData, compressMediaBeforeSending, selectedRoomIds)
    +            // This is it, pass the first roomId to let the screen open it
    +            _viewEvents.post(IncomingShareViewEvents.MultipleRoomsShareDone(selectedRoomIds.first()))
    +        }
    +    }
    +
    +    private fun handleSelectRoom(action: IncomingShareAction.SelectRoom) = withState { state ->
    +        if (state.isInMultiSelectionMode) {
    +            // One room is clicked (or long clicked) while in multi selection mode -> toggle this room
    +            val selectedRooms = state.selectedRoomIds
    +            val newSelectedRooms = selectedRooms.toggle(action.roomSummary.roomId)
    +            setState { copy(isInMultiSelectionMode = newSelectedRooms.isNotEmpty(), selectedRoomIds = newSelectedRooms) }
    +        } else if (action.enableMultiSelect) {
    +            // One room is long clicked, not in multi selection mode -> enable multi selection mode
    +            setState { copy(isInMultiSelectionMode = true, selectedRoomIds = setOf(action.roomSummary.roomId)) }
    +        } else {
    +            // One room is clicked, not in multi selection mode -> direct share
    +            val sharedData = state.sharedData ?: return@withState
    +            val doNotShowAlert = when (sharedData) {
    +                is SharedData.Attachments -> {
    +                    // Do not show alert if the shared data contains only previewable attachments, because the user will get another chance to cancel the share
    +                    sharedData.attachmentData.all { it.isPreviewable() }
    +                }
    +                is SharedData.Text        -> {
    +                    // Do not show alert when sharing text to one room, because it will just fill the composer
    +                    true
    +                }
    +            }
    +            _viewEvents.post(IncomingShareViewEvents.ShareToRoom(action.roomSummary, sharedData, !doNotShowAlert))
    +        }
         }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewState.kt b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewState.kt
    new file mode 100644
    index 0000000000..71b416ef7e
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewState.kt
    @@ -0,0 +1,30 @@
    +/*
    + * Copyright (c) 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.riotx.features.share
    +
    +import com.airbnb.mvrx.Async
    +import com.airbnb.mvrx.MvRxState
    +import com.airbnb.mvrx.Uninitialized
    +import im.vector.matrix.android.api.session.room.model.RoomSummary
    +
    +data class IncomingShareViewState(
    +        val sharedData: SharedData? = null,
    +        val roomSummaries: Async> = Uninitialized,
    +        val filteredRoomSummaries: Async> = Uninitialized,
    +        val selectedRoomIds: Set = emptySet(),
    +        val isInMultiSelectionMode: Boolean = false
    +) : MvRxState
    diff --git a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutController.kt b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutController.kt
    index 4f686a4a76..4711726a7b 100644
    --- a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutController.kt
    @@ -26,7 +26,14 @@ import im.vector.riotx.core.error.ErrorFormatter
     import im.vector.riotx.core.extensions.toReducedUrl
     import im.vector.riotx.core.resources.StringProvider
     import im.vector.riotx.features.login.LoginMode
    -import im.vector.riotx.features.signout.soft.epoxy.*
    +import im.vector.riotx.features.signout.soft.epoxy.loginCenterButtonItem
    +import im.vector.riotx.features.signout.soft.epoxy.loginErrorWithRetryItem
    +import im.vector.riotx.features.signout.soft.epoxy.loginHeaderItem
    +import im.vector.riotx.features.signout.soft.epoxy.loginPasswordFormItem
    +import im.vector.riotx.features.signout.soft.epoxy.loginRedButtonItem
    +import im.vector.riotx.features.signout.soft.epoxy.loginTextItem
    +import im.vector.riotx.features.signout.soft.epoxy.loginTitleItem
    +import im.vector.riotx.features.signout.soft.epoxy.loginTitleSmallItem
     import javax.inject.Inject
     
     class SoftLogoutController @Inject constructor(
    diff --git a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewState.kt b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewState.kt
    index 01776d1982..775558499b 100644
    --- a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewState.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewState.kt
    @@ -16,7 +16,11 @@
     
     package im.vector.riotx.features.signout.soft
     
    -import com.airbnb.mvrx.*
    +import com.airbnb.mvrx.Async
    +import com.airbnb.mvrx.Loading
    +import com.airbnb.mvrx.MvRxState
    +import com.airbnb.mvrx.Success
    +import com.airbnb.mvrx.Uninitialized
     import im.vector.riotx.features.login.LoginMode
     
     data class SoftLogoutViewState(
    diff --git a/vector/src/main/java/im/vector/riotx/features/themes/ActivityOtherThemes.kt b/vector/src/main/java/im/vector/riotx/features/themes/ActivityOtherThemes.kt
    index 411069e9ca..b37c1a4818 100644
    --- a/vector/src/main/java/im/vector/riotx/features/themes/ActivityOtherThemes.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/themes/ActivityOtherThemes.kt
    @@ -32,4 +32,10 @@ sealed class ActivityOtherThemes(@StyleRes val dark: Int,
                 R.style.AppTheme_Black,
                 R.style.AppTheme_Status
         )
    +
    +    object AttachmentsPreview : ActivityOtherThemes(
    +            R.style.AppTheme_AttachmentsPreview,
    +            R.style.AppTheme_AttachmentsPreview,
    +            R.style.AppTheme_AttachmentsPreview
    +    )
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutViewModel.kt b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutViewModel.kt
    index 2526e8cbe3..2f26fdf377 100644
    --- a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutViewModel.kt
    @@ -30,36 +30,36 @@ class SignOutViewModel @Inject constructor(private val session: Session) : ViewM
         var keysBackupState = MutableLiveData()
     
         init {
    -        session.getKeysBackupService().addListener(this)
    +        session.cryptoService().keysBackupService().addListener(this)
     
    -        keysBackupState.value = session.getKeysBackupService().state
    +        keysBackupState.value = session.cryptoService().keysBackupService().state
         }
     
         /**
          * Safe way to get the current KeysBackup version
          */
         fun getCurrentBackupVersion(): String {
    -        return session.getKeysBackupService().currentBackupVersion ?: ""
    +        return session.cryptoService().keysBackupService().currentBackupVersion ?: ""
         }
     
         /**
          * Safe way to get the number of keys to backup
          */
         fun getNumberOfKeysToBackup(): Int {
    -        return session.inboundGroupSessionsCount(false)
    +        return session.cryptoService().inboundGroupSessionsCount(false)
         }
     
         /**
          * Safe way to tell if there are more keys on the server
          */
         fun canRestoreKeys(): Boolean {
    -        return session.getKeysBackupService().canRestoreKeys()
    +        return session.cryptoService().keysBackupService().canRestoreKeys()
         }
     
         override fun onCleared() {
             super.onCleared()
     
    -        session.getKeysBackupService().removeListener(this)
    +        session.cryptoService().keysBackupService().removeListener(this)
         }
     
         override fun onStateChange(newState: KeysBackupState) {
    @@ -68,7 +68,7 @@ class SignOutViewModel @Inject constructor(private val session: Session) : ViewM
     
         fun refreshRemoteStateIfNeeded() {
             if (keysBackupState.value == KeysBackupState.Disabled) {
    -            session.getKeysBackupService().checkAndStartKeysBackup()
    +            session.cryptoService().keysBackupService().checkAndStartKeysBackup()
             }
         }
     }
    diff --git a/vector/src/main/res/color/checked_accent_color_selector.xml b/vector/src/main/res/color/checked_accent_color_selector.xml
    new file mode 100644
    index 0000000000..dfd9cfcac6
    --- /dev/null
    +++ b/vector/src/main/res/color/checked_accent_color_selector.xml
    @@ -0,0 +1,5 @@
    +
    +
    +    
    +    
    +
    diff --git a/vector/src/main/res/drawable/background_checked_accent_color.xml b/vector/src/main/res/drawable/background_checked_accent_color.xml
    new file mode 100644
    index 0000000000..ee2cdeae23
    --- /dev/null
    +++ b/vector/src/main/res/drawable/background_checked_accent_color.xml
    @@ -0,0 +1,6 @@
    +
    +
    +
    +    
    +
    +
    \ No newline at end of file
    diff --git a/vector/src/main/res/drawable/ic_material_share.xml b/vector/src/main/res/drawable/ic_material_share.xml
    new file mode 100644
    index 0000000000..5ecbfe3388
    --- /dev/null
    +++ b/vector/src/main/res/drawable/ic_material_share.xml
    @@ -0,0 +1,24 @@
    +
    +    
    +    
    +    
    +    
    +    
    +
    diff --git a/vector/src/main/res/layout/activity_big_image_viewer.xml b/vector/src/main/res/layout/activity_big_image_viewer.xml
    new file mode 100644
    index 0000000000..124b76f64e
    --- /dev/null
    +++ b/vector/src/main/res/layout/activity_big_image_viewer.xml
    @@ -0,0 +1,27 @@
    +
    +
    +
    +    
    +
    +    
    +
    +
    \ No newline at end of file
    diff --git a/vector/src/main/res/layout/activity_incoming_share.xml b/vector/src/main/res/layout/activity_incoming_share.xml
    index 986a852b5a..031e1b958e 100644
    --- a/vector/src/main/res/layout/activity_incoming_share.xml
    +++ b/vector/src/main/res/layout/activity_incoming_share.xml
    @@ -13,7 +13,6 @@
                 style="@style/VectorToolbarStyle"
                 android:layout_width="0dp"
                 android:layout_height="?attr/actionBarSize"
    -            android:elevation="4dp"
                 app:contentInsetStart="0dp"
                 app:layout_constraintEnd_toEndOf="parent"
                 app:layout_constraintStart_toStartOf="parent"
    diff --git a/vector/src/main/res/layout/bottom_sheet_verification.xml b/vector/src/main/res/layout/bottom_sheet_verification.xml
    index 7ed760b45a..6e4a952e69 100644
    --- a/vector/src/main/res/layout/bottom_sheet_verification.xml
    +++ b/vector/src/main/res/layout/bottom_sheet_verification.xml
    @@ -60,7 +60,7 @@
                 android:id="@+id/bottomSheetFragmentContainer"
                 android:layout_width="0dp"
                 android:layout_height="wrap_content"
    -            android:layout_marginTop="16dp"
    +            android:layout_marginTop="8dp"
                 app:layout_constraintEnd_toEndOf="parent"
                 app:layout_constraintStart_toStartOf="parent"
                 app:layout_constraintTop_toBottomOf="@+id/verificationRequestAvatar" />
    diff --git a/vector/src/main/res/layout/fragment_attachments_preview.xml b/vector/src/main/res/layout/fragment_attachments_preview.xml
    new file mode 100644
    index 0000000000..ec99cce9b3
    --- /dev/null
    +++ b/vector/src/main/res/layout/fragment_attachments_preview.xml
    @@ -0,0 +1,75 @@
    +
    +
    +
    +    
    +
    +    
    +
    +    
    +
    +        
    +
    +        
    +
    +    
    +
    +    
    +
    +
    diff --git a/vector/src/main/res/layout/fragment_incoming_share.xml b/vector/src/main/res/layout/fragment_incoming_share.xml
    new file mode 100644
    index 0000000000..e88e52a773
    --- /dev/null
    +++ b/vector/src/main/res/layout/fragment_incoming_share.xml
    @@ -0,0 +1,58 @@
    +
    +
    +
    +    
    +
    +        
    +
    +            
    +
    +        
    +
    +        
    +
    +        
    +
    +    
    +
    +
    \ No newline at end of file
    diff --git a/vector/src/main/res/layout/fragment_keys_backup_restore_from_key.xml b/vector/src/main/res/layout/fragment_keys_backup_restore_from_key.xml
    index cf24ab6af1..3592ff074d 100644
    --- a/vector/src/main/res/layout/fragment_keys_backup_restore_from_key.xml
    +++ b/vector/src/main/res/layout/fragment_keys_backup_restore_from_key.xml
    @@ -4,7 +4,8 @@
         xmlns:tools="http://schemas.android.com/tools"
         android:id="@+id/keys_backup_root"
         android:layout_width="match_parent"
    -    android:layout_height="match_parent">
    +    android:layout_height="match_parent"
    +    android:background="?riotx_background">
     
         
    +    android:layout_height="match_parent"
    +    android:background="?riotx_background">
     
         
    +    android:layout_height="match_parent"
    +    android:background="?riotx_background">
     
         
     
         
     
         
     
         
    +                app:errorEnabled="true"
    +                tools:hint="@string/login_signin_username_hint">
     
                     
    +
    +
    +    
    +
    +        
    +
    +        
    +
    +
    +        
    +
    +
    +        
    +
    +
    +        
    +
    +            
    +
    +        
    +
    +        
    +
    +
    +        
    +
    +
    +        
    +
    +    
    +
    \ No newline at end of file
    diff --git a/vector/src/main/res/layout/item_attachment_big_preview.xml b/vector/src/main/res/layout/item_attachment_big_preview.xml
    new file mode 100644
    index 0000000000..e3fb89416e
    --- /dev/null
    +++ b/vector/src/main/res/layout/item_attachment_big_preview.xml
    @@ -0,0 +1,14 @@
    +
    +
    +
    +    
    +
    +
    \ No newline at end of file
    diff --git a/vector/src/main/res/layout/item_attachment_miniature_preview.xml b/vector/src/main/res/layout/item_attachment_miniature_preview.xml
    new file mode 100644
    index 0000000000..a0fde7b778
    --- /dev/null
    +++ b/vector/src/main/res/layout/item_attachment_miniature_preview.xml
    @@ -0,0 +1,20 @@
    +
    +
    +
    +    
    +
    +
    \ No newline at end of file
    diff --git a/vector/src/main/res/layout/item_room.xml b/vector/src/main/res/layout/item_room.xml
    index 3ae16ddca9..05a2ff4ca9 100644
    --- a/vector/src/main/res/layout/item_room.xml
    +++ b/vector/src/main/res/layout/item_room.xml
    @@ -21,21 +21,37 @@
             app:layout_constraintTop_toTopOf="parent"
             tools:visibility="visible" />
     
    -    
    +        app:layout_constraintTop_toTopOf="parent">
    +
    +        
    +
    +        
    +
    +    
     
         
     
         
     
    diff --git a/vector/src/main/res/layout/view_stub_room_profile_header.xml b/vector/src/main/res/layout/view_stub_room_profile_header.xml
    index 9b733b7fa6..f7ae1c77a5 100644
    --- a/vector/src/main/res/layout/view_stub_room_profile_header.xml
    +++ b/vector/src/main/res/layout/view_stub_room_profile_header.xml
    @@ -12,6 +12,7 @@
             android:layout_width="128dp"
             android:layout_height="128dp"
             android:layout_marginBottom="16dp"
    +        android:transitionName="roomProfileAvatarView"
             app:layout_constraintBottom_toTopOf="@+id/roomProfileNameView"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="parent"
    diff --git a/vector/src/main/res/menu/vector_attachments_preview.xml b/vector/src/main/res/menu/vector_attachments_preview.xml
    new file mode 100755
    index 0000000000..c3c8529b18
    --- /dev/null
    +++ b/vector/src/main/res/menu/vector_attachments_preview.xml
    @@ -0,0 +1,19 @@
    +
    +
    +
    +    
    +
    +    
    +
    +
    diff --git a/vector/src/main/res/menu/vector_room_member_profile.xml b/vector/src/main/res/menu/vector_room_member_profile.xml
    new file mode 100644
    index 0000000000..e7fffd2a6b
    --- /dev/null
    +++ b/vector/src/main/res/menu/vector_room_member_profile.xml
    @@ -0,0 +1,10 @@
    +
    +
    +    
    +
    \ No newline at end of file
    diff --git a/vector/src/main/res/menu/vector_room_profile.xml b/vector/src/main/res/menu/vector_room_profile.xml
    new file mode 100644
    index 0000000000..f6331018f0
    --- /dev/null
    +++ b/vector/src/main/res/menu/vector_room_profile.xml
    @@ -0,0 +1,10 @@
    +
    +
    +    
    +
    \ No newline at end of file
    diff --git a/vector/src/main/res/values-bg/strings.xml b/vector/src/main/res/values-bg/strings.xml
    index 873fe447f1..3428cbbfde 100644
    --- a/vector/src/main/res/values-bg/strings.xml
    +++ b/vector/src/main/res/values-bg/strings.xml
    @@ -1907,7 +1907,7 @@
         Телефонния номер изглежда невалиден. Моля проверете го
     
         Регистрация в %1$s
    -    Потребителско име или имейл
    +    Потребителско име или имейл
         Парола
         Напред
         Това потребителско име е заето
    diff --git a/vector/src/main/res/values-cs/strings.xml b/vector/src/main/res/values-cs/strings.xml
    index 0cf8f9d8e9..bbbc7b5af4 100644
    --- a/vector/src/main/res/values-cs/strings.xml
    +++ b/vector/src/main/res/values-cs/strings.xml
    @@ -48,13 +48,13 @@
         Aktivní hovor
         Probíhající konferenční hovor.
     \nPřipojit se jako %1$s nebo %2$s.
    -    hlasem
    -    videem
    +    Hlasem
    +    Videem
         Nemohu spustit hovor, prosím zkusit později
         Z důvodu chybějících práv mohou některé funkce chybět…
         Potřebujete práva pro spuštění konference
         Nemohu spustit hovor
    -    Informace o zařízení
    +    Informace o relaci
         Konferenční hovory nejsou podporovány v šifrovaných místnostech
         Stejně pošli
         Ukončit
    @@ -80,11 +80,11 @@
         Místnosti
         Komunity
     
    -    Hledat místnost
    -    Hledat v oblíbených
    -    Hledat v lidech
    -    Hledat v místnostech
    -    Hledat v komunitách
    +    Filtrovat jména místností
    +    Filtrovat oblíbené
    +    Filtrovat lidi
    +    Filtrovat jména místností
    +    Filtrovat jména komunit
     
         Pozvání
         Nízká priorita
    @@ -129,7 +129,7 @@
     
         Vstoupit do místnosti
         Uživatelské jméno
    -    Registrace
    +    Založit účet
         Přihlásit se
         Odhlásit se
         Adresa domácího serveru URL
    @@ -196,7 +196,7 @@
         Nelze se registrovat: Síťová chyba
         Nelze se registrovat
         Nelze se registrovat: chyba oveření vlastnictví emailu
    -    Zadej platnou URL
    +    Prosím, zadejte platné URL
         Mobil
     
         Neplatný uživatelský jméno/heslo
    @@ -239,9 +239,9 @@
         Registrace e-mailem a telefonním číslem najednou není zatím podporována z důvodu neexistujícího API. Pouze telefonní čislo bude bráno v potaz.
     
     Vaši e-mailovou adresu můžete přidat k profilu v nastavení.
    -    Vaše heslo bylo obnoveno.
    -
    -Byli jste odhlášeni ze všech zařízení a již nebudete dostávat oznámení. Pro opětovnou aktivaci oznámení se znovu přihlašte na každém zařízení.
    +    Vaše heslo bylo obnoveno. 
    +\n
    +\nByli jste odhlášeni ze všech relací a již nebudete dostávat oznámení. Pro opětovnou aktivaci oznámení se znovu přihlašte na každém zařízení.
         Prosím, přečtěte si a souhlaste s pravidly tohoto serveru:
     
         Přistupový token nebyl rozpoznán
    @@ -249,10 +249,10 @@ Byli jste odhlášeni ze všech zařízení a již nebudete dostávat oznámení
         E-mailový odkaz, na který ještě nebylo kliknuto
     
         Je nutné se znovu přihlásit, aby byly vygenerovány E2E šifrovací klíče pro toto zařízení a odeslán veřejný klíč Vašemu homeserveru.
    -Toto je nutné pouze jednou.
    -Omlouváme se za způsobené nepříjemnosti.
    +\nToto je nutné pouze jednou. 
    +\nOmlouváme se za způsobené nepříjemnosti.
     
    -    Znovu požádat o šifrovací klíče  z Vašich ostatních zařízení.
    +    Znovu požádat o šifrovací klíče  z Vašich ostatních relací.
     
         Žádost o klíče odeslána.
     
    @@ -263,7 +263,7 @@ Omlouváme se za způsobené nepříjemnosti.
             
         
     
    -    "Odeslat jako "
    +    Odeslat jako
         Původní
         Velký
         Střední
    @@ -326,7 +326,7 @@ Omlouváme se za způsobené nepříjemnosti.
         Náhled
         Byli jste pozváni %s ke vstupu do místnosti
         Žádost odeslána
    -    Prosím, spusťte Riot na jiném zařízení, které může dešifrovat zprávu, aby poslalo klíče tomuto zařízení.
    +    Prosím, spusťte Riot na jiném zařízení, které může dešifrovat zprávu, aby poslalo klíče této relaci.
     
         Seznam doručenek
     
    @@ -396,7 +396,7 @@ Omlouváme se za způsobené nepříjemnosti.
         NÁSTROJE SPRÁVCE
         HOVOR
         PŘÍMÉ KONVERZACE
    -    ZAŘÍZENÍ
    +    RELACE
     
         Pozvat
         Opustit tuto místnost
    @@ -411,7 +411,7 @@ Omlouváme se za způsobené nepříjemnosti.
         Zobrazit všechny zprávy od tohoto uživatele
         ID uživatele, jméno nebo e-mailová adresa
         Zmínit
    -    Zobrazit seznam zařízení
    +    Zobrazit seznam relací
         Toto je náhled místnosti. Interakce s místností byla vypnuta.
     
         
    @@ -447,7 +447,7 @@ Omlouváme se za způsobené nepříjemnosti.
         Spojení se serverem bylo ztraceno.
         Zpráva neodeslána. %1$s nebo %2$s nyní?
         Znovu odeslat vše
    -    zrušit vše
    +    Zrušit vše
         Znovu odeslat neodeslané zprávy
         Smazat neodeslané zprávy
         Soubor nenalezen
    @@ -481,7 +481,7 @@ Omlouváme se za způsobené nepříjemnosti.
         Záloha klíče není ještě dokončena, prosím počkejte…
         Pokud se nyní odhlásíte, ztratíte vaše zašifrované zprávy
         Probíhá záloha klíče. Pokud se nyní odhlásíte, ztratíte přístup k vašim zašifrovaným zprávám.
    -    Zabezpečená záloha klíčů by měla být aktivní na všech vašich zařízeních aby se předešlo ztrátě přístupu k vašim zašifrovaným zprávám.
    +    Zabezpečená záloha klíčů by měla být aktivní na všech vašich relacích, aby se předešlo ztrátě přístupu k vašim zašifrovaným zprávám.
         Nechci své zašifrované zprávy
         Zálohuji klíče…
         Použít zálohu klíče
    @@ -500,15 +500,13 @@ Omlouváme se za způsobené nepříjemnosti.
         Riot potřebuje oprávnění pro přístup k Vaší kameře a mikrofonu pro uskutečnění video hovoru.
     \n
     \nProsím, povolte přístup na následující hlášce abyste mohli uskutečnit hovor.
    -    Riot potřebuje oprávnění pro přístup k Vašim kontaktům aby bylo možné nalézt ostatní uživatelé Matrixu na základě jejich emailu a telefonního čísla.
    -\n
    -\nProsím, povolte přístup na následující hlášce pro nalezení uživatelů dosažitelných z Riot ve vašich kontaktech.
    -    Riot potřebuje oprávnění pro přístup k Vašim kontaktům aby bylo možné nalézt ostatní uživatelé Matrixu na základě jejich emailu a telefonního čísla.
    -\n
    -\nPovolit aplikaci Riot přístup k Vašim kontaktům\?
    +    Riot může nahlédnout do Vašeho adresáře, aby nalezl ostatní uživatele Matrixu na základě jejich emailu a telefonního čísla. Pokud souhlasíte se sdílením svého adresáře za tímto účelem, prosím, povolte přístup v příští hlášce.
    +    Riot může nahlédnout do Vašeho adresáře, aby nalezl ostatní uživatele Matrixu na základě jejich emailu a telefonního čísla. 
    +\n 
    +\nSouhlasíte se sdílením svěho adresáře za tímto účelem\?
     
    -    Pozvánka byla odeslána na %s, což není spárováno s tímto účtem.
    -\nPřihlaste se s jiným účtem, nebo přidejte tento email k Vašemu současnému účtu.
    +    Pozvánka byla odeslána na %s, což není spárováno s tímto účtem. 
    +\nPřihlaste se s jiným účtem nebo přidejte tento email k svému současnému účtu.
         Snažíte se přistupovat k %s. Chcete vstoupit, abyste se mohli podílet na diskuzi\?
         Zobrazit všechny zprávy od tohoto uživatele\?
     \n
    @@ -517,7 +515,7 @@ Omlouváme se za způsobené nepříjemnosti.
     \nJste si jisti\?
     
         Prosím zadejte jednu nebo více emailových adres nebo Matrix ID
    -    Zprávy nebyly odeslány, protože je přítomno neznámé zařízení. %1$s nebo %2$s nyní\?
    +    Zprávy nebyly odeslány z důvodu přítomnosti neznámých relací. %1$s nebo %2$s nyní\?
         Toto by mohlo znamenat, že někdo škodlivě zachytává Vaši komunikaci nebo že Váš telefon nedůvěřuje certifikátu poskytnutému vzdáleným serverem.
         Pokud administrátor serveru řekl že toto je předpokládané, ujistěte se, že otisk níže se shoduje s otiskem který Vám poskytl.
         Certifikát se změnil z toho, kterému Váš telefon důvěřoval. Toto je VYSOCE NEOBVYKLÉ. Je doporučeno abyste NEPŘIJÍMALI tento nový certifikát.
    @@ -603,11 +601,448 @@ Omlouváme se za způsobené nepříjemnosti.
         Důležitost upozornění na základě události
     
         Inicializuji službu
    -    Ověřte zařízení
    +    Ověřte relaci
     
         Odpojit
         Ignorovat
         Odmítnout
     
         Označit za přečtené
    +    Latn
    +
    +    Žádný
    +    Zrušit
    +    Přehled
    +    Nekonfigurován žádný ověřovací server.
    +
    +    Volání se nezdařilo kvůli chybné konfiguraci serveru
    +    Prosím, požádejte administrátora homeserveru (%1$s) o konfiguraci TURN serveru, aby hovory fungovaly spolehlivě.
    +\n
    +\nPřípadně můžete zkusit použít veřejný server %2$s, ale nebude tak spolehlivý a budu sdílet Vaši IP adresu s tímto serverem. V nastavení lze toto změnit.
    +    Zkuste použít %s
    +    Dále se netázat
    +
    +    Přihlásit se se single sign-on
    +    Nastavte email pro obnovení účtu a volitelně aby jej později lidé, kteří Vás znají, mohli najít.
    +    Nastavte telefonní číslo, aby později bylo volitelně k nalezení lidmi, kteří Vás znají.
    +    Nastavte email pro obnovení účtu. Použijte email nebo telefonní číslo volitelně později, aby je našli lidé, kteří Vás znají.
    +    Nastavte email pro obnovení účtu. Použijte email nebo telefonní číslo volitelně později, aby je našli lidé, kteří Vás znají.
    +    Toto URL není dostupné, prosím, prověřte
    +    To není platná adresa Matrix serveru
    +    Homeserver není dostupný na tomto URL, prosím, prověřte
    +    Vaše zařízení používá zastaralý bezpečnostní protokol TLS, zranitelný útokem, pro Vaše bezpečí se nebudete moci spojit
    +    Povolit server fallback call assist
    +    Použiiji %s jako nápomoc, pokud Váš homeserver žádnou nenabízí (Vaše IP adresa bude sdělena během hovoru)
    +    K provedení této akce přidat ověřovací server v nastavení.
    +    Potvrďte své heslo
    +    Nelze provést z Riot mobile
    +    Ověření se vyžaduje
    +
    +
    +    Soukromí oznámení
    +    Řešit oznámení
    +    Řešit diagnostiku
    +    Spustit testy
    +    Spouštím… (%1$d z %2$d)
    +    Základní diagnostika je ok. Pokud stále ještě nedocházejí oznámení, prosím, pomůžete nám s vyšetřováním, nahlásíte-li chybu.
    +    Jeden či více testů selhalo, zkuste navržené opravy.
    +    Jeden či více testů selhalo, prosím, pomůžete nám s vyšetřováním, nahlásíte-li chybu.
    +
    +    Systémová nastavení.
    +    Oznámení jsou zapnuta v systémových nastavení.
    +    Oznámení jsou vypnuta v systémových nastavení.
    +\nProsím, prověřte systémová nastavení.
    +    Otevřít nastavení
    +
    +    Nastavení účtu.
    +    Oznámení jsou zapnuta pro Váš účet.
    +    Oznámení jsou vypnuta pro Váš účet.
    +\nProsím, prověřte systémová nastavení.
    +    Zapnout
    +
    +    Nastavení relací.
    +    Oznámení jsou zapnuta pro tuto relaci.
    +    Oznámení nejosu zapnuta pro tuto relaci.
    +\nProsím, prověřte nastavení Riotu.
    +    Zapnout
    +
    +    Vlastní nastavení.
    +    Všimněte si, že některé typy zpráv jsou jsou umlčeny (vyvolají oznámeni bez zvuku).
    +    Některá oznámení jsou vypnuta ve vlastních nastavení.
    +    Načtení vlastních pravidel selhalo, prosím, zkuste znovu.
    +    Kontrola nastavení
    +
    +    Kontrola služeb Play
    +    Google Play Services APK je k dispozici a aktuální.
    +    Riot používá Google Play Services pro doručení zpráv push, ale patrně nejsou správně nastaveny:
    +\n%1$s
    +    Opravit Play Services
    +
    +    Token Firebase
    +    FCM token byl úspěšně načten:
    +\n%1$s
    +    Načtení FCM tokenu selhalo:
    +\n%1$s
    +    [%1$s]
    +\nTato chyba je mimo kontrolu Riotu a podle Googlu indikuje, že zařízení má příliš mnoho aplikací registrovaných s FCM. Tato chyba se ukáže jen v případech extrémního množství aplikací, a proto by neměla mít vliv na normálního uživatele.
    +    [%1$s]
    +\nTato chyba je mimo kontrolu Riotu. Múže k ní dojít z několika důvodů. Snad bude fungovat, když zkusíte znovu později, můžete též zkontrolovat, zda Google Play Service nejsou omezeny v množství dat v systémových nastavení nebo zda hodiny zařízení jdou správné nebo k chybě může dojít na zvláštní ROM.
    +    [%1$s]
    +\nTato chyba je mimo kontrolu Riotu. V telefonu není žádný účet Google. Prosím, spusťte správce účtů a doplňte účet Google.
    +    Přidat účet
    +
    +    Registrace tokenu
    +    FCM token se podařilo úspěšné registrovat na homeserveru.
    +    Registrace FCM tokenu na homeserveru se nezdařila:
    +\n%1$s
    +
    +    Služba oznámení
    +    Služba oznámení je v chodu.
    +    Služba oznámení není v chodu.
    +\nZkuste restarovat aplikaci.
    +    Spustit službu
    +
    +    Auto-restart služby oznámení
    +    Služba se zbořila a spustila se automaticky.
    +    Restart služby se nezdařil
    +
    +    Spustit při zavádění
    +    Služba se spustí při restartu zařízení.
    +    Služba se nespustí při startu zařízení, neobdržíte oznámení, dokud jednou neotevřete Riot.
    +    Zapnout Spustit při zavádění
    +
    +    Zkontrolovat omezení na pozadí
    +    Omezení na pozadí jsou pro Riot vypnuta. Tento test by měl běžet s mobilními daty (ne WIFI).
    +\n%1$s
    +    Omezení na pozadí jsou pro Riot zapnuta.
    +\nČinnosti prováděné aplikací budou agresivně omezeny, bude-li v pozadí, a to může mít vliv na oznámení.
    +\n%1$s
    +    Vypnout omezení
    +
    +    Optimalizace baterie
    +    Optimalizace baterie nemá na Riot vliv.
    +    Nechá-li uživatel zařízení vytažený ze zásuvky a v klidu po nějakou dobu a s obrazovkou vypnutou, zařízení vstoupí do stavu spánku. Ten zamezí aplikacím přístup k síti a odloží jejich úlohy, synchronizaci a standartní upozornění.
    +    Ignorovat optimalizaci
    +
    +    Normální
    +    Snížené soukromí
    +    Aplikaci potřebuje svolení k chodu na pozadí
    +    Aplikace na pozadí opravdu nepotřebuje spojení k homeserveru, to by mělo snížit spotřebu
    +    "• Oznámení se posílají pomocí  Firebase Cloud Messaging"
    +    • Oznámení pouze obsahují metadata
    +    • Obsah oznámení je bezpečně uložen přímo na homeserveru Matrixu
    +    • Oznámení obsahují metadata a data zprávy
    +    • Oznámení neukážou obsah zprávy
    +
    +    Zvuk oznámení
    +    Zapnout oznámení pro tento účet
    +    Zapnout oznámení pro tuto relaci
    +    Zapněte obrazovku na 3 vteřiny
    +    Nastavit hlučná oznámení
    +    Nastavit oznámení hovoru
    +    Nastavit tichá oznámení
    +    Zvolit barvu LED, vibrace, zvuk…
    +
    +
    +    Zprávy osahující mé veřejné jméno
    +    Zprávy osahující mé uživatelské jméno
    +    Zprávy chatu one-to-one
    +    Zprávy skupinových chatů
    +    Jsem-li pozván do místnosti
    +    Pozvání k hovoru
    +    Zprávy poslány botem
    +
    +    Synchronizace na pozadí
    +    Režim synchronizace na pozadí (experimentální)
    +    Optimalizován pro baterii
    +    Riot bude synchronizovat na pozadí způsobem, který šetří omezené zdroje zařízení (baterii).
    +\nV závislosti na stavu zdrojů zařízení může být sync operačním systémem odložen.
    +    Optimalizováno pro reálný čas
    +    Riot bude synchronizovat na pozadí periodicky v přesný čas (nastavitelné).
    +\nTo bude mít vliv na využití rádia a baterie, stálé oznámení o tom, že Riot čeká na události, bude zobrazeno.
    +    Žádný sync na pozadí
    +    Neobdržíte oznámení o příchozích zprávách, je-li aplikace na pozadí.
    +    Aktualizace nastavení se nezdařila.
    +
    +
    +    Start při zavádění
    +    Zapnout sync na pozadí
    +    Čas požadavku na sync vypršel
    +    Preferovaný sync interval
    +    %s
    +\nSync může být odložen v závislosti na zdrojích (baterie) nebo stavu zařízení (spánek).
    +    Prodleva mezi jednotlivými syncy
    +    vteřina
    +    vteřiny
    +
    +    Verze
    +    verze olm
    +    Všeobecné podmínky
    +    Oznámení třetích stran
    +    Vlastnická práva
    +    Ochrana osobníh údajů
    +    Ponechat media
    +    Vymazat mezipaměť
    +    Vymazat mezipaměť medií
    +
    +    Uživatelská nastavení
    +    Oznámení
    +    Ignorovaní uživatelé
    +    Ostatní
    +    Pokročilé
    +    Integrace
    +    Použijte správce integrací ke správě botů, můstků, widgetů a nálepkových sad.
    +\nSprávci integrací obdrží konfigurační data a mohou změnit widgety, poslat pozvánky do místností a nastavit power levels Vaším jménem.
    +    Kryptografie
    +    Správa kryprografických klíčů
    +    Cíle oznámení
    +    Místní kontakty
    +    Svolení ohledně kontaktů
    +    Země adresáře
    +    Domovská obrazovka
    +    Připnout místnosti se zmeškanými oznámeními
    +    Připnout místnosti s nepřečtenými zprávami
    +    Relace
    +    Interní náhled URL
    +    Náhled odkazů v chatu, pokud Váš homeserver podporuje tuto funkci.
    +    Poslat oznámení o psaní
    +    Dejte ostatním vědět, že právě píšete.
    +    Formát markdown
    +    Formátujte zprávy s markdown syntaxí, než je odešlete. Umožní pokročilý formát jako je použití hvězdiček k zobrazení kurzívy.
    +    Zobrazit časovou značku u všech zpráv
    +    Zobrazit časovou značku ve 12-hodinovém formátu
    +    Ukázat potvrzení o přečtení
    +    Kliknout na potvrzení o přečtení pro podrobnosti.
    +    Ukázat události příchodů a odchodů
    +    Nemá vliv na pozvánky, nakopnutí a zákazy.
    +    Ukázat události účtu
    +    Zahrnout avatar a změny veřejného jména.
    +    Vibrovat při zmínce uživatele
    +    Prohlédnout média před odesláním
    +    Odeslat zprávu stiskem enter
    +    Tlačítko enter na dotykové klávesnici odešle zprávu místo nového řádku
    +
    +    Deaktivace účtu
    +    Deaktivovat můj účet
    +    Odhalení
    +    Správa Vašich nastaveni pro odhalení.
    +    Soukromí ohledně oznámení
    +    Riot může běžet na pozadí, aby spravoval Vaše oznámení bezpečně a v soukromí. To může mít vliv na baterii.
    +    Udělit svolení
    +    Vybrat jinou volbu
    +
    +    Spojení na pozadí
    +    Riot potřebuje udržovat spojení na pozadí se slabým vlivem, abyste obdrželi spolehlivá oznámení.
    +\nNa příští obrazovce budete dotázáni o svolení nechat Riot vždy v chodu na pozadí, prosím, souhlaste.
    +    Udělit svolení
    +
    +    Analýza
    +    Odeslat analytická data
    +    Riot sbírá anonymní analytická data pro vylepšení aplikace.
    +    Prosím, zapněte analýzu pro vylepšení Riotu.
    +    Ano, chci pomoci!
    +
    +    Režim úsporných dat
    +    Režim úsporných data zavádí specifický filtr, takže aktualizace přítomnosti a oznámení o psaní jsou fitrovány.
    +
    +    Informace o relaci
    +    ID
    +    Veřejné jméno
    +    Aktualizovat veřejné jméno
    +    Viděn naposledy
    +    %1$s @ %2$s
    +    Tato operace vyžaduje dodatečné ověření.
    +\nProsím, zadejte heslo a pokračujte.
    +    Ověření
    +    Heslo:
    +    Odeslat
    +
    +    Přihlášen jako
    +    Home server
    +    Ověřovací server
    +    Povolit integrace
    +    Správce integrací
    +
    +    Uživatelské rozhraní
    +    Jazyk
    +    Zvolit jazyk
    +
    +    Čekám na potvrzení
    +    Prosím, zkontrolujte svůj email a klikněte na odkaz v něm. Jakmile hotovo, klikněte pokračovat.
    +    Nemohu potvrdit emailovou adresu. Prosím, zkontrolujte svůj email a klikněte na odkaz v něm. Jakmile hotovo, klikněte pokračovat.
    +    Tato emailová adresa byla již zadána.
    +    Tato emailová adresa nebyla nalezena.
    +    Toto telefonní číslo bylo již zadáno.
    +    Při potvrzování Vaší emailové adresy došlo k chybě.
    +
    +    Heslo
    +    Změnit heslo
    +    Stávající heslo
    +    Nové heslo
    +    Potvrdit nové heslo
    +    Aktualizovat heslo
    +    Aktualizace hesla se nezdařila
    +    Heslo není správné
    +    Vaše heslo bylo aktualizováno
    +    Ukázat všechny zprávy od %s\?
    +\nPřipomínám, že tato akce restartuje aplikaci a může chvíli trvat.
    +    Hesla se neshodují
    +
    +    Jste si jisti, že chcete odstranit tento cíl oznámení\?
    +
    +    Jste si jisti, že chcete odstranit %1$s %2$s\?
    +
    +    Vybrat zemi
    +
    +    Země
    +    Prosím, vyberte zemi
    +    Telefonní číslo
    +    Neplatné telefonní číslo pro zvolenou zemi
    +    Potvrzení telefonního čísla
    +    Poslali jsme SMS s aktivačním kódem. Prosím, zadejte kód níže.
    +    Zadejte aktivační kód
    +    Chyba při ověření Vašeho telefonního čísla
    +    Kód
    +    Při ověření Vašeho telefonního čísla došlo k chybě.
    +    Dodatečné info: %s
    +
    +    Média
    +    Výchozí komprese
    +    Vybrat
    +    Výchozí zdroj médií
    +    Vybrat
    +    Přehrát zvuk uzávěrky
    +
    +    Flér
    +    Zatím nejste členem žádné komunity.
    +
    +    3 dny
    +    1 týden
    +    1 měsíc
    +    Navždy
    +
    +    Obrázek místnosti
    +    Název místnosti
    +    Téma
    +    Štítek místnosti
    +    Označena jako:
    +
    +    Oblíbené
    +    Nízká priorita
    +    Žádný
    +
    +    Přístup a viditelnost
    +    Uvést tuto místnost v adresáři místností
    +    Oznámení
    +    Přístup do místnosti
    +    Čitelnost historie místnosti
    +    Kdo smí číst historii\?
    +    Kdo smí do této místnosti\?
    +
    +    Kdokoli
    +    Jen členové (od okamžiku výběru této volby)
    +    Jen členové (od okamžiku jejich pozvání)
    +    Jen členové (od okamžiku jejich příchodu)
    +
    +    Odkázat na místnost si žádá adresu místnosti.
    +    Jen lidé, kteří byli pozváni
    +    Kdokoli, kdo zná odkaz na místnost, kromě hostů
    +    Kdokoli, kdo zná odkaz na místnost, včetně hostů
    +
    +    Zakázaní uživatelé
    +
    +    Pokročilé
    +    Interní ID této místnosti
    +    Adresa
    +    Labs
    +    Toto jsou experimentální funkce, které mohou selhat neočekávanými způsoby. Použijte obezřetně.
    +    Šifrování end-to-end
    +    Šifrování end-to-end je aktivní
    +    Pro aktivaci šifrování se musíte odhlásit.
    +    Šifruj pouze do ověřených relací
    +    Nikdy neposílejte šifrované zprávy do neověřených relací v této místnosti z této relace.
    +
    +    Tato místnost nemá místní adresy
    +    Nová adresa (např. #foo:matrix.org)
    +
    +    Tato místnost neukazuje flér pro žádné komunity
    +    Nové ID komunity (např. +foo:matrix.org)
    +    Neplatné ID komunity
    +    \'%s\' není platné ID komunity
    +
    +
    +    Neplatný formát aliasu
    +    \'%s\' není platný formát pro alias
    +    Nebudete mít žádnou hlavní adresu učenou této místnosti.
    +    Varování o hlavní adrese
    +
    +    Nastavit jako hlavní adresu
    +    Odebrat jako hlavní adresu
    +    Kopírovat ID místnosti
    +    Kopírovat adresu místnosti
    +
    +    Šifrování je aktivní v této místnosti.
    +    Šifrování je neaktivní v této místnosti.
    +    Zapnout šifrování
    +\n(varování: nelze vypnout!)
    +
    +    Adresář
    +    Motiv
    +
    +    %s se pokusil načíst určitý okamžik v historii této místnosti, ale nemohl jej najít.
    +
    +    Informace o šifrování end-to-end
    +
    +    Informace of události
    +    Uživatelské id
    +    Identifikační klíč Curve25519
    +    Nárokovaný klíč s otiskem Ed25519
    +    Algoritmus
    +    ID relace
    +    Chyba dešifrování
    +
    +    Informace of relace odesílatele
    +    Veřejné jméno
    +    Veřejné jméno (viditelné pro lidi, s nimiž komunikujete)
    +    Veřejné jméno relace je viditelné pro lidi, s nimiž komunikujete
    +    Veřejné jméno
    +    ID
    +    Klíč relace
    +    Potvrzení
    +    Otisk Ed25519
    +
    +    Export E2E klíčů místností
    +    Export klíčů místností
    +    Export klíčů do místního souboru
    +    Export
    +    Prosím, založte heslo k zašifrování exportovaných klíčů. Pro import klíčů budete muset zadat stejné heslo.
    +    E2E klíče místností byly uloženy do \'%s\'.
    +\n
    +\nVarování: může dojít k smazání souboru, pokud bude aplikace odinstalována.
    +
    +    Obnovení zašifrovaných zpráv
    +    Správa zálohy klíčů
    +
    +    Import E2E klíčů místností
    +    Import klíčů místností
    +    Import klíčů z místního souboru
    +    Import
    +    Šifruj pouze do ověřených relací
    +    Nikdy neposílejte šifrované zprávy do neověřených relací z této relace.
    +    Klíče %1$d/%2$d importovány úspěšně.
    +
    +    NENÍ potvrzeno
    +    Potvrzeno
    +    Na černé listině
    +
    +    neznámá relace
    +    neznámá ip
    +    není
    +
    +    Potvrdit
    +    Odebrat potvrzení
    +    Černá listina
    +    Odebrat z černé listiny
    +
    +    Potvrdit relaci
     
    diff --git a/vector/src/main/res/values-da/strings.xml b/vector/src/main/res/values-da/strings.xml
    index fff61797f5..62383f40bb 100644
    --- a/vector/src/main/res/values-da/strings.xml
    +++ b/vector/src/main/res/values-da/strings.xml
    @@ -495,4 +495,20 @@ Er du sikker?
         Avanceret
         Kryptografi
         Lokale kontakter
    +    Klargører service
    +    Send et klistermærke
    +    Nøgle-backup
    +    Brug Nøgle-backup
    +    Bekræft session
    +
    +    Nøgle-backup er ikke færdig, vent venligst…
    +    Du vil miste dine krypterede beskeder, hvis du logger ud nu
    +    Jeg ønsker ikke mine krypterede beskeder
    +    Benyt Nøgle-backup
    +    Er du sikker\?
    +    Back up
    +    Du vil miste adgang til dine krypterede beskeder, medmindre du tager backup af dine nøgler, før du logger ud.
    +
    +    Tredjeparts licenser
    +
     
    diff --git a/vector/src/main/res/values-de/strings.xml b/vector/src/main/res/values-de/strings.xml
    index fe059320d3..8af2391b5f 100644
    --- a/vector/src/main/res/values-de/strings.xml
    +++ b/vector/src/main/res/values-de/strings.xml
    @@ -173,8 +173,8 @@
         Eine E-Mail wurde an %s gesendet. Bitte dem in der E-Mail enthaltenen Link folgen und anschließend unten klicken.
         Verifizierung der E-Mail-Adresse ist fehlgeschlagen: Stelle sicher, dass du den Link in der E-Mail geöffnet hast
         Dein Passwort wurde zurückgesetzt.
    -
    -Du wurdest auf allen Geräten abgemeldet und wirst keine Push-Benachrichtigungen mehr erhalten. Um die Push-Benachrichtigungen wieder zu aktivieren, musst du dich auf jedem Gerät erneut anmelden.
    +\n
    +\nDu wurdest aus allen Sitzungen abgemeldet und wirst keine Push-Benachrichtigungen mehr erhalten. Um die Push-Benachrichtigungen wieder zu aktivieren, musst du dich auf jedem Gerät erneut anmelden.
     
         
         URL muss mit \'http[s]://\' beginnen
    @@ -194,7 +194,9 @@ Du wurdest auf allen Geräten abgemeldet und wirst keine Push-Benachrichtigungen
         Der Link in der E-Mail wurde noch nicht geöffnet
     
         
    -    Du musst dich erneut anmelden um Ende-zu-Ende-Verschlüsselungs-Schlüssel for dieses Gerät zu generieren und den öffentlichen Schlüssel an deinen Heimserver zu senden.\nDies ist einmalig.\nEntschuldige die Unannehmlichkeiten.
    +    Du musst dich erneut anmelden, um Ende-zu-Ende-Verschlüsselungsschlüssel für diese Sitzung zu generieren und den öffentlichen Schlüssel an deinen Heimserver zu senden.
    +\nDies ist einmalig.
    +\nEntschuldige die Unannehmlichkeiten.
     
         
         Lesebestätigungsliste
    @@ -303,7 +305,7 @@ Du kannst dich mit einem anderen Konto anmelden oder diese E-Mail-Adresse zu die
         ADMIN-WERKZEUGE
         ANRUFE
         DIREKT-CHATS
    -    GERÄTE
    +    SITZUNGEN
     
         Einladen
         Diesen Raum verlassen
    @@ -317,7 +319,7 @@ Du kannst dich mit einem anderen Konto anmelden oder diese E-Mail-Adresse zu die
         Alle Nachrichten dieses Nutzers anzeigen
         Benutzer-ID, Name oder E-Mail-Adresse
         Erwähnen
    -    Geräteliste anzeigen
    +    Sitzungsliste anzeigen
         Du wirst diese Änderung nicht rückgängig machen können, da der Benutzer dasselbe Berechtigungslevel wie du selbst erhalten wirst.
     Bist du sicher?
     
    @@ -341,7 +343,7 @@ Bist du sicher?
         Nachricht senden (unverschlüsselt)…
         Verbindung zum Server wurde unterbrochen.
         Nachrichten konnten nicht gesendet werden. %1$s oder %2$s?
    -    Nachrichten wurden nicht gesendet, da unbekannte Geräte anwesend sind. %1$s oder %2$s?
    +    Nachrichten wurden nicht gesendet, da unbekannte Sitzungen anwesend sind. Jetzt %1$s oder %2$s\?
         Alles erneut senden
         alles abbrechen
         Nicht gesendete Nachrichten erneut senden
    @@ -433,7 +435,7 @@ Beachte: Diese Aktion wird die App neu starten und einige Zeit brauchen.App-Info
     
         Benachrichtigungen für diesen Account aktivieren
    -    Benachrichtigungen für dieses Gerät aktivieren
    +    Benachrichtigungen für diese Sitzung aktivieren
         Bildschirm für 3 Sekunden aktivieren
     
         Nachrichten in direkten Chats
    @@ -472,8 +474,8 @@ Beachte: Diese Aktion wird die App neu starten und einige Zeit brauchen.Startseite
         Räume mit ungelesenen Benachrichtigungen anheften
         Räume mit ungelesenen Nachrichten anheften
    -    Geräte
    -    Geräteinformationen
    +    Sitzungen
    +    Sitzungsinformationen
         ID
         Öffentlicher Name
         Öffentlichen Namen aktualisieren
    @@ -936,12 +938,12 @@ Willst du welche hinzufügen?
         Download
         Sprechen
         Leeren
    -    Verschlüsselungsschlüssel von deinen anderen Geräten erneut anfragen.
    +    Verschlüsselungsschlüssel von deinen anderen Sitzungen erneut anfragen.
     
         Schlüsselanfrage gesendet.
     
         Anfrage gesendet
    -    Bitte öffne Riot auf einem anderen Gerät, das die Nachricht entschlüsseln kann, damit es diesem Gerät die Schlüssel senden kann.
    +    Bitte öffne Riot auf einem anderen Gerät, das die Nachricht entschlüsseln kann, damit es die Schlüssel an diese Sitzung senden kann.
     
         Hier tippen…
     
    @@ -1120,9 +1122,9 @@ Bitte überprüfe die Systemeinstellungen.
     Bitte überprüfe die Kontoeinstellungen.
         Aktiviere
     
    -    Geräteeinstellungen.
    -    Benachrichtigungen sind für dieses Gerät aktiviert.
    -    Benachrichtigungen sind für dieses Gerät nicht erlaubt.
    +    Sitzungseinstellungen.
    +    Benachrichtigungen sind für diese Sitzung aktiviert.
    +    Benachrichtigungen sind für diese Sitzung nicht aktiviert. 
     \nBitte überprüfe die Einstellungen für Riot.
         Aktiviere
     
    @@ -1941,7 +1943,7 @@ Wenn du diese neue Wiederherstellungsmethode nicht eingerichtet hast, kann ein A
         Wir haben einen Code an %1$s gesendet. Gib diesen unten ein um dich zu verifizieren.
         Code eingeben
         Erneut senden
    -    Benutzername oder E-Mail-Adresse
    +    Benutzername oder E-Mail-Adresse
         Passwort
         Dieser Benutzername ist bereits belegt
         Dein Benutzerkonto ist noch nicht erstellt.
    diff --git a/vector/src/main/res/values-es/strings.xml b/vector/src/main/res/values-es/strings.xml
    index 48639968d0..9ae69b7e52 100644
    --- a/vector/src/main/res/values-es/strings.xml
    +++ b/vector/src/main/res/values-es/strings.xml
    @@ -40,7 +40,7 @@
         Debido a que faltan permisos, pueden faltar algunas características…
         Necesitas permiso para invitar a iniciar una conferencia en esta sala
         No se puede iniciar la llamada
    -    Información de dispositivo
    +    Información de la sesión
         No se admiten llamadas de conferencia en salas cifradas
         Enviar de Todos Modos
         o
    @@ -175,8 +175,8 @@ Puedes añadir tu correo electrónico a tu perfil en ajustes.
         Se envió un correo electrónico a %s. Una vez que hayas seguido el enlace que contiene, haz clic a continuación.
         No se pudo verificar la dirección de correo electrónico: asegúrate de hacer clic en el enlace del correo electrónico
         Tu contraseña fue restablecida.
    -
    -Se ha cerrado sesión en todos tus dispositivos y ya no recibirás notificaciones push. Para volver a habilitar las notificaciones, vuelve a iniciar sesión en cada dispositivo.
    +\n
    +\nSe ha cerrado sesión en todas tus sesiones y ya no recibirás notificaciones push. Para volver a habilitar las notificaciones, vuelve a iniciar sesión en cada dispositivo.
     
         
         La URL debe comenzar con http[s]://
    @@ -196,9 +196,9 @@ Se ha cerrado sesión en todos tus dispositivos y ya no recibirás notificacione
         El enlace del correo electrónico que aún no se ha seguido
     
         
    -    Tienes que volver a iniciar sesión para generar claves de cifrado de extremo a extremo para este dispositivo y enviar la clave pública a tu servidor doméstico.
    -Esto es por única vez.
    -Disculpas por la molestia.
    +    Tienes que volver a iniciar sesión para generar claves de cifrado de extremo a extremo para esta sesión y enviar la clave pública a tu servidor doméstico. 
    +\nSolo deberás hacer esto una vez. 
    +\nDisculpa por las molestia.
     
         
         Lista de Recibos de Lectura
    @@ -313,7 +313,7 @@ Quizás quieras iniciar sesión con otra cuenta, o añadir este correo electrón
         HERRAMIENTAS DE ADMINISTRACIÓN
         LLAMAR
         CONVERSACIONES DIRECTAS
    -    DISPOSITIVOS
    +    SESIONES
     
         Invitar
         Salir de esta sala
    @@ -327,7 +327,7 @@ Quizás quieras iniciar sesión con otra cuenta, o añadir este correo electrón
         Mostrar todos los mensajes de este usuario
         ID de Usuario, Nombre o correo electrónico
         Mencionar
    -    Mostrar Lista de Dispositivos
    +    Mostrar Lista de Sesiones
         No podrás deshacer este cambio porque estás promoviendo al usuario para tener el mismo nivel de autoridad que tú.
     ¿Estás seguro?
     
    @@ -351,7 +351,7 @@ Quizás quieras iniciar sesión con otra cuenta, o añadir este correo electrón
         Enviar un mensaje (sin cifrar)…
         Se perdió la conexión con el servidor.
         Los mensajes no se enviaron. ¿%1$s o %2$s ahora?
    -    Los mensajes no se enviaron debido a la presencia de dispositivos desconocidos. ¿%1$s o %2$s ahora?
    +    Los mensajes no se enviaron debido a la presencia de sesiones desconocidas. ¿%1$s o %2$s ahora\?
         Reenviar todo
         Cancelar todo
         Reenviar mensajes no enviados
    @@ -443,7 +443,7 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de
         Información de la aplicación
     
         Habilitar notificaciones para esta cuenta
    -    Habilitar notificaciones para este dispositivo
    +    Habilitar notificaciones para esta sesión
         Enciende la pantalla por 3 segundos
     
         Mensajes en conversaciones uno a uno
    @@ -455,7 +455,7 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de
         Sincronización en segundo plano
         Habilitar sincronización en segundo plano
         Venció el tiempo de espera para la solicitud de sincronización
    -    Retraso entre las solicitudes de sincronización
    +    Retraso entre cada sincronización
         segundo
         segundos
     
    @@ -482,10 +482,10 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de
         Pantalla de inicio
         Fijar salas con notificaciones pendientes
         Fijar salas con mensajes no leídos
    -    Dispositivos
    -    Detalles del dispositivo
    +    Sesiones
    +    Detalles de la sesión
         ID
    -    Nombre
    +    Nombre Público
         Actualizar Nombre Público
         Visto por última vez
         %1$s @ %2$s
    @@ -579,8 +579,8 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de
         Cifrado de Extremo a Extremo
         El Cifrado de Extremo a Extremo está activo
         Necesitas cerrar sesión para poder habilitar el cifrado.
    -    Cifrar solo a dispositivos verificados
    -    Nunca enviar mensajes cifrados a dispositivos sin verificar en esta sala desde este dispositivo.
    +    Cifrar solo a sesiones verificadas
    +    Nunca enviar mensajes cifrados a sesiones sin verificar en esta sala desde esta sesión.
     
         
         Esta sala no tiene direcciones locales
    @@ -618,11 +618,11 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de
         ID de Sesión
         Error de descifrado
     
    -    Información del dispositivo emisor
    -    Nombre de dispositivo
    -    Nombre
    +    Información de la sesión emisora
    +    Nombre público
    +    Nombre público
         ID
    -    Clave de dispositivo
    +    Clave de sesión
         Verificación
         Huella digital Ed25519
     
    @@ -640,14 +640,14 @@ Advertencia: este archivo puede ser eliminado si la aplicación se desinstala.Importar claves de sala
         Importar las claves desde un archivo local
         Importar
    -    Cifrar solo a dispositivos verificados
    -    Nunca enviar mensajes cifrados a dispositivos sin verificar desde este dispositivo.
    +    Cifrar solo a sesiones verificadas
    +    Nunca enviar mensajes cifrados a sesiones sin verificar desde esta sesión.
     
         SIN Verificar
         Verificado
         Prohibido
     
    -    dispositivo desconocido
    +    sesión desconocida
         ninguno
     
         Verificar
    @@ -655,9 +655,9 @@ Advertencia: este archivo puede ser eliminado si la aplicación se desinstala.Prohibir
         Dejar de Prohibir
     
    -    Verificar dispositivo
    -    Para verificar que este dispositivo es confiable, por favor contacta a su dueño por algún otro medio (ej. cara a cara o por teléfono) y pregúntale si la clave que ve en sus Ajustes de Usuario para este dispositivo coincide con la clave a continuación:
    -    Si coincide, presione el botón de verificar a continuación. Si no coincide, entonces alguien está interceptando este dispositivo y probablemente debería prohibirlo. En el futuro, este proceso de verificación será más sofisticado.
    +    Verificar sesión
    +    Para verificar que esta sesión es confiable, por favor contacta a su dueño por algún otro medio (ej. cara a cara o por teléfono) y pregúntale si la clave que ve en sus Ajustes de Usuario para esta sesión coincide con la clave a continuación:
    +    Si coincide, presione el botón de verificar a continuación. Si no coincide, entonces alguien está interceptando esta sesión y probablemente debería prohibirlo. En el futuro, este proceso de verificación será más sofisticado.
         Verifico que las claves coinciden
     
         Riot ahora admite cifrado de extremo a extremo pero debes volver a iniciar sesión para habilitarlo.
    @@ -665,12 +665,8 @@ Advertencia: este archivo puede ser eliminado si la aplicación se desinstala.
     
         
    -    La sala contiene dispositivos desconocidos
    -    Esta sala contiene dispositivos desconocidos que no han sido verificados.
    -Esto significa que no hay garantía de que los dispositivos pertenezcan a los usuarios a los que dicen pertenecer.
    -Recomendamos que pases por el proceso de verificación para cada dispositivo antes de continuar, pero puedes reenviar el mensaje sin verificarlos si prefieres.
    -
    -Dispositivos desconocidos:
    +    La sala contiene sesiones desconocidas
    +    Esta sala contiene sesiones desconocidas que no han sido verificados. Esto significa que no hay garantía de que las sesiones pertenezcan a los usuarios a los que dicen pertenecer. Recomendamos que pases por el proceso de verificación para cada sesión antes de continuar. Puedes reenviar el mensaje sin verificarlas si prefieres. Sesiones desconocidas:
     
         
         Selecciona un directorio de salas
    @@ -757,8 +753,8 @@ Dispositivos desconocidos:
         Utilizar cámara nativa
     
         
    -    Añadiste un nuevo dispositivo \'%s\', que está solicitando claves de cifrado.
    -    Tu dispositivo sin verificar \'%s\' está solicitando claves de cifrado.
    +    Añadiste una nueva sesión \'%s\', que está solicitando claves de cifrado.
    +    Tu sesión sin verificar \'%s\' está solicitando claves de cifrado.
         Iniciar verificación
         Compartir sin verificar
         Ignorar solicitud
    @@ -947,7 +943,7 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu
         Falta un parámetro requerido.
         Solicitud enviada
         Conversar
    -    Por favor, inicia Riot en otro dispositivo que pueda descifrar el mensaje para que pueda enviar las claves a este dispositivo.
    +    Por favor, inicia Riot en otro dispositivo que pueda descifrar el mensaje para que pueda enviar las claves a esta sesión.
     
         Licencias de terceros
     
    @@ -955,7 +951,7 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu
         continuar con…
         Lo sentimos, no se encontró ninguna aplicación externa para completar esta acción.
     
    -    Volver a solicitar las claves de cifrado de tus otros dispositivos.
    +    Volver a solicitar las claves de cifrado de tus otras sesiones.
     
         Solicitud de clave enviada.
     
    @@ -1127,9 +1123,9 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de
     \nPor favor comprueba los ajustes de cuenta.
         Activar
     
    -    Ajustes de dispositivo.
    -    Las notificaciones están activadas para este dispositivo.
    -    Las notificaciones no están permitidos para este dispositivo.
    +    Ajustes de sesión.
    +    Las notificaciones están activadas para esta sesión.
    +    Las notificaciones no están habilitadas para esta sesión. 
     \nPor favor comprueba los ajustes Riot.
         Activar
     
    @@ -1149,7 +1145,7 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de
         Una o más pruebas han fallado, por favor mándanos un informe de error para que podamos investigar.
     
         Copia de seguridad en progreso. Si cierras sesión ahora perderás el acceso a tus mensajes encriptados.
    -    La copia de seguridad debería estar activa ahora en todos tus dispositivos para evitar la pérdida del acceso a tus mensajes encriptados.
    +    La copia de seguridad debería estar activa ahora en todas tus sesiones para evitar la pérdida del acceso a tus mensajes encriptados.
         Riot usa los servicios de Google Play para entregar mensajes Push pero no parece estar configurado correctamente:
     \n%1$s
         solucionar error con los Servicios de Google Play
    @@ -1203,7 +1199,7 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de
         Comprobar restricciones en segundo plano
         Las restricciones de segundo plano están desactivadas para Riot. Este debería funcionar con datos móviles (sin WIFI).
     \n%1$s
    -    Las restricciones de segundo plano estan activadas para Riot.
    +    Las restricciones de segundo plano están activadas para Riot.
     \nLa app estará completamente restringida mientras esté en segundo plano y esto podría afectar a las notificaciones.
     \n%1$s
         Desactivar restricciones
    @@ -1233,7 +1229,7 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de
         Mostrar notificaciones de la cuenta
         Incluye cambios en el avatar y en el nombre.
         Enviar mensaje con intro
    -    La tecla Intro enviará el mensaje en vez de añadir un salto de linea
    +    La tecla Intro enviará el mensaje en vez de añadir un salto de línea
     
         Conexión en segundo plano
         Riot necesita mantener una leve conexión en segundo plano para poder ofrecer notificaciones de confianza.
    @@ -1288,7 +1284,7 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de
     
         Silencioso
         Por favor introduzca un nombre de usuario.
    -    Mostrar el area de información
    +    Mostrar el área de información
         Siempre
         Para mensajes y errores
         Solo para errores
    @@ -1300,7 +1296,7 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de
         No se ha encontrado ningún APK válido de Servicios de Google Play. Las notificaciones podrían no funcionar correctamente.
     
         Riot.im - Comunicate a tu manera
    -    Siempre estamos haciendo cambios y mejoras para Riot.im. Puedes encontrar el registro de cambios aqui: %1$s. Para asegurarte de que no te pierdes nada, tan solo mantén las actualizaciones activadas.
    +    Siempre estamos haciendo cambios y mejoras para Riot.im. Puedes encontrar el registro de cambios aquí: %1$s. Para asegurarte de que no te pierdes nada, tan solo mantén las actualizaciones activadas.
         Una app de chat universal completamente bajo tu control.
         Una app de chat, bajo tu total control y completamente flexible. Riot deja que te comuniques de la manera que prefieras. Hecho para [matrix] - el estándar para la comunicación abierta y descentralizada.
     \nConsigue tu cuenta de matrix.org gratis. Consigue tu servidor en https://modular.im o usa otro servidor de Matrix.
    @@ -1385,14 +1381,14 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de
         La copia de seguridad no se ha podido descifrar con esta contraseña: por favor verificar que has introducido la contraseña de recuperación correcta.
     
         Copia de seguridad restaurada %s !
    -    Se han restaurado %1$d claves de sesión y se han añadido %2$d nuevas claves que este dispositivo no conocía
    +    Se han restaurado %1$d claves de sesión y se han añadido %2$d nuevas claves que esta sesión no conocía
         
             Copia de seguridad restaurada con la clave %d.
             Copia de seguridad restaurada con las claves %d.
         
         
    -        Se ha añadido %d como clave en este dispositivo.
    -        Se han añadido %d como claves en este dispositivo.
    +        Se ha añadido %d como clave a esta sesión.
    +        Se han añadido %d como claves a esta sesión.
         
     
         Error al recuperar la ultima versión de las claves (%s).
    @@ -1402,13 +1398,13 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de
         Restaurada desde copia de seguridad
         Borrar copia de seguridad
     
    -    La copia de seguridad ha sido correctamente activada para este dispositivo.
    -    La copia de seguridad ha sido correctamente desactivada para este dispositivo.
    -    Tus claves no están siendo guardadas en este dispositivo.
    +    La copia de seguridad ha sido correctamente activada para esta sesión.
    +    La copia de seguridad ha sido correctamente desactivada para esta sesión.
    +    Tus claves no están siendo guardadas en esta sesión.
     
    -    La copia de seguridad tiene una firma de un dispositivo desconocido con el ID %s.
    -    La copia de seguridad tiene una firma valida de este dispositivo.
    -    La copia de seguridad tiene tiene una firma válida para el dispositivo verificado %s.
    +    La copia de seguridad tiene una firma de una sesión desconocida con el ID %s.
    +    La copia de seguridad tiene una firma valida de esta sesión.
    +    La copia de seguridad tiene una firma válida para la sesión verificada %s.
         Usar copia de seguridad de la clave
     
         Todas las claves guardadas
    @@ -1427,12 +1423,12 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de
         Origen predeterminado de medios
         Configurar copia de seguridad de las claves de cifrado
         Obteniendo una versión de copia de seguridad…
    -    La copia de seguridad tiene una firma valida del dispositivo no verificado %s
    -    La copia de seguridad tiene una forma inválida del dispositivo verificado %s
    -    La copia de seguridad tiene una firma inválida del dispositivo no verificado %s
    +    La copia de seguridad tiene una firma valida de la sesión no verificada %s
    +    La copia de seguridad tiene una firma inválida de la sesión verificada %s
    +    La copia de seguridad tiene una firma inválida de la sesión no verificada %s
         Error al conseguir información de confianza para la copia de seguridad (%s).
     
    -    Para usar la copia de seguridad de la clave en este dispositivo introduzca su contraseña o su clave de recuperación ahora.
    +    Para usar la copia de seguridad de la clave en esta sesión introduzca su contraseña o su clave de recuperación ahora.
         Desea borrar sus claves cifradas guardadas del servidor\? No podrás usar tu clave de recuperación para leer el historial de mensajes cifrados.
     
         Una nueva copia de seguridad de mensajes ha sido detectada.
    @@ -1441,17 +1437,17 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de
         Respuesta inválida del descubrimiento del servidor doméstico
         Reproducir sonido de cámara
     
    -    Verificar dispositivo
    +    Verificar sesión
     
         ip desconocida
    -    Un nuevo dispositivo solicita claves de cifrado.
    -\nDispositivo: %1$s
    -\nVisto por última vez: %2$s
    -\nSi no inicias sesión en otros dispositivos ignora esta solicitud.
    -    Un dispositivo no verificado solicita claves de cifrado.
    -\nDispositivo: %1$s
    -\nVisto por última vez: %2$s
    -\nSi no inicias sesión en otros dispositivos ignora esta solicitud.
    +    Una nueva sesión solicita claves de cifrado. 
    +\nSesión: %1$s 
    +\nVisto por última vez: %2$s 
    +\nSi no has iniciado sesión en otro dispositivo ignora esta solicitud.
    +    Una sesión no verificada solicita claves de cifrado. 
    +\nSesión: %1$s 
    +\nVisto por última vez: %2$s 
    +\nSi no has iniciado sesión en otro dispositivo ignora esta solicitud.
     
         Verificar
         Compartir
    @@ -1459,7 +1455,7 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de
         Ignorar
     
         Ya existe una copia de respaldo en tu servidor
    -    Parece que ya habías configurado una clave para copias de respaldo en otro dispositivo. ¿Quieres reemplazarla por la nueva que has creado\?
    +    Parece que ya habías configurado una copia de seguridad para las claves en otra sesión. ¿Quieres reemplazarla por la nueva que has creado\?
         Reemplazar
         Parar
     
    @@ -1470,18 +1466,18 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de
         Para más seguridad, te recomendamos que hagas esto en persona o por otros medios confiables.
         Empezar verificación
         Solicitud de verificación
    -    Verifica este dispositivo para marcarlo como confiable. Confiar en dispositivos de otros te da aún más tranquilidad cuando usas cifrado punto a punto de mensajes.
    -    Verificar este dispositivo lo marcará como confiable, y también marcará como confiable tu dispositivo para la contraparte.
    +    Verifica esta sesión para marcarla como confiable. Confiar en sesiones de otros te da aún más tranquilidad cuando usas cifrado de mensajes de punto a punto.
    +    Verificar esta sesión la marcará como confiable, y también marcará como confiable tu sesión para la contraparte.
     
    -    Verifica este dispositivo confirmando los emojis que aparecen en la pantalla de tu compañero
    -    Verifica este dispositivo confirmando que los siguietes números aparecen en la pantalla de la contraparte
    +    Verifica esta sesión confirmando los emojis que aparecen en la pantalla de la contraparte
    +    Verifica esta sesión confirmando que los siguietes números aparecen en la pantalla de la contraparte
     
         Se ha recibido una solicitud de verificación.
         Ver solicitud
         Esperando confirmación de la contraparte…
     
         ¡Verificado!
    -    Has verificado correctamente este dispositivo.
    +    Has verificado correctamente esta sesión.
         Los mensajes con este usuario están cifrados punto a punto y no son legible por terceros.
         Ok
     
    @@ -1495,13 +1491,13 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de
         La verificación ha sido cancelada.
     \nRazón: %s
     
    -    Verificación de dispositivo interactiva
    +    Verificación de sesión interactiva
         Solicitud de verificación
    -    %s quiere verificar tu dispositivo
    +    %s quiere verificar tu sesión
     
         El usuario canceló la verificación
         La verificación ha superado el límite de espera
    -    El dispositivo recibió un mensaje inesperado
    +    La sesión recibió un mensaje inesperado
         Se ha recibido un mensaje inválido
         Error en clave
         Error en usuario
    @@ -1519,7 +1515,7 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de
         No tienes más mensajes sin leer
         ¡Bienvenido!
         Conversaciones
    -    Tus conversaciones 1 a 1 se mostrarán aquí
    +    Tus conversaciones directas (1 a 1) se mostrarán aquí
         Salas
         Tus salas se mostrarán aquí
     
    @@ -1608,4 +1604,278 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de
         No se puede acceder al servidor en esta URL, por favor, compruébelo
         El SAS no coincidió
         ¡Bienvenido a la beta!
    +    Pon un número de teléfono para que las personas que conoces te puedan encontrar.
    +    Se usará %s como asistencia cuando el servidor doméstico no la ofrezca (su dirección IP se compartirá durante una llamada)
    +    Modo sincronización en segundo plano (Experimental)
    +    Riot se sincronizará en segundo plano de manera que se preserven los recursos del dispositivo (batería).
    +\nDependiendo del estado de los recursos del dispositivo, la sincronización puede ser aplazada por el sistema operativo.
    +    Riot se sincronizará en segundo plano periódicamente en un momento preciso (configurable).
    +\nEsto afectará al uso de la radio y la batería, se mostrará una notificación permanente que indica que Riot está escuchando a nuevos acontecimientos.
    +    No se le notificará de los mensajes entrantes cuando la aplicación esté en segundo plano.
    +    Intervalo de sincronización preferido
    +    %s
    +\nLa sincronización puede ser aplazada dependiendo de los recursos (batería) o del estado del dispositivo (dormido).
    +    Utiliza un Gestor de Integración para gestionar los bots, puentes, widgets y paquetes de pegatinas.
    +\nLos Gestores de Integración reciben los datos de configuración y pueden modificar los widgets, enviar invitaciones a salas y establecer niveles de poder en su nombre.
    +    Permitir integraciones
    +    Adiministrador de integraciones
    +
    +    Nombre público (visible por las personas con quien te comuniques)
    +    Un nombre de sesión público es visible por las personas con quién te comunicas
    +    Widget
    +    Cargar Widget
    +    Este widget ha sido añadido por:
    +    Utilizarlo puede establecer cookies y compartir datos con: %s:
    +    Utilizarlo puede compartir datos con %s:
    +    Error al cargar el widget.
    +\n%s
    +    Recargar widget
    +    Abrir en el navegador
    +    Revocar acceso para mi
    +
    +    Tu nombre visible
    +    La URL de tu avatar
    +    Tu ID de usuaria
    +    Tu tema
    +    ID de Widget
    +    ID de Sala
    +
    +
    +    Este widget quiere usar los siguientes recursos:
    +    Permitir
    +    Bloquear todos
    +    Usar la cámara
    +    Usar el micrófono
    +    Leer medios protegidos por DRM
    +
    +    No se ha configurado ningún administrador de integraciones.
    +    Para continuar es necesario que aceptes los Términos de este servicio.
    +
    +    La sesión no sabe nada de esa transacción
    +    La sesión no puede acordar el acuerdo de llaves, hash, MAC o método SAS
    +    El compromiso hash no ha coincidido
    +    No estás usando ningún Servidor de Identidad
    +    No hay ningún Servidor de Identidad configurado, esto es requerido para restablecer tu contraseña.
    +
    +    Las versiones anteriores de Riot tenían un error de seguridad que podía dar a su Servidor de Identidad (%1$s) acceso a su cuenta. Si confías en %2$s, puedes ignorar esto; de lo contrario, por favor cierra la sesión y vuelve a entrar.
    +\n
    +\nLee más detalles aquí:
    +\nhttps://medium.com/@RiotChat/36b4792ea0d6
    +
    +    Parece que estás intentando conectarte a otro servidor doméstico. ¿Quieres cerrar sesión\?
    +
    +    La última lista de características está siempre en %1$s, y si encuentras errores por favor envía un informe des del menú superior izquierdo de Inicio, y los arreglaremos tan rápido como podamos.
    +    Descripción de Play Store
    +    Si encuentras errores por favor envía un informe de errores des del menú superior izquierdo del menú de Inicio. Resolveremos el error tan rápido como podamos.
    +
    +    Importar llaves E2E des del fichero \"%1$s\".
    +
    +    Versión del SDK de Matrix
    +    Otros avisos de terceros
    +    ¡Ya estas viendo esta sala!
    +
    +    Reacciones rápidas
    +
    +    General
    +    Experto
    +    Reglas Push
    +    No hay reglas push definidas
    +    No hay salidas push registradas
    +
    +    app_id:
    +    push_key:
    +    app_display_name:
    +    Url:
    +    Formato:
    +
    +    Ayuda y Acerca de
    +
    +
    +    Registrar token
    +
    +    Gracias, la sugerencia ha sido enviada correctamente
    +    El envio de la sugerencia ha fallado (%s)
    +
    +    Mostrar eventos ocultos en la línea de tiempo
    +
    +    RiotX - Cliente Matrix de Futura Generación
    +    Un cliente para Matrix más rápido y ligero que usa los últimos frameworks de Android
    +    RiotX es un nuevo cliente del protocolo Matrix (Matrix.org): una red abierta para una comunicación segura y descentralizada. RiotX es una reescritura completa del cliente Riot Android, basada en una reescritura completa del SDK Matrix Android.
    +\n
    +\nDescargo de responsabilidad: Esta es una versión beta. RiotX está actualmente en desarrollo activo y contiene limitaciones y (esperamos que no demasiados) errores. Todas las sugerencias son bienvenidas!
    +\n
    +\nRiotX soporta: - Entrar en una cuenta existente - Crear una sala y unirse a las salas públicas - Aceptar y rechazar invitaciones - Listar las salas de los usuarios - Ver los detalles de la sala - Enviar mensajes de texto - Enviar archivos adjuntos - Leer y escribir mensajes en salas cifradas - Criptografía: Copia de seguridad de claves E2E, verificación avanzada de dispositivos, solicitud y respuesta de compartir claves - Notificación push - Temas claros, oscuros y negros
    +\n
    +\nNo todas las características de Riot están implementadas en RiotX todavía. Principales características que faltan (¡pronto disponibles!): - Configuración de la sala (lista de miembros de la sala, etc.) - Llamadas - Widgets - …
    +
    +    Mensajes Directos
    +
    +    Esperando…
    +    Cifrando la miniatura…
    +    Enviando miniatura (%1$s / %2$s)
    +    Cifrando el archivo…
    +    Enviando el archivo (%1$s / %2$s)
    +
    +    Descargando archivo %1$s…
    +    ¡El archivo %1$s ha sido descargado!
    +
    +    (editado)
    +
    +    %1$s para crear una cuenta.
    +    Usar la app antigua
    +
    +
    +    Modificación de mensajes
    +    No se han encontrado modificaciones
    +
    +    Filtrar conversaciones…
    +    ¿No encuentras lo que buscas\?
    +    Crear una nueva sala
    +    Enviar un nuevo mensaje directo
    +    Ver el directorio de la sala
    +
    +    Nombre o ID (#ejemplo:servidor.org)
    +
    +    Habilitar \"desplazar para contestar\" en la línea de tiempo
    +
    +    Enlace copiado al portapapeles
    +
    +    Agregar por ID de matrix
    +    Creando sala…
    +    No se ha encontrado ningún resultado, utiliza \"Agregar usando ID de matrix\" para buscar en el servidor.
    +    Empieza a escribir para ver resultados
    +    Filtrar por usuario o ID…
    +
    +    Entrando en la sala…
    +
    +    Ver historial de modificaciones
    +
    +    Términos de Servicio
    +    Revisar Términos
    +    Ser descubierta por otros
    +    Utiliza Bots, puentes, widgets y packs de stickers
    +
    +    Leer en
    +
    +
    +    Servidor de identidad
    +    Desconectar servidor de identidad
    +    Configurar servidor de identidad
    +    Cambiar servidor de identidad
    +    Estas usando %1$s actualmente para buscar y ser encontrado por contactos existentes que conoces.
    +    Actualmente no estás usando ningún servidor de identidad. Para buscar y ser encontrado por contactos que conoces, configura uno abajo.
    +    Dirección de correo para ser descubierto
    +    Las opciones para ser descubierto aparecerán una vez hayas añadido un correo.
    +    Las opciones para ser descubierta aparecerán una vez hayas añadido un número de teléfono.
    +    Desconectarse del servidor de identidad significará que no serás descubrible por otros usuarios y no podrás invitar a otros a través del correo o teléfono.
    +    Teléfonos para ser descubierto
    +    Te hemos enviado un correo de confirmación a %s, comprueba tu correo y haz click en el enlace de confirmación
    +    Pendiente
    +
    +    Entra en un nuevo servidor de identidad
    +    No se ha podido conectar al servidor de identidad
    +    Porfavor entra la url del servidor de identidad
    +    El servidor de identidad no tiene términos de servicio
    +    El servidor de identidad que has escojido no tiene términos de servicio. Solo continúa si confias en el propietario del servicio
    +    Un mensaje de texto ha sido enviado a %s. Porfavor escribe el código de verificación que contiene.
    +
    +    Actualmente estás compartiendo direcciones de correo electrónico o números de teléfono en el servidor de identidad %1$s. Necesitarás reconectarte a %2$s para dejar de compartirlos.
    +    Acepte los términos de servicio del servidor de identidad (%s) para permitir que sea descubierto por correo electrónico o número de teléfono.
    +
    +    Habilitar registros extensos.
    +    Los logs extensivos ayudarán a los desarrolladores proporcionando más información cuando envíes un \"RageShake\" (Sacudir el dispositivo). Incluso cuando esto está habilitado, la aplicación no registra el contenido de los mensajes ni ningún otro dato privado.
    +
    +
    +    Por favor, vuelva a intentarlo una vez que haya aceptado los términos y condiciones de su servidor.
    +
    +    Parece que el servidor está tardando demasiado en responder, esto puede ser causado por una mala conectividad o un error con el servidor. Por favor, inténtelo de nuevo en un rato.
    +
    +    Enviar el archivo adjunto
    +
    +    Abrir el cajón de navegación
    +    Abrir el menú de creación de sala
    +    Cerrar el menú de creación de sala…
    +    Crear una nueva conversación directa
    +    Crear una nueva sala
    +    Cerrar el banner de copia de seguridad de las llaves
    +    Mostrar contraseña
    +    Esconder contraseña
    +    Saltar al final
    +
    +    Leído por %1$s, %2$s, %3$d y otros
    +    Leído por %1$s, %2$s y %3$s
    +    Leído por %1$s y %2$s
    +    Leído por %s
    +    
    +        Leído por 1 usuario
    +        Leído por %d usuarias
    +    
    +
    +    El archivo \'%1$s\' (%2$s) es demasiado grande para ser subido. El límite es %3$s.
    +
    +    Ha ocurrido un error recuperando el archivo adjunto.
    +    Archivo
    +    Contacto
    +    Cámara
    +    Audio
    +    Galería
    +    Pegatina
    +    No se han podido compartir los datos
    +
    +    Es spam
    +    Es inapropiado
    +    Reporte personalizado…
    +    Reportar contenido
    +    Razón por la que se ha reportado el contenido
    +    REPORTAR
    +    BLOQUEAR USUARIO
    +
    +    Contenido reportado
    +    El contenido ha sido reportado
    +\n
    +\nSi no quieres ver más contenido de este usuario, puedes bloquearlo para ocultar sus mensajes
    +    Reportar como spam
    +    Este contenido ha sido reportado como spam.
    +\n
    +\nSi no quieres ver más contenido de este usuario, puedes bloquearlo para ocultar sus mensajes
    +    Reportado como inapropiado
    +    Este contenido fue reportado como inapropiado.
    +\n
    +\nSi no quieres ver más contenido de este usuario, puedes bloquearlo para ocultar sus mensajes
    +
    +    Riot necesita permiso para guardar tus claves E2E en la memória del dispositivo.
    +\n
    +\nPorfavor permite el acceso en el siguiente pop-up para poder exportar tus claves manualmente.
    +
    +    No hay conexión de red
    +
    +    Bloquear usuario
    +
    +    Todos los mensajes (sonido)
    +    Todos los mensajes
    +    Solo menciones
    +    Mutear
    +    Ajustes
    +    Abandonar sala
    +    %1$s no ha hecho ningún cambio
    +    Envía el mensaje como spoiler
    +    Spoiler
    +    Escribe las palabras clave para encontrar una reacción.
    +
    +    No hay usuarios ignorados
    +
    +    Mantén pulsada una sala para ver más opciones
    +
    +
    +    %1$s ha hecho la sala pública para cualquier persona con el link.
    +    %1$s: Ahora la sala solo es accesible por invitación.
    +    Mensajes no leídos
    +
    +    Libera tu comunicación
    +    Envía mensajes a personas o grupos
    +    Mantén las conversaciones privadas con encriptación
    +    Extiende y personaliza tu experiencia
    +    Empieza
    +
    +    Selecciona un servidor
     
    diff --git a/vector/src/main/res/values-eu/strings.xml b/vector/src/main/res/values-eu/strings.xml
    index 91432ac66e..86fb1e0e12 100644
    --- a/vector/src/main/res/values-eu/strings.xml
    +++ b/vector/src/main/res/values-eu/strings.xml
    @@ -1909,7 +1909,7 @@ Abisua: Fitxategi hau ezabatu daiteke aplikazioa desinstalatzen bada.
         Telefono zenbakia baliogabea dirudi. Egiaztatu ezazu
     
         Erregistratu %1$s zerbitzarian
    -    Erabiltzaile-izena edo e-maila
    +    Erabiltzaile-izena edo e-maila
         Pasahitza
         Hurrengoa
         Erabiltzaile-izen hori hartuta dago
    diff --git a/vector/src/main/res/values-fi/strings.xml b/vector/src/main/res/values-fi/strings.xml
    index 4966a5ea6a..fef747d546 100644
    --- a/vector/src/main/res/values-fi/strings.xml
    +++ b/vector/src/main/res/values-fi/strings.xml
    @@ -1962,7 +1962,7 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös
         Puhelinnumero vaikuttaa epäkelvolta. Tarkista numero
     
         Rekisteröidy palvelimelle %1$s
    -    Käyttäjätunnus tai sähköpostiosoite
    +    Käyttäjätunnus tai sähköpostiosoite
         Salasana
         Seuraava
         Käyttäjätunnus on varattu
    diff --git a/vector/src/main/res/values-fr/strings.xml b/vector/src/main/res/values-fr/strings.xml
    index 86547b02fd..341a247d52 100644
    --- a/vector/src/main/res/values-fr/strings.xml
    +++ b/vector/src/main/res/values-fr/strings.xml
    @@ -766,7 +766,7 @@ Vous pouvez le faire maintenant ou plus tard à partir des paramètres de l’ap
         Accueil
         Personnes
         Salons
    -    Pas d’utilisateurs
    +    Pas d’utilisateur
     
         Salons
         Rejoint
    @@ -1835,7 +1835,7 @@ Si vous n’avez pas configuré de nouvelle méthode de récupération, un attaq
         Démarrer
     
         Sélectionner un serveur
    -    Comme les e-mails, les comptes ont une maison, même si vous pouvez parler à n’importe qui
    +    Comme les e-mails, les comptes ont un serveur d\'accueil, même si vous pouvez parler à tout le monde
         Rejoignez des millions de personnes gratuitement sur le plus grand serveur public
         Hébergement premium pour les organisations
         En savoir plus
    @@ -1918,7 +1918,7 @@ Si vous n’avez pas configuré de nouvelle méthode de récupération, un attaq
         Le numéro de téléphone n’a pas l’air d’être valide. Veuillez le vérifier
     
         S’inscrire sur %1$s
    -    Nom d’utilisateur ou e-mail
    +    Nom d’utilisateur ou e-mail
         Mot de passe
         Suivant
         Ce nom d’utilisateur est déjà pris
    diff --git a/vector/src/main/res/values-hu/strings.xml b/vector/src/main/res/values-hu/strings.xml
    index 4598e445fa..64eee79075 100644
    --- a/vector/src/main/res/values-hu/strings.xml
    +++ b/vector/src/main/res/values-hu/strings.xml
    @@ -1913,7 +1913,7 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró
         A telefonszám érvénytelennek látszik. Kérlek ellenőrizd
     
         Bejelentkezés ide: %1$s
    -    Felhasználónév vagy e-mail
    +    Felhasználónév vagy e-mail
         Jelszó
         Következő
         A felhasználónév már használatban van
    diff --git a/vector/src/main/res/values-it/strings.xml b/vector/src/main/res/values-it/strings.xml
    index 6c10f7b04f..4d1ac8edaf 100644
    --- a/vector/src/main/res/values-it/strings.xml
    +++ b/vector/src/main/res/values-it/strings.xml
    @@ -1963,7 +1963,7 @@
         Il numero di telefono non sembra valido. Ricontrollalo
     
         Registrati su %1$s
    -    Nome utente o email
    +    Nome utente o email
         Password
         Avanti
         Quel nome utente esiste già
    diff --git a/vector/src/main/res/values-ja/strings.xml b/vector/src/main/res/values-ja/strings.xml
    index b52edc0df5..c44c400208 100644
    --- a/vector/src/main/res/values-ja/strings.xml
    +++ b/vector/src/main/res/values-ja/strings.xml
    @@ -218,7 +218,7 @@
         端末詳細
         ID(端末固有番号)
         公開端末名
    -    端末名
    +    公開端末名の更新
         最終接続日
         %1$s @ %2$s
         認証
    diff --git a/vector/src/main/res/values-nn/strings.xml b/vector/src/main/res/values-nn/strings.xml
    index 1a7331efa4..44755a1ef0 100644
    --- a/vector/src/main/res/values-nn/strings.xml
    +++ b/vector/src/main/res/values-nn/strings.xml
    @@ -3,12 +3,12 @@
         nn
         NO
     
    -    Ljost preg
    -    Dimt preg
    -    Svart preg
    +    Lyst tema
    +    Mørkt tema
    +    Svart tema
     
    -    Samstiller
    -    Høyrer etter hendingar
    +    Synkroniserar…
    +    Lyttar etter hendingar
         Bråkete varsel
         Stille varsel
     
    @@ -16,118 +16,119 @@
         Rom
         Innstillingar
         Medlemdetaljar
    -    Sogalege
    -    Feil-fråmelding
    -    Samfundsdetaljar
    +    Historiske
    +    Feilrapport
    +    Felleskapsdetaljar
         Send eit klistremerke
     
         Tredjepartilisensar
     
         Lastar…
     
    -    Greidt
    -    Brjot av
    +    Greitt
    +    Avbryt
         Lagra
    -    Far frå
    +    Forlat
         Send
    -    Rit av
    +    Kopier
         Send på nytt
    -    Trekk attende
    -    Herm
    +    Fjern
    +    Siter
         Last ned
    -    Deil
    -    Tal
    +    Del
    +    Snakk
         Tøm
         Seinare
         Send vidare
    -    Varande lenk
    -    Sjå kjelda
    -    Sjå den avkrypterte kjelda
    -    Strjuk
    +    Permanent lenkje
    +    Vis kjelde
    +    Vis den dekrypterte kjelda
    +    Slett
         Gje nytt namn
    -    Meld frå um innhaldet
    -    Pågåande samtala
    -    Pågåande gruppesamtale.\nVert med med %1$s eller %2$s.
    -    røyst
    +    Rapporter innhaldet
    +    Pågåande samtale
    +    Pågåande konferanseanrop.
    +\nBli med som %1$s eller %2$s.
    +    Tale
         Video
    -    Kann ikkje få i gang samtala, prøv att seinare
    -    På grunn av vantande løyve, kann nokre funksjonar mangla…
    -    Du treng løyve til å bjoda inn for å setja i gang ei gruppasamtala i detta romet
    -    Klarer ikkje å setja i gang samtala
    -    Einingsinfo
    -    Gruppasamtalor er ikkje stydja i krypterte rom
    +    Kan ikkje starte samtalen, prøv litt seinare
    +    Grunna manglande rettigheiter, kan det hende at funksjonar manglar…
    +    Du treng invitasjonsrett for å starte ein konferanse i dette rommet
    +    Kunne ikkje starte samtalen
    +    Sesjonsinformasjon
    +    Konferanseanrop er ikkje støtta i krypterte rom
         Send likevel
         eller
    -    Bjod inn
    +    Inviter
         Fråkopla
     
         Gå ut
         Handlingar
    -    Logga av
    -    Røystsamtala
    -    Videosamtala
    -    Søk i alt
    +    Logg av
    +    Taleanrop
    +    Videoanrop
    +    Globalt søk
         Merk alle som lesne
    -    Sogalege
    -    Snøggsvara
    +    Historisk
    +    Hurtigsvar
         Opna
         Lat att
    -    Rita av til utklippstavla
    +    Kopier til utklippstavla
         Skru av
     
         Stadfesting
         Åtvaring
     
         Heim
    -    Yndlingar
    +    Favorittar
         Folk
         Rom
         Samfund
     
         Filtrer romnamn
    -    Filtrer yndlingar
    +    Filtrer favorittar
         Filtrer folk
         Filtrer romnamn
    -    Filtrer samfund
    +    Filtrer felleskap
     
    -    Innbjodingar
    -    Lågrett
    +    Invitasjonar
    +    Låg prioritering
     
    -    Samtalor
    +    Samtalar
         Lokal adressebok
    -    Brukarutval
    +    Brukarkatalog
         Berre Matrix-kontaktar
    -    Ingi samtalor
    -    Du gav ikkje riot tilgang til dei lokale kontaktane dine
    -    Ingi treff
    +    Ingen samtalar
    +    Du gav ikkje Riot tilgang til dei lokale kontaktane dine
    +    Ingen treff
     
         Rom
    -    Romutval
    -    Ingi rom
    -    Ingi ålmene rom er tilgjengelege
    +    Romkatalog
    +    Ingen rom
    +    Ingen offentelege rom er tilgjengelege
         
             Ein brukar
             %d brukarar
         
     
    -    Bjod inn
    -    Samfund
    -    Ingi gruppor
    +    Inviter
    +    Fellesskap
    +    Ingen grupper
     
         Send loggar
    -    Send kræsjloggar
    -    Send skjermbilæte
    -    Meld frå um feil
    -    Gjer vel og skildra feilen. Kva gjorde du\? Kva venta du at skulde henda\? Kva hende\?
    -    Um mogeleg, ver venleg og skriv skildringi på engelsk.
    -    Skildra problemet ditt her
    -    Fyr å diagnostisera vanskar, vert loggar frå denna einingi sende saman med feilfråmeldingi. Feilfråmeldinga, saman med loggane og skjermbilætet, vil ikkje vera ålment synlege. Um du helder vil senda berre teksti yver, gjer vel og tak burt merket:
    -    Det verkar som om du ristar mobilen i sinne. Vil du senda inn ein feilfråmelding\?
    -    Æppen kræsja fyrre gong. Vil du opna kræsjfråmeldingsvindauget\?
    -    Rista i sinne fyr å melda frå um feil
    +    Send krasjrapportar
    +    Send skjermbilde
    +    Rapporter om feil
    +    Beskriv feilen. Kva gjorde du\? Kva forventa du skulle hende\? Kva hendte\?
    +    Om mogleg, skriv beskrivelsen på engelsk.
    +    Beskriv problemet ditt her
    +    For å diagnostisere feil, vil loggar frå denne klienten bli sendt med i feilrapporten. Denne feilrapporten inklusiv loggar, skjermbilete vil ikkje vere offentleg. Fjern avkryssinga om du berre vil sende teksten ovanfor:
    +    Det verkar som du ristar mobilen i vonbrot. Ønskjer du å opne skjerm for feilrapport \?
    +    Programmet krasja sist gong. Ønskjer du å opne skjerm for feilrapport \?
    +    Rist i sinne for å rapportera feil
     
    -    Feilfråmeldingi vart send
    -    Fekk ikkje til å senda feilfråmeldingi (%s)
    +    Feilrapporten vart sendt
    +    Fekk ikkje til å senda feilrapporten (%s)
         Framgang (%s%%)
     
         Send inn i
    @@ -135,121 +136,121 @@
     
         Gå inn i romet
         Brukarnamn
    -    Laga ein brukar
    -    Logga inn
    -    Logga ut
    +    Lag brukar
    +    Logg inn
    +    Logg ut
         Heimtenar-URL
         Identitetstenar-URL
         Søk
     
    -    Byrja ei ny samtala
    -    Byrja ei røystsamtala
    -    Byrja ei videosamtala
    +    Start ny samtale
    +    Start taleanrop
    +    Start videosamtale
     
    -    Send røyst
    +    Send tale
     
    -    Er du trygg på at du vil byrja ei ny samtala med %s\?
    -    Er du trygg på at du vil byrja ei røystsamtala\?
    -    Er du trygg på at du vil byrja ei videosamtala\?
    +    Er du sikker på at du vil starta ein ny samtale med %s\?
    +    Er du sikker på at du vil starta eit nytt taleanrop\?
    +    Er du sikker på at du vil starta ein ny videosamtale\?
     
         Send filer
         Send klistremerke
    -    Tak eit bilæte eller ein video
    -    Tak eit bilæte
    -    Tak ein video
    +    Ta bilete eller video
    +    Ta bilete
    +    Ta video
     
    -    Du hev fyrebils ingi klistremerkepakkar skrudde på. Legg til nokre no\?
    +    Du har enno ingen klistremerkepakker skrudde på. Legg til nokre no\?
     
         hald fram med…
    -    Orsak, ingi ytre æppar som kunde gjera detta vart funne.
    +    Beklagar, det vart ikkje funne nokon ekstern applikasjon som kan fullføre handlinga.
     
    -    Logga inn
    -    Laga ein brukar
    +    Logg inn
    +    Lag konto
         Send inn
    -    Hoppa yver
    -    Send attendestillingsepost
    -    Gå attende til innloggingsvindauget
    -    Epost eller brukarnamn
    -    Åtgangsord
    -    Nytt åtgangsord
    +    Hopp over
    +    Send tilbakestillings-epost
    +    Gå tilbake til innloggingsvindauget
    +    E-post eller brukarnamn
    +    Passord
    +    Nytt passord
         Brukarnamn
    -    Epostadressa
    -    Epostadressa (valfritt)
    +    E-postadresse
    +    E-postadresse (valfritt)
         Telefonnummer
         Telefonnummer (valfritt)
    -    Skriv upp att åtgangsordet
    -    Stadfest det nye åtgangsordet ditt
    -    Gale brukarnamn og/eller åtgangsord
    -    Brukarnamn kann berre innehalda bokstavar, tal, prikkar, bindestrek og understrek
    -    Åtgangsordet er for kort (i det minste 6 teikn)
    -    Åtgangsordet vantar
    -    Detta ser ikkje ut som ei gangbar epostadressa
    -    Detta ser ikkje ut som eit gangbart telefonnummer
    -    Denna epostadressa er allereie nytta.
    -    Epostadressa vantar
    -    Telefonnummeret vantar
    -    Epostadressa eller telefonnummer vantar
    +    Skriv om att passordet
    +    Stadfest det nye passordet ditt
    +    Feil brukarnamn og/eller passord
    +    Brukarnamn kan berre innehalda bokstavar, tal, prikkar, bindestrek og understrek
    +    Passordet er for kort (i det minste 6 teikn)
    +    Passord manglar
    +    Dette ser ikkje ut som ei gyldig e-postadresse
    +    Dette ser ikkje ut som eit gyldig telefonnummer
    +    Denne e-postadressa er allereie i bruk.
    +    E-postadressa manglar
    +    Telefonnummeret manglar
    +    E-postadressa eller telefonnummer manglar
         Ugyldig teikn
    -    Åtgangsordi er ikkje dei same
    -    Gløymt åtgangsordet\?
    -    Bruk eigentenarinnstillingar (umfattande)
    -    Gjer vel og sjekk eposten din for å halda fram med innmeldingi
    -    Innmelding med epost og telefonnummer samtidig er ikkje stydja fram til api-en finst. Berre telefonnummeret vert nytta.
    -\n
    -\nDu kann knyta eposten din til brukaren din i innstillingane.
    -    Heimtenaren ynskjer å bli trygg på at du ikkje er ein robot
    +    Passorda er ulike
    +    Gløymt passord\?
    +    Bruk tilpassa tenarinnstillingar (avansert)
    +    Ver venleg, sjekk e-posten din for å halda fram med registreringa
    +    Innmelding med epost og telefonnummer samtidig er ikkje støtta før API-et er implementert. Berre telefonnummeret vil bli tatt i bruk.
    +\n 
    +\nDu kan koble e-posten din til brukarprofilen i innstillingane.
    +    Heimtenaren ynskjer å stadfeste at du ikkje er ein robot
         Brukarnamnet er allereie nytta
         Heimtenar:
         Identitetstenar:
    -    Eg hev stadfesta epostdressa mi
    -    For å attendestilla åtgangsordet, skriv inn epostadressa som er knytt til brukaren din:
    -    Du må skriva inn epostadressa som er knytt til brukaren din.
    -    Du må skriva inn eit nytt åtgangsord.
    -    Ein epost vart send til %s. Når du hev fylgt lenken han inneheld, klikk under.
    -    Fekk ikkje til å stadfesta epostadressa: sjå til at du klikka på lenken i eposten
    -    Åtgangsordet ditt vart attendestilt. Du vart logga ut av alle einingar og fær ikkje push-varsel lenger. For å skru varsel på att, logg inn att på kvar eining.
    +    Eg har verifisert e-postadressa mi
    +    For å nullstille passordet, skriv inn e-postadressa som er knytt til brukaren din:
    +    Du må skriva inn e-postadressa som er knytt til brukaren din.
    +    Du må skriva inn eit nytt passord.
    +    Ein e-post vart send til %s. Når du har opna linken den inneheld, klikk under.
    +    Fekk ikkje til å stadfesta e-postadressa: sjå til at du klikka på lenken i e-posten
    +    Passordet ditt vart nullstilt. Du vart logga ut av alle sesjonar og får ikkje push-varsel lenger. For å skru varsel på att, logg inn att på kvar eining.
     
         URL-en må byrja på http[s]://
    -    Fekk ikkje til å logga inn: netverksfeil
    +    Fekk ikkje til å logga inn: Nettverksfeil
         Fekk ikkje til å logga inn
    -    Fekk ikkje til å melda inn: Netverksfeil
    -    Fekk ikkje til å melda inn
    -    Fekk ikkje til å melda inn: feil med eposteigarskapen
    -    Gjer vel og skriv inn ein gangbar URL
    +    Fekk ikkje til å registrere: Nettverksfeil
    +    Fekk ikkje til å registrere
    +    Fekk ikkje til å registrere: feil med e-posteigarskap
    +    Skriv inn ein gyldig URL
         Mobil
     
    -    Brukarnamnet/passordet er ikkje gangbart
    -    Det oppgjevne åtgangsteiknet vart ikkje kjent att
    -    Misforma JSON
    -    Inneheldt ikkje ein gangbar JSON
    -    For mange fyrespurnader vart sende
    +    Brukarnamnet/passordet er ikkje gyldig
    +    Den oppgjevne tilgangsnøkkelen vart ikkje kjent att
    +    Feilformatert JSON
    +    Innehaldt ikkje ein gyldig JSON
    +    For mange førespurnader vart sendt
         Brukarnamnet er allereie i bruk
    -    Epostlenken som ikkje hev verta klikka på enno
    +    E-postlenken har ikkje blitt opna endå
     
    -    Spyr på nytt um krypteringsnyklar frå dei andre einingane dine.
    +    Etterspør på nytt krypteringsnøkklar frå dei andre einingane dine.
     
    -    Nykelfyrespurnaden er send.
    +    Nøkkelførespurnaden er sendt.
     
    -    Fyrespurnaden er send
    -    Gjer vel og køyr Riot på ei onnor eining som kan avkryptera meldingi slik at ho kann senda nyklane til denna einingi.
    +    Førespurnaden er send
    +    Start Riot på ein annan eining som kan dekryptere meldingen, slik at den kan sende nøkklane til denne sesjonen
     
    -    Lesenlapp-lista
    +    Les kvitteringsliste
     
    -    Gruppalista
    +    Gruppeliste
     
         
    -        1 medlemskapsbrigde
    -        %d medlemskapsbrigde
    +        1 endring i medlemskap
    +        %d endringar i medlemskap
         
     
         Send som
    -    Upphavleg
    +    Original
         Stor
    -    Millomstor
    +    Middels
         Liten
     
    -    Brjot av nedlastingi\?
    -    Brjot av opplastingi\?
    +    Avbryt nedlasting\?
    +    Avbryt opplasting\?
         %d s
         %1$dm %2$ds
     
    @@ -260,45 +261,44 @@
         Romemne
     
         Ring
    -    Samtala er kopla upp
    -    Samtala koplar upp…
    -    Samtala er enda
    +    Anrop tilkopla
    +    Koplar saman anrop…
    +    Anrop avslutta
         Ringjer…
    -    Du vert ringd
    -    Innkomande videosamtala
    -    Innkomande Røystsamtala
    -    Ei samtala er i gang…
    +    Innkommande anrop
    +    Innkommande videosamtale
    +    Innkommande taleanrop
    +    Anrop pågår…
     
    -    Den onnor sida tok ikkje røyret.
    -    Klarer ikkje skru på kameraet
    -    røyret er teke ein annan stad
    +    Den andre parten tok ikkje samtalen.
    +    Klarar ikkje å skru på kameraet
    +    Anropet vart teke ein annan plass
     
    -    Tak eit bilæte eller ein video
    -    Kann ikkje spela inn video
    +    Ta bilete eller video
    +    Kan ikkje spela inn video
     
         Info
    -    Riot treng åtgang til bilæti og videoane dine fyr å kunna senda og lagra vedlegg.
    -\n
    -\nGjer vel og gjev åtgang på sprettvindauget som kjem fyr å kunna senda filer frå mobilen.
    -    Riot treng åtgang til kameraet ditt fyr å taka bilæte og videosamtalor.
    +    Riot treng tilgang til bilete- og videobiblioteket for å senda og lagra vedlegg. 
    +\n 
    +\nGje tilgang i sprettvindauget som kjem for å senda filer frå mobilen.
    +    Riot treng tilgang til kameraet ditt for å ta bilete og videosamtalar.
         "
     \n
    -\nVer venleg og gjev løyve på sprettvindauget som kjem fyr å kunna ringja."
    -    Riot treng åtgang til mikrofonen din fyr å kunna ringja.
    -    Mediesamankoplingi gjekk gale
    +\nVer venleg og gje tilgang på sprettvindauget som kjem for å starte anropet."
    +    Riot treng tilgang til mikrofonen din for å utføra taleanrop.
    +    Mediaforbindelsen feila
         "
     \n
    -\nGjer vel og gjev åtgang på sprettvindauget som kjem fyr å kunna ringja."
    -    Riot treng åtgang til kameraet og mikrofonen din fyr å videosamtala. 
    -\nGjer vel og gjev åtgang på sprettvindauget som kjem fyr å kunna ringja.
    -    Riot treng åtgang til adresseboki di fyr å finna andre Matrix-brukarar utifrå epostadressone og telefonnummeri deira.
    +\nGjer vel og gje tilgang på sprettvindauget som kjem for å utføra samtalen."
    +    Riot treng tilgang til kameraet og mikrofonen din for å utføra videosamtalar.
     \n
    -\nGjer vel og gjev løyve på sprettvindauget som kjem fyr å finna addressebokbrukarar som du kann nå frå Riot.
    -    Riot treng løyve til å gå inn i adresseboki di fyr å finna andre Matrix-brukarar ut ifrå epostadressone og telefonnummeri deira. 
    +\nGjer vel og gjev tilgang på sprettvindauget som kjem for å utføra samtalen.
    +    Riot treng tilgang til kontaktliste for å finna andre Matrix-brukarar basert på e-post og telefonnummer. Viss du samtykker til å dele kontaktlista, ver venleg å tillat tilgang på sprettvindauget som kjem på neste skjermbilete.
    +    Riot treng tilgang til kontaktliste for å finna andre Matrix-brukarar basert på e-post og telefonnummer. 
     \n
    -\nGje Riot løyve til å gå inn i kontaktane dine\?
    +\nSamtykker du til å dele adresseboka for dette føremålet \?
     
    -    Orsak. Handlingi vart ikkje gjenomført på grunn av løyve som vantar
    +    Beklagar. Grunna manglande tilgangar, vart ikkje handlinga utført
     
         Lagra
         Lagra til nedlastingar\?
    @@ -306,38 +306,39 @@
         NEI
         Gå fram
     
    -    Tak burt
    -    Vert med
    -    Fyrehandsvising
    -    Seg nei
    +    Ta vekk
    +    Bli med
    +    Førehandsvisning
    +    Avvis
     
    -    Syn fram medlemer
    -    Opna yverskriften
    -    Samstiller…
    -    Hoppa til den fyrste ulesne meldigni.
    +    Medlemsoversikt
    +    Opne overskrift
    +    Synkroniserer…
    +    Hopp til første uleste melding.
     
    -    Du vart boden inn til detta romet av %s
    -    Innbjodingi vart send til %s, som ikkje er knytt til denna brukaren. Du byr kann henda logga inn med ein annan brukar, eller knyta eposten din til brukaren din.
    -    Du freistar å gå inn i %s. Vil du verta med so du kan taka del i meiningsutvekslingi\?
    +    Du vart invitert med i rommet av %s
    +    Invitasjonen vart sendt til %s, som ikkje er assosiert med denne kontoen.
    +\nDu kan logge inn med ein annan konto, eller legge til denne e-postadressa i din konto.
    +    Du prøver å gå inn i %s. Ønskjer du å bli med for å delta i diskusjonen\?
         eit rom
    -    Detta er ei fyrehandsvising av romet. Romsamhandlingar hev vorte skrudde av.
    +    Dette er ei førehandsvisning av rommet. Interaksjonar med rommet er slått av.
     
    -    Ny samtala
    -    Legg til ein medlem
    +    Ny samtale
    +    Legg til medlem
         
    -        1 verksam medlem
    -        %d verksame medlemer
    +        1 aktiv medlem
    +        %d aktive medlem
         
         
             1 medlem
    -        %d medlemer
    +        %d medlem
         
         1 medlem
     
    -    Far frå romet
    -    Er du trygg på at du vil fara frå romet\?
    -    Er du trygg på at du vil taka burt %s frå denna samtala\?
    -    Laga
    +    Forlat rom
    +    Er du sikker på at du vil forlata rommet\?
    +    Er du sikker på at du vil ta vekk %s frå denne samtalen\?
    +    Lag
     
         Tilkopla
         Fråkopla
    @@ -346,39 +347,39 @@
         %1$s %2$s sidan
     
         ADMINISTRATORVERKTY
    -    RØYSTSAMTALA
    -    BEINVEGES SAMTALOR
    -    EININGAR
    +    ANROP
    +    DIREKTESAMTALE
    +    SESJONAR
     
    -    Bjod inn
    -    Far frå romet
    -    Spark ut frå detta romet
    +    Inviter
    +    Forlat dette rommet
    +    Fjern frå dette romet
         Steng ute
         Slepp inn att
         Gjer til vanleg brukar
         Gjer til moderator
         Gjer til administrator
    -    Gøym alle meldingar frå denna brukaren
    -    Syn alle meldingar frå denna brukaren
    +    Gøym alle meldingar frå denne brukaren
    +    Syn alle meldingar frå denne brukaren
         Syn alle meldingar frå denne brukaren\? 
    -\n
    -\nMerk deg at denna handlingi startar æppen på nytt og kann taka litt tid.
    -    Brukar-ID, namn, eller epost
    +\n 
    +\nMerk deg at denne handlinga startar programmet på nytt og kan ta litt tid.
    +    Brukar-ID, namn, eller e-post
         Nemn
    -    Syn einingslista
    -    Du vil ikkje kunna gjera um på detta brigdet fyrdi du hev ein brukar si høgd upp til di eiga. 
    -\nEr du trygg på detta\?
    +    Vis sesjonsliste
    +    Du vil ikkje kunne angre denne endringa, då du promoterar brukaren til å ha same tilgangsnivå som deg sjølv.
    +\nEr du sikker på dette\?
     
    -    Er du trygg på at du vil bjoda %s inn til samtala\?
    +    Er du sikker på at du vil invitera %s inn til samtala\?
         Er du trygg på at du vil stengja brukaren ute frå samtala\?
     
    -    Bjod inn gjenom ID
    +    Inviter med ID
         LOKALE KONTAKTAR (%d)
    -    BRUKARUTVAL (%s)
    -    Berre Matrix-brukarar
    +    BRUKARKATALOG (%s)
    +    Berre for Matrix-brukarar
     
    -    Byd inn brukarar gjennom ID
    -    Ver venleg og skriv ei eller fleire epostladresser eller Matrix-IDar inn
    +    Inviter inn brukarar med ID
    +    Ver venleg og skriv ei eller fleire e-postadresser eller Matrix-IDar inn
         Epost eller Matrix-ID
     
         Søk
    @@ -391,29 +392,29 @@
         Send eit svar (ikkje-kryptert)…
         Tilkoplinga til tenaren gjekk tapt.
         Meldingane vart ikkje sende. %1$s eller %2$s no?
    -    Meldingane vart ikkje sende av di ukjende einingar er til stades. %1$s eller %2$s no\?
    +    Meldingane vart ikkje sende då ukjende sesjonar er aktive. %1$s eller %2$s no\?
         Send alle på nytt
    -    bryt alle av
    +    Avbryt alle
         Send usende meldingar på nytt
         Slett usende meldingar
         Fila vart ikkje funne
    -    Du hev ikkje løyve til å senda meldingar i detta romet
    +    Du har ikkje løyve til å senda meldingar i dette rommet
         
             1 ny melding
             %d nye meldingar
         
     
    -    Lit på
    -    Ikkje lit på
    -    Logga ut
    -    Yversjå
    +    Stol på
    +    Ikkje stol på
    +    Logg ut
    +    Ignorer
         Fingeravtrykk (%s):
    -    Fekk ikkje til å stadfesta identiteten på fjerrtenaren.
    -    Detta kann tyda at nokon tjuvlyder på koplingi di, eller at mobilen ikkje liter på sertifikatet frå fjerrtenaren.
    -    Um tenaradministratoren hev sagt at detta er å venta, pass på at fingeravtrykket under samsvarar med fingeravtrykket han gjev deg.
    -    Sertifikatet har forandra seg frå ein som var stolt på av mobilen din. Dette er SÆRS UVANLEG. Det tilrådast å IKKJE GODKJENNA det nye sertifikatet.
    -    "Sertifikatet hev brigda seg frå eit som tidlegare var lite på til eit som ikkje er det. Det kann henda at tenaren hev nya uppatt sertifikatet sitt. Snakk med tenaradministratoren fyr å få det venta  fingeravtrykket."
    -    Godkjenn BERRE sertifikatet viss tenaradministratoren har gjeve ut eit fingeravtrykk som samsvarar med det over.
    +    Fekk ikkje til å stadfesta identiteten på den eksterne serveren.
    +    Detta kan vere teikn på at tilkoplinga blir avlytta eller at telefonen ikkje stolar på sertifikatet den eksterne tenaren brukar.
    +    Om administratoren av serveren har informert at dette er forventa, pass på at fingeravtrykket under samsvarar med fingeravtrykket han gav deg.
    +    Sertifikatet har forandra seg frå det som var stolt på av mobilen din. Dette er SÆRS UVANLEG. Det er tilrådd å IKKJE GODKJENNA dette nye sertifikatet.
    +    Sertifikatet har endra seg frå eit som tidlegare var stole på, til eit som ikkje er det. Det kan henda at tenaren har fornya sertifikatet. Snakk med administrator for å få det forventa fingeravtrykket.
    +    Godkjenn BERRE sertifikatet viss tenaradministratoren har publisert eit fingeravtrykk som samsvarar med det over.
     
         Romdetaljar
         Folk
    @@ -423,10 +424,10 @@
         BODNE INN
         KOMNE INN
     
    -    Grunnen til at du melder innhaldet
    -    Vil du gøyma alle meldingar frå denna brukaren\?
    -\n
    -\nMerk at detta vil starta æppen på nytt og kann taka litt tid.
    +    Grunnlag til at du rapporterer innhaldet
    +    Vil du gøyma alle meldingar frå denne brukaren\? 
    +\n 
    +\nMerk at detta vil starta applikasjonen på nytt og kan ta litt tid.
         Brjot av upplastingi
         Brjot av nedlastingi
     
    @@ -441,7 +442,7 @@
     
         VERT MED
         UTVAL
    -    YNDLINGAR
    +    FAVORITTAR
         ROM
         LÅGRETT
         INNBJODINGAR
    @@ -460,41 +461,41 @@
             Fann %1$s rom fyr %2$s
             Fann %1$s rom fyr %2$s
         
    -    Søkjer i utvalet..
    +    Søkjer gjennom katalogen…
     
         Alle meldingar (bråkete)
         Alle meldingar
         Berre nemningar
         Stilna
         Gjer til yndling
    -    Tak burt høgrett
    -    Beinveges samtala
    -    Far frå samtala
    +    Nedprioriter
    +    Direktesamtale
    +    Forlat samtale
         Gløym
    -    Laga snarveg på heimskjermen
    +    Lag snarveg på heimskjermen
     
         Meldingar
         Innstillingar
    -    Utgåva
    -    Vilkår og fyresetnader
    -    Tridjepartivarsel
    -    Upphavsrett
    -    Personvernretningslinor
    +    Versjon
    +    Vilkår for bruk
    +    Informasjon frå tredjepart
    +    Opphavsrett
    +    Personvern
     
    -    Profilbilæte
    +    Profilbilete
         Visingsnamn
         Epost
    -    Legg til ei epostadressa
    +    Legg til e-postadresse
         Telefon
    -    Legg til eit telefonnummer
    -    Syn æppinfoen i systeminnstillingane
    -    Æppinfo
    +    Legg til telefonnummer
    +    Vis informasjon om appen i systeminnstillingane
    +    Informasjon om applikasjonen
     
    -    Varselpersonvern
    +    Personvern for varslingar
         Vanleg
         Minska personvern
    -    Æppen treng løyve til å køyra i bakgrunnen
    -    • Varsel vert sende gjennom Google Cloud-Meldingar
    +    App\'en treng løyve til å køyra i bakgrunnen
    +    • Varsel blir sende via Firebase Cloud Messaging
         • Varsel inneheld berre metadata
         • Meldingsinnhaldet i varselet ertrygt henta beinveges frå Matrix-heimtenaren
         • Varsel inneheld meta- og meldingsdata
    @@ -502,14 +503,14 @@
     
         Varselljod
         Skru på varsel fyr denne brukaren
    -    Skru på varsel fyr denna einingi
    +    Aktiver varslingar for denne sesjonen
         Skru skjermen på i 3 sekund
     
         Meldingar som inneheld visingsnamnet mitt
         Meldingar som inneheld brukarnamnet mitt
    -    Meldingar i ein-og-ein-samtalor
    -    Meldingar i gruppasamtalor
    -    Når eg vert boden inn i eit rom
    +    Meldingar i ein-og-ein-samtalar
    +    Meldingar i gruppesamtalar
    +    Når eg blir invitert til eit rom
         Røystsamtalainnbjodingar
         Meldingar frå botar
     
    @@ -517,60 +518,60 @@
         Bakgrunnsamstilling
         Skru på bakgrunnsamstilling
         Samstillingsfyrespurnaden fekk tidsavbrot
    -    Forseining mellom to samstillingsførespurnader
    +    Forsinkelse mellom kvar synkronisering
         sekund
         sekund
     
    -    Utgåva
    -    olm-utgåva
    -    Vilkår og fyresetnader
    -    Tridjepartivarsel
    -    Upphavsrett
    -    Personvernsretningslinor
    -    Tøm buffaren
    -    Tøm mediabuffaren
    -    Hald på mediet
    +    Versjon
    +    olm-versjon
    +    Vilkår for bruk
    +    Informasjon frå tredjepart
    +    Opphavsrett
    +    Personvern
    +    Tøm buffer
    +    Tøm mediabuffer
    +    Behald mediefiler
     
         Brukarinnstillingar
         Varsel
    -    Oversedde brukarar
    +    Ignorerte brukarar
         Anna
    -    Omfattande
    +    Avansert
         Kryptografi
         Varselmål
    -    Lokale kontakter
    -    Kontakttillating
    -    Kontaktar-land
    -    Heimeside
    -    Fest rom med usedde varsel
    +    Lokale kontaktar
    +    Kontakttilgang
    +    Land for telefonbok
    +    Startskjerm
    +    Fest rom med tapte varslingar
         Fest rom med uleste meldingar
    -    Einingar
    +    Sesjonar
         Skru URL-førehandsvisingar i tekstfeltet på
    -    Vis alltid meldingstidspunkt
    -    Vis tidspunkt i 12-timarsform (t.d. 4:20pm)
    +    Vis tidsspunkt for alle meldingar
    +    Vis tidspunkt i 12-timarsformat
         Rist ved nemning
         Førehandsvis medium før avsending
     
    -    Avliv Brukar
    -    Avliv brukaren min
    +    Deaktiver konto
    +    Deaktiver kontoen min
     
         Varselpersonvern
    -    Riot kan køyra i bakgrunnen for å sikkert og privat halda styr på varsla dine (dette kan påverka batteribruket).
    +    Riot kan køyra i bakgrunnen for å sikkert og privat halda styr på varsla dine (dette kan påverka batteribruk).
         Gje tillating
         vel noko anna
     
         Statistikk
         Send statistikkdata
    -    Riot samlar anonym statistikk inn for å forbetra æppen.
    +    Riot samlar anonym statistikk inn for å forbetra applikasjonen.
         Ver venleg og skru statistikkinnsamling på for å hjelpa oss med å forbetra Riot.
         Ja, eg vil hjelpa til!
     
         Datasparingsmodus
     
    -    Einingsdetaljar
    +    Informasjon om sesjon
         ID
    -    Namn
    -    Einingsnamn
    +    Offentleg namn
    +    Oppdater offentleg namn
         Sist sedd
         %1$s @ %2$s
         Denne handlinga krev vidare sjølvstadfesting.
    @@ -584,25 +585,23 @@ For å gå fram, ver venleg og skriv passordet ditt inn.
         Identitetstenar
     
         Brukargrensesnitt
    -    Grensesnitts-mål
    -    Vel eit mål
    +    Språk
    +    Vel språk
     
         Ventar på Godkjenning
         Ver venleg og sjekk eposten din og klikk på lenkja han inneheld. Når det er gjort, klikk gå fram.
    -    Fekk ikkje til å stadfesta epostadressa. Ver venleg og sjekk eposten din og klikk på lenkja han inneheld. Når det er gjort, klikk gå fram
    -    Epostadressa er allereie i bruk
    -    Fekk ikkje til å senda eposten: Fann ikkje epostadressa
    -    Telefonnummeret er allereie i bruk
    +    Fekk ikkje til å stadfesta e-postadressa. Ver venleg og sjekk e-posten din og klikk på lenkja han inneheld. Når det er gjort, klikk gå fram
    +    E-postadressa er allereie i bruk
    +    Fann ikkje e-postadressa
    +    Telefonnummeret er allereie i bruk.
     
         Forandr passordet
    -    gamalt passord
    -    nytt passord
    -    stadfest passordet
    +    Gjeldande passord
    +    Nytt passord
    +    Stadfest passordet
         Fekk ikkje til å oppdatera passordet
         Passordet ditt vart oppdatert
    -    Vis alle meldingane frå %s?
    -
    -Merk at denne handlinga gjer at æppen startar på nytt og kan taka litt tid.
    +    Vis alle meldingane frå %s\? Merk at denne handlinga gjer at applikasjonen startar på nytt og kan taka litt tid.
     
         Er du sikker på at du vil fjerna varselmålet?
     
    @@ -615,7 +614,7 @@ Merk at denne handlinga gjer at æppen startar på nytt og kan taka litt tid.Telefonnummer
         Ugangbart telefonnummer for det valde landet
         Mobilstadfesting
    -    Vi har send deg ein SMS med ein kode. Ver venleg og skriv han inn under.
    +    Vi har sendt deg ein SMS med aktiveringskode. Ver venleg og skriv han inn under.
         Skriv ein stadfestingskode inn
         Noko gjekk galt under stadfestinga av telefonummeret ditt
         Kode
    @@ -654,34 +653,34 @@ Merk at denne handlinga gjer at æppen startar på nytt og kan taka litt tid.For å lenkja eit rom må det ha ei adresse.
         Berre folk som har vorte bodne inn
    -    Kven som helst som veit om romkenkja, sett vekk frå gjester
    -    Kven som helst som veit om romlenkja, gjester òg
    +    Kven som helst som veit romlenkja, sett vekk frå gjester
    +    Kven som helst som veit romlenkja, inkluderande gjester
     
         Utestengde brukarar
     
         Omfattande
    -    Rommet sin indre ID
    +    Rommet sin interne ID
         Adresser
    -    Labbar
    +    Eksperimentelle funksjonar
         Desse funksjonane er under utprøving og uventa vanskar kan dukka opp. Bruk med omhug.
         Ende-til-Ende-Kryptering
         Ende-til-Ende-Enrkyptering er skrudd på
         Du må vera logga ut for å kunna skru krypteringa på.
    -    Krypter berre til godkjende einingar
    -    Aldri send enkryterte meldingar til ikkje-godkjende einingar i dette rommet frå denne eininga.
    +    Krypter berre til godkjende sesjonar
    +    Aldri send krypterte meldingar til ikkje-godkjende sesjonar i dette rommet frå denne sesjonen.
     
         Dette rommet har ingen lokale adresser
         Ny adresse (t.d. #foo:matrix.org)
     
         Dette rommet viser ikkje særpreg for nokre samfunn
    -    Ny samfunns-ID (t.d. +foo:matrix.org)
    -    Ugangbar samfunns-ID
    -    \'%s\' er ikkje ein gangbar samfunns-ID
    +    Ny fellesskaps-ID (t.d. +foo:matrix.org)
    +    Ugyldig fellesskaps-ID
    +    \'%s\' er ikkje ein gyldig fellesskaps-ID
     
     
         Ugangbar aliasform
         \'%s\' er ikkje ei gangbar form for eit alias
    -    Du har inga oppgjeven hovudadresse. Hovudadressa til rommet vert tilfeldig vald
    +    Du vil ikkje ha spesifisert ei hovudadresse for dette rommet.
         Hovudadresse-åtvaringar
     
         Set som Hovudadresse
    @@ -697,7 +696,7 @@ Merk at denne handlinga gjer at æppen startar på nytt og kan taka litt tid.Utval
         Preg
     
    -    %s prøvde å lasta eit gjeve punkt i rommet si tidsline men klarte ikkje å finna det.
    +    %s prøvde å lasta eit spesifikt punkt i rommet si tidslinje men klarte ikkje å finna det.
     
         Ende-til-ende-krypteringsinfo
     
    @@ -709,36 +708,34 @@ Merk at denne handlinga gjer at æppen startar på nytt og kan taka litt tid.Økt-ID
         Noko gjekk gale med dekrypteringa
     
    -    Avsendareiningsinfo
    -    Einingsnamn
    -    Namn
    -    Einings-ID
    -    Einings-nykel
    +    Avsendar-sesjonsinformasjon
    +    Offentleg namn
    +    Offentleg namn
    +    ID
    +    Sesjonsnøkkel
         Godkjenning
         Ed25519-fingeravtrykk
     
    -    Hent E2E-romnyklar ut
    -    Hent romnyklar ut
    -    Hent nyklane ut til ei lokal fil
    +    Eksporter E2E-romnøkklar
    +    Eksporter romnøkklar
    +    Hent nøkklane ut til ei lokal fil
         Hent ut
         Skriv passetning inn
         Stadfest passetninga
    -    E2E-romnyklane vart lagra til \'%s\'
    +    E2E-romnøkklane vart lagra til \'%s\' Åtvaring: fila kan verta sletta viss app\'en avinstallerast.
     
    -Åtvaring: fila kan verta sletta vis æppen avinstallerast.
    -
    -    Hent E2E-romnyklar inn
    -    Hent romnyklar inn
    -    Hent nyklane inn frå ei lokal fil
    +    Importer E2E-romnøkklar
    +    Importer romnøkklar
    +    Hent nøkklane inn frå ei lokal fil
         Hent inn
    -    Krypter berre til godkjende einingar
    -    Aldri send krypterte meldingar til ikkje-godkjende einingar frå denne eininga
    +    Krypter berre til godkjende sesjonar
    +    Aldri send krypterte meldingar til ikkje-godkjende sesjonar frå denne sesjonen
     
         IKKJE Godkjend
         Godkjend
         Svartelista
     
    -    ukjend eining
    +    ukjend sesjon
         ingen
     
         Godkjenn
    @@ -746,23 +743,17 @@ Merk at denne handlinga gjer at æppen startar på nytt og kan taka litt tid.Set på svartelista
         Fjern frå svartelista
     
    -    Godkjenn eininga
    -    For å godkjenna at denne eininga er til å stola på, ver venleg og snakk med eigaren på ei anna måte (t.d. ansikt til ansikt eller på telefon) og spør han om nykelen han ser i Brukarinnstillingane for denne eininga samsvarar med nykelen under:
    -    Viss han samsvarer, klikk godkjennknappen under.
    -Viss han ikkje gjer det, tjuvlytter nokon andre på eininga og du bør sannsynlegvis setja ho på svartelista.
    -I framtida kjem denne godkjenningsprosessen til å vera betre utvikla.
    -    Eg stadfestar at nyklane er den same
    +    Verifiser sesjonen
    +    For å godkjenna at denne sesjonen er til å stola på, ver venleg og snakk med eigaren på ein anna måte (t.d. ansikt til ansikt eller på telefon) og spør han om nøkkelen han ser i Brukarinnstillingane for denne sesjonen samsvarar med nøkkelen under:
    +    Viss det samsvarer, klikk Verifiser-knappen under. Viss det ikkje gjer det, avlyttar nokon andre denne sesjonen og du bør sannsynlegvis svarteliste den. I framtida vil denne godkjenningsprosessen bli meir forbetra.
    +    Eg stadfestar at nøkklane er like
     
    -    Riot støttar no ende-til-ende-kryptering men du må logga inn att for å skru det på.
    +    Riot støttar no ende-til-ende-kryptering men du må logga inn att for å skru det på. Du kan gjera det no eller seinare i App-innstillingane.
     
    -Du kan gjera det no eller seinare frå æpp-innstillingane.
    -
    -    Rommet inneheld ukjende einingar
    -    Rommet inneheld ukjende einingar som ikkje har verta godkjend.
    -Dette tydar at det ikkje er sikkert at einingane høyrer til dei brukarane dei hevdar.
    -Vi tilråder deg å gå gjennom godkjenninga for kvar eining før du går fram, men du kan senda meldinga på nytt utan å godkjenne viss du vil.
    -
    -Ukjende einingar:
    +    Rommet inneheld ukjende sesjonar
    +    Rommet inneheld ukjende sesjonar som ikkje har blitt verifiserte. Det er med andre ord ingen garanti for at sesjonane høyrer til brukarane dei hevdar. Vi tilråder deg å gå gjennom verifikasjonsprosessen for kvar eining før du går vidare, men du kan sende meldingen på nytt utan å godkjenne viss du vil. 
    +\n
    +\nUkjende sesjonar:
     
         Vel eit romutval
         Tenaren er kanskje utilgjengeleg eller overlasta
    @@ -800,8 +791,8 @@ Ukjende einingar:
         Widget-laging mislukkast
         %1$s lagt til av %2$s
         %1$s fjerna av %2$s
    -    Laga gruppasamtalor med jitsi
    -    Er du sikker på at du vil sletta widgeten?
    +    Lag konferansesamtalar med Jitsi
    +    Er du sikker på at du vil slette widgeten frå dette rommet\?
         
             1 verkande widget
             %d verkande widgetar
    @@ -817,12 +808,12 @@ Ukjende einingar:
         Rommet %s er ikkje synleg.
         Ein krevd parameter vantar.
         Ein parameter er ikkje gangbar.
    -    Legg Matrix-æppar til
    +    Legg til Matrix-applikasjonar
         Bruk entertasten på tastaturet for å senda meldingar
    -    Send røystmelding (krever ein tredjepartisæpp for å spela inn meldinga)
    +    Send talemelding
     
    -    Du la til den nye eininga \'%s\', som bed om krypteringsnyklar.
    -    Den ikkje-godkjende eininga di \'%s\' bed om krypteringsnyklar.
    +    Du la til den nye sesjonen \'%s\', som etterspør krypteringsnøkklar.
    +    Den ikkje-verifiserte sesjonen din \'%s\' etterspør krypteringsnøkklar.
         Byrj godkjenning
         Del utan godkjenning
         Oversjå førespurnaden
    @@ -844,7 +835,7 @@ Ukjende einingar:
         Sparkar brukarar med gjeven ID
         Forandrar visingsnamnet ditt
         På/Av markdown
    -    For å retta opp i Matrix-Æppadministrering
    +    For å ordne opp i Matrix-App-administrasjon
     
         Av
         Bråket
    @@ -852,10 +843,10 @@ Ukjende einingar:
         Kryptert melding
     
         Laga
    -    Laga samfund
    -    Samfunnsnamn
    +    Lag fellesskap
    +    Fellesskapsnamn
         Døme
    -    Samfunns-ID
    +    Fellesskaps-ID
         døme
     
         Heim
    @@ -869,7 +860,7 @@ Ukjende einingar:
         Filtrer gruppemedlemer
         Filtrer grupperom
     
    -    Samfunnsadministratoren har ikkje gjeve ei lang skildring for dette samfunnet.
    +    Fellesskapssadministratoren har ikkje satt ei lang skildring for dette fellesskapet.
     
         Du vart sparka frå %1$s av %2$s
         Du var stengd ute frå %1$s av %2$s
    @@ -877,9 +868,9 @@ Ukjende einingar:
         Vert med att
         Gløym rommet
     
    -    Du må logga på att for å laga ende-til-ende-krypteringsnyklar til denna eininga og senda den ålmene nykelen til heimetenaren. 
    -\nDetta trengst berre éin gong. 
    -\nOrsak for ulempa.
    +    Du må logga på att for å laga ende-til-ende-krypteringsnnøkklar til denne sesjonen og senda den offentlege nøkkelen (public key) til heimetenaren. 
    +\nDette trengst berre éin gong. 
    +\nBeklagar ulempa.
     
         Alle %s-rom på Matrix
     
    @@ -891,15 +882,15 @@ Ukjende einingar:
         For å halda fram med å bruka %1$s-heimtenaren må du sjå over og seia deg einig i vilkåret og føresetnader.
         Sjå over no
     
    -    Avliv Brukar
    +    Deaktiver konto
         Dette gjer at brukaren din vert ubrukeleg til evig tid. Du kjem ikkje til å kunna logga inn, og ingen andre kjem til å kunna melde seg inn med den gamle brukar-IDen din. Brukaren din forlét òg alle rom han er i, og brukardetaljane dine vil verta fjerna frå identitetstenaren. Denne handlinga kan ikkje gjerast om.
     
     Å avliva brukaren din gjer i utgangspunktet ikkje at vi gløymer meldingane du har send. Viss du vil at vi skal gløyma meldingane dine, ver venleg og kryss av i firkanten under.
     
     Meldingssynlegheit på Matrix liknar på epost. At vi gløymer meldingane dine tyder at meldingar du har send ikkje vil verta delt med nye, ikkje-innmeldte brukarar, men brukare som er meldt på som allereie har tilgang til desse meldingane vil fortsatt kunne sjå kopien deira.
    -    Ver venleg og gløym alle meldingane eg har send når brukaren min vert avliven (Åtvaring: dette gjer at framtidige brukarar ikkje fær eit fullstendig oversyn av samtalene)
    +    Ver venleg og gløym alle meldingane eg har send når brukarkontoen min vert deaktivert (Åtvaring: dette gjer at framtidige brukarar ikkje får eit fullstendig oversyn av samtalene)
         For å gå fram, ver venleg og skriv passordet ditt inn:
    -    Avliv Brukar
    +    Deaktiver konto
     
         Ver venleg og skriv passordet ditt inn.
         Rommet har verta erstatta og er ikkje aktivt lenger
    @@ -947,7 +938,7 @@ Meldingssynlegheit på Matrix liknar på epost. At vi gløymer meldingane dine t
     
         kontakt tenesteadministratoren din
     
    -    Grunna vantande løyve er denna handlingi umogeleg.
    +    Grunna manglande rettigheiter, er ikkje denne handlinga muleg.
         Ressursgrensa er Forbigått
         Heimtenaren har truffe ei av ressursgrensene sine so nokre brukarar vil ikkje kunna logga inn.
         Heimtenaren har forbigått ei av ressursgrensene sine.
    @@ -958,47 +949,47 @@ Meldingssynlegheit på Matrix liknar på epost. At vi gløymer meldingane dine t
         Ver venleg og %s for å heva grensa.
         Ver venleg og %s for å halda fram med å bruka tenesten.
     
    -    Status.im-preg
    +    Status.im-tema
     
         Ring likevel
    -    Seg ja
    +    Aksepter
     
         Feil
     
    -    Hoppa yver
    -    Ferdug
    -    Brjot av
    -    Yversjå
    +    Hopp over
    +    Ferdig
    +    Avbryt
    +    Ignorer
     
    -    Er du trygg på at du vil logga ut\?
    +    Sikker på at du vil logga ut\?
         Merk som lest
    -    Gjer vel å sjå yver og godtak rettelinone til heimtenaren:
    +    Venligast sjå over og godta retningslinjene til heimtenaren:
     
         Det gjeng ikkje å nå URL-en, gjer vel og sjå til honom
    -    Uppringjingar
    -    Nytta den upphavlege Riot-ringjetona når du vert ringd upp
    -    Ringjetona fyr uppringjing
    -    Vel ringjetona til tilrop:
    +    Anrop
    +    Bruk standard Riot-ringetone for innkommande anrop
    +    Ringetone for innkommande anrop
    +    Vel ringetone for anrop:
     
    -    Ei videosamtala er i gang…
    +    Ein videosamtale pågår…
     
         Spark
         
    -        Er du trygg på at du vil sparka denna brukaren frå samtala\?
    -        Er du trygg på at du vil sparka dessa brukarane frå samtala\?
    +        Er du trygg på at du vil sparka denne brukaren frå samtala\?
    +        Er du trygg på at du vil sparka desse brukarane frå samtala\?
         
         Grunn
     
    -    Umfattande varselinnstilingar
    -    Set kor viktige varsel er ut ifrå hendingar, og stilla på ljod, LED, og risting
    -    Kor viktige varsel er ut ifrå hendingar
    +    Avanserte varslingsinnstillingar
    +    Sett prioritet for varslingar etter hending, konfiguer lyd, LED, vibrasjon
    +    Prioritet på varsel ut ifrå hendingar
     
    -    Leit etter vanskar med varsli
    -    Diagnostikk fyr feilsøkjing
    -    Køyr prøvar
    +    Løys problem med varslingar
    +    Diagnostikk for feilsøking
    +    Køyr testar
         Køyrer… (%1$d av %2$d)
    -    Det grunnleggjande er i ordning. Um du framleis ikkje fær varsel, gjer vel og send inn ei feilmelding so me kann sjå nærare på det.
    -    Éin eller fleire prøvar verka ikkje. Freista dessa løysingane.
    +    Det grunnleggjande er i orden. Om du framleis ikkje får varsel, send inn ei feilmelding så vi kan undersøke dette.
    +    Ein eller fleire testar har feila. Prøv føreslegne løysing(ar).
         Éin eller fleire prøvar gjekk galne. Gjer vel og send inn ei feilmelding so me kann sjå nærare på det.
     
         Systeminnstillingar.
    @@ -1013,10 +1004,70 @@ Meldingssynlegheit på Matrix liknar på epost. At vi gløymer meldingane dine t
     \nGjer vel og sjå på brukarinnstillingane.
         Skru på
     
    -    Eininginnstillingar.
    -    Varsel er skrudde på fyr denna einingi.
    -    Varsel er ikkje skrudde på fyr denna einingi.
    -\nGjer vel og sjå på Riot-innstillingane.
    +    Sesjonsinnstillingar.
    +    Varslingar er aktivert for denne sesjonen.
    +    Varslingar er deaktivert for denne sesjonen.
    +\nSjekk Riot-innstillingane.
         Skru på
     
    +    setter opp tenesta
    +    Sikkerheitskopi av nøkkel
    +    Bruk sikkerheitskopiering av nøkkel
    +    Stadfest sesjon
    +
    +    Sikkerheitskopi av nøklar er ikkje ferdig, vent litt…
    +    Om du loggar ut no, vil dine krypterte meldingar gå tapt
    +    Sikkerheitskopi av nøklar pågår, viss du loggar ut no vil du miste tilgang til krypterte meldingar.
    +    For å hindre tap av krypterte meldingar, bør sikkerheitskopiering av nøklar (Secure Key Backup) vere aktivert i alle sesjonar der du er innlogga.
    +    Eg ønsker ikkje dei krypterte meldingane mine
    +    Sikkerheitskopierar nøklar…
    +    Bruk sikkerheitskopiering av nøklar
    +    Er du sikker \?
    +    Sikkerheitskopier
    +    Du vil miste tilgang til dine krypterte meldingar, med mindre nøklane dine er sikkerheitskopierte før utlogging.
    +
    +    Bli
    +    Ingen
    +    Trekk tilbake
    +    Koble frå
    +    Sjå gjennom
    +    Avvis
    +
    +    Ingen identitetsserver er konfiguert.
    +
    +    Anrop feila fordi serveren er feilkonfiguert
    +    Be administratoren for din heimeserver (%1$s) om, å konfiguere ein TURN-servern for å handtere anrop på rett måte.
    +\n
    +\nEventuelt, kan du prøve å bruke den offentlege serveren på %2$s, men dette vil kanskje ikkje vere like stabilt og IP-adressa di vil bli delt mot den serveren. Du kan også styre dette under Innstillingar.
    +    Prøv med %s
    +    Spør meg ikkje om att
    +
    +    Logg inn med SSO (single sign-on)
    +    Sett e-postadresse for gjenoppretting av konto, så kan du og seinare bli synleg for folk som kjenner deg.
    +    Sett telefonnummer, så kan du og seinare bli synleg for folk som kjenner deg.
    +    Sett e-postadresse for gjenoppretting av konto. Bestem valfritt seinare om å vere synleg for folk som kjenner deg via e-post eller telefonnummer.
    +    Sett e-postadresse for gjenoppretting av konto. Bestem valfritt seinare om å vere synleg for folk som kjenner deg via e-post eller telefonnummer.
    +    Dette er ikkje ei gyldig Matrix-serveradresse
    +    Klarar ikkje å nå ein heimetenar på denne URL\'en, sjekk at den er korrekt
    +    Dingsen din brukar ein utdatert TLS-sikkerheitsprotokoll som er sårbar for angrep. For din sikkerheit vil du ikkje kunne koble til
    +    Tillat assistanseserver for tilbake-ring
    +    Vil nytte %s for assistanse når heimetenaren din ikkje tilbyr ein (IP-adressa di vil bli delt under samtalen)
    +    Legg til ein identitetstenar i innstillingane for å utføre denne handlinga.
    +    Versjon %s
    +    Stadfest ditt passord
    +    Denne handlinga kan ikkje utførast frå Riot på mobil
    +    Authentisering er påkrevd
    +
    +
    +    Integrasjonar
    +    Bruk ein integrasjonshandterar (Integration Manager) for å handtere botar, bruker, tillegg og klistermerkepakker.
    +\nIntegrasjonshandterarar hentar konfigurasjonsdata, kan endre tillegg, sende rominvitasjonar og sette tilgangsnivå på vegne av deg.
    +    Handtering av kryptografiske nøkklar
    +    Dette valet krev ein tredjepartsapplikasjon for å registrere meldingane.
    +    Last rom-medlemmar etter behov (lazy-load)
    +    Heimetenaren din støttar ikkje såklalla lazy-loading av rommelemar endå. Prøv igjen seinare.
    +
    +    Sikre meldingar med denne brukaren er ende-til-ende kryptert, dei kan ikkje bli lesne av tredje partar.
    +    Anna informasjon frå tredjepart
    +    Meldingar med denne brukaren er ende-til-ende kryptert, dei kan ikkje bli lesne av tredje part.
     
    diff --git a/vector/src/main/res/values-ru/strings.xml b/vector/src/main/res/values-ru/strings.xml
    index 3233ef2a33..9fbcc509be 100644
    --- a/vector/src/main/res/values-ru/strings.xml
    +++ b/vector/src/main/res/values-ru/strings.xml
    @@ -40,7 +40,7 @@
         "Из-за отсутствия разрешений некоторые функции могут быть недоступны..
         Вам нужно разрешение на приглашение для начала конференции в этой комнате
         Не удалось осуществить вызов
    -    Информация об устройстве
    +    Информация о сессии
         Конференц звонки не поддерживаются в зашифрованных комнатах
         Все равно отправить
         или
    @@ -176,9 +176,7 @@
         Должен быть введен новый пароль.
         На адрес %s было отправлено письмо. После перехода по ссылке в письме, нажмите ниже.
         Не удалось проверить email: убедитесь, что вы перешли по присланной ссылке
    -    Ваш пароль сброшен.
    -
    -Осуществлен выход на всех устройствах - вы не будете получать push уведомления. Для включения push уведомлений заново войдите на каждом из ваших устройств.
    +    Ваш пароль сброшен. Осуществлен выход на всех сессиях - вы не будете получать push уведомления. Для включения push уведомлений заново войдите на каждом из ваших устройств.
     
         
         URL должен начинаться с http[s]://
    @@ -198,8 +196,7 @@
         Вы не перешли по высланной в email ссылке
     
         
    -    Вам нужно войти в систему, чтобы сгенерировать ключи шифрования для этого устройства и отправлять публичный ключ вашему серверу.
    -Это необходимо только один раз; извините за неудобства.
    +    Вам нужно войти в систему, чтобы сгенерировать ключи шифрования для этой сессии и отправлять публичный ключ вашему серверу. Это необходимо только один раз; извините за неудобства.
     
         
         Чтение списка вступивших
    @@ -305,7 +302,7 @@
         АДМИНИСТРИРОВАНИЕ
         ВЫЗОВ
         ПРЯМЫЕ ЧАТЫ
    -    УСТРОЙСТВА
    +    СЕССИИ
     
         Пригласить
         Покинуть этот чат
    @@ -319,7 +316,7 @@
         Отобразить все сообщения этого пользователя
         ID пользователя, имя или email
         Упомянуть
    -    Отобразить список устройств
    +    Отобразить список сессий
         Вы не сможете отменить это действие, поскольку пользователь получит такой же уровень доступа, как и у вас. Вы уверены?
     
         "Вы уверены что хотите пригласить %s в этот чат?"
    @@ -342,7 +339,7 @@
         Отправить сообщение…
         Соединение с сервером потеряно.
         Сообщения не отправлены. %1$s или %2$s сейчас?
    -    Сообщения, не отправлены из-за присутствия неизвестных устройств. %1$s или %2$s сейчас?
    +    Сообщения, не отправлены из-за присутствия неизвестных сессий. %1$s или %2$s сейчас\?
         Повторить отправку
         Удалить все
         Отправить неотправленные сообщения
    @@ -434,7 +431,7 @@
         Сведения о приложении
     
         Включить уведомления для этой учетной записи
    -    Включить уведомления для этого устройства
    +    Включить уведомления для этой сессии
         Включить экран на 3 секунды
     
         В персональных чатах
    @@ -473,8 +470,8 @@
         Домашний экран
         Прикрепить комнаты с отключенными уведомлениями
         Прикрепить комнаты с непрочитанными сообщениями
    -    Устройства
    -    Информация об устройстве
    +    Сессии
    +    Информация о сессии
         ID
         Общеизвестное имя
         Обновить публичное имя
    @@ -570,8 +567,8 @@
         Сквозное шифрование
         Сквозное шифрование активно
         Вам необходимо выйти, чтобы включить шифрование.
    -    Шифровать сообщения только для проверенных устройств
    -    Никогда не отправлять шифрованное сообщение на непроверенные устройства в этой комнате с этого устройства.
    +    Шифровать сообщения только для проверенных сессий
    +    Никогда не отправлять шифрованное сообщение на непроверенные сессии в этой комнате с этой сессии.
     
         
         У этой комнаты еще нет локального адреса
    @@ -610,11 +607,11 @@
         ID сесии
         Ошибка дешифровки
     
    -    Информация об устройстве отправителя
    +    Информация о сессии отправителя
         Публичное имя
         Публичное имя
         ID
    -    Ключ устройства
    +    Ключ сеанса
         Проверка
         Ed25519 отпечаток
     
    @@ -632,14 +629,14 @@
         Импорт ключей комнаты
         Импортировать ключи из локального файла
         Импорт
    -    Шифровать только для проверенных устройств
    -    Не отправлять зашифрованные сообщения непроверенным устройствам.
    +    Шифровать только для проверенных сессий
    +    Не отправлять зашифрованные сообщения непроверенным сессиям с этой сессии.
     
         НЕ проверено
         Проверено
         В черном списке
     
    -    неизвестное устройство
    +    неизвестная сессия
         ничего
     
         Подтвердить
    @@ -647,11 +644,9 @@
         Блокировать
         Разрешить
     
    -    Проверить устройство
    -    Чтобы убедиться, что этому устройству можно доверять, обратитесь к его владельцу, используя другие способы (например, лично или по телефону), и спросите, соответствует ли ключ, который он видит в настройках для этого устройства:
    -    "Если совпадает, то нажмите кнопку подтвердить ниже.
    -Если не совпадает, возможно кто-то пытается перехватить устройство и  вы захотите добавить его в черный список.
    -В будущем данный процесс будет улучшен."
    +    Проверить сессию
    +    Чтобы убедиться, что этой сессии можно доверять, обратитесь к его владельцу, используя другие способы (например, лично или по телефону), и спросите, соответствует ли ключ, который он видит в настройках для этой сессии:
    +    "Если совпадает, то нажмите кнопку подтвердить ниже. Если не совпадает, возможно кто-то пытается перехватить сессию и  вы захотите добавить его в черный список. В будущем данный процесс будет улучшен."
         Я проверил, что ключи совпадают
     
         Riot теперь поддерживает сквозное шифрование, но вам нужно снова войти в систему, чтобы включить его.
    @@ -659,12 +654,12 @@
     Вы можете сделать это сейчас или позже из настроек приложения.
     
         
    -    Комната содержит неизвестные устройства
    -    Эта комната содержит неизвестные устройства, которые не были подтверждены.
    -Это означает, что эти устройства могут не принадлежать тем пользователям, на которых они претендуют. 
    -Перед продолжением рекомендуем вам пройти процесс проверки для каждого устройства, но вы можете отправить сообщение повторно, не проверяя.
    -
    -Неизвестные устройства:
    +    Комната содержит неизвестные сессии
    +    Эта комната содержит неизвестные сессии, которые не были подтверждены. 
    +\nЭто означает, что эти сессии могут не принадлежать тем пользователям, на которых они претендуют. 
    +\nПеред продолжением рекомендуем вам пройти процесс проверки для каждой сессии, но вы можете отправить сообщение повторно, не проверяя.
    +\n
    +\nНеизвестные сессии:
     
         
         Выбор списка комнат
    @@ -738,8 +733,8 @@
     
         Вызов
         Сообщения, содержащие мое имя пользователя
    -    Вы добавили новое устройство \'%s\', запрашивающее ключи шифрования.
    -    Ваше непроверенное устройство \'%s\' запрашивает ключи шифрования.
    +    Вы добавили новою сессию \'%s\', запрашивающая ключи шифрования.
    +    Ваше непроверенная сессия \'%s\' запрашивает ключи шифрования.
         Сообщения, содержащие мое отображаемое имя
         Начать проверку
         Поделиться без проверки
    @@ -909,9 +904,9 @@
         
         Получить аватар
         Заметка аватара
    -    Флэр
    +    Сообщества
     
    -    Эта комната не показывает флэр сообществ
    +    Эта комната не показывает любые сообщества
         Конфиденциальность уведомлений
         Нормальный
         Приложение нуждается в разрешении на работу в фоновом режиме
    @@ -962,12 +957,12 @@
         Скачать
         Говорить
         Очистить
    -    Повторно запросить ключи шифрования с другого вашего устройства.
    +    Повторно запросить ключи шифрования с других ваших сессий.
     
         Отправлен запрос ключа.
     
         Запрос отправлен
    -    Запустите Riot на другом устройстве, которое может расшифровать сообщение, для отправки ключа на это устройство.
    +    Запустите Riot на другом устройстве, которое может расшифровать сообщение, для отправки ключа на эту сессию.
     
         Введите здесь…
     
    @@ -1157,10 +1152,10 @@
     Пожалуйста, проверьте настройки аккаунта.
         Включить
     
    -    Настройки устройства.
    -    Уведомления включены для этого устройства.
    -    Уведомления не разрешены для этого устройства.
    -Пожалуйста, проверьте настройки Riot.
    +    Настройки сессии.
    +    Уведомления включены для этой сессии.
    +    Уведомления не включено для этой сессии. 
    +\nПожалуйста, проверьте настройки Riot.
         Включить
     
         Проверка сервисов Play
    @@ -1429,7 +1424,7 @@
         В режиме экономии траффика применяется специальный фильтр, поэтому обновление присутствия и уведомления о наборе отфильтровываются.
     
         Или защитите резервную копию с помощью ключа восстановления, сохранив его в безопасном месте.
    -    Безопасная резервная копия ключей должна быть активирована на всех ваших устройствах, чтобы не потерять доступ к зашифрованным сообщениям.
    +    Безопасная резервная копия ключей должна быть активирована на всех ваших сессиях, чтобы не потерять доступ к зашифрованным сообщениям.
         "Зашифрованная копия ключей  будет храниться на вашем сервере. Для безопасности, защитите её парольной фразой.
     
     Для максимальной безопасности он должен отличаться от пароля вашей учетной записи."
    @@ -1472,7 +1467,7 @@
         Мне
         Использовать настройку
     
    -    Проверить устройство
    +    Проверить сессию
     
         Ваше устройство использует устаревший TLS протокол, уязвимый для атак, для вашей же безопасности вам отказано в подключении
         Приложениям не" нужно  подключаться к HomeServer в фоновом режиме, это должно снизить расход заряда батареи"
    @@ -1485,10 +1480,10 @@
     
         К сожалению, конференц-звонки с Jitsi не поддерживаются на старых устройствах (ниже Android OS - 5.0)
     
    -    Новое устройство запрашивает ключи шифрования.
    -\nИмя устройства: %1$s
    -\nПоследний раз в сети: %2$s
    -\nЕсли вы не вошли с другого устройства, проигнорируйте этот запрос.
    +    Новая сессия запрашивает ключи шифрования. 
    +\nИмя сессии: %1$s 
    +\nПоследний раз в сети: %2$s 
    +\nЕсли вы не вошли с другой сессии, проигнорируйте этот запрос.
         Непроверенное устройство запрашивает ключи шифрования.
     \nИмя устройства: %1$s
     \nПоследний раз в сети: %2$s
    @@ -1763,7 +1758,7 @@
     \nСинхронизация может быть отложена в зависимости от ресурсов (батареи) или состояния устройства (спящий режим).
         Управляйте настройками обнаружения.
         Публичное имя (видимое для людей, с которыми вы общаетесь)
    -    Публичное имя устройства видны людям, с которыми вы общаетесь
    +    Публичное имя сессии видны людям, с которыми вы общаетесь
         Вы не используете какой-либо сервер идентификации
         Идентификационный сервер не настроен, требуется сброс пароля.
     
    diff --git a/vector/src/main/res/values-sq/strings.xml b/vector/src/main/res/values-sq/strings.xml
    index 397a245289..7ff5253530 100644
    --- a/vector/src/main/res/values-sq/strings.xml
    +++ b/vector/src/main/res/values-sq/strings.xml
    @@ -591,7 +591,7 @@
         Importo kyçe nga një kartelë vendore
         Importo
         Jepni frazëkalimin
    -    Fshehtëzoje vetëm për pajisje të verifikuara
    +    Fshehtëzoje vetëm për sesione të verifikuar
         JO i verifikuar
         I verifikuar
         Në Listë të Zezë
    @@ -793,8 +793,8 @@
         Shtypni një shërbyes home që të paraqiten dhoma publike prej tij
         Ju duhen leje për të administruar widget-e në këtë dhomë
         Përdor kamerë të brendshme
    -    Shtuat një pajisje të re \'%s\', e cila po kërkon kyçe fshehtëzimi.
    -    Pajisja juaj e paverifikuar \'%s\' po kërkon kyçe fshehtëzimi.
    +    Shtuat një sesion të ri \'%s\', i cili po kërkon kyçe fshehtëzimi.
    +    Sesioni juaj i paverifikuar \'%s\' po kërkon kyçe fshehtëzimi.
         I heq cilësinë e operatorit përdoruesit me ID-në e dhënë
         Që të vazhdohet të përdoret shërbyesi home %1$s, duhet të shqyrtoni dhe pajtoheni me termat dhe kushtet.
         Kjo dhomë është zëvendësuar dhe s’është më aktive
    @@ -902,7 +902,7 @@
         S’arrihet të verifikohet adresë email. Ju lutemi, kontrolloni email-in tuaj dhe klikoni mbi lidhjen që përmban. Pasi të jetë bërë kjo, klikoni që të vazhdohet.
         Këto janë veçori eksperimentale që mund të ngecin në rrugë të papritura. Përdorini me kujdes.
         Lypset të dilni nga llogaria, që të jeni në gjendje të aktivizoni fshehtëzimin.
    -    Mos dërgo kurrë prej këtij sesioni mesazhe të fshehtëzuar te sesione të paverifikuar në këtë dhomë
    +    Mos dërgo kurrë prej këtij sesioni mesazhe të fshehtëzuar te sesione të paverifikuar në këtë dhomë.
     
         %s u rrek të ngarkonte një pikë të dhënë prej rrjedhës kohore në këtë dhomë, por s’qe në gjendje ta gjente.
     
    @@ -911,13 +911,13 @@
     \n
     \nKujdes: kjo kartelë mund të fshihet, nëse çinstalohet aplikacioni.
     
    -    Mos dërgo kurrë mesazhe të fshehtëzuar, nga kjo pajisje te pajisje të paverifikuara.
    +    Mos dërgo kurrë prej këtij sesioni mesazhe të fshehtëzuar te sesione të paverifikuar.
     
         Hiqi verifikimin
         Hiqe nga listë e zezë
     
    -    Që të verifikoni se kësaj pajisje mund t’i zihet besë, ju lutemi, lidhuni me të zotët e saj përmes ndonjë rruge tjetër (p.sh., personalisht, ose përmes një thirrjeje telefonike) dhe kërkojuni nëse kyçi që shohin te Rregullime të tyret të Përdoruesit për këtë pajisje përputhet me kyçin më poshtë:
    -    Nëse përputhet, shtypni butonin e verifikimit më poshtë. Nëse jo, atëherë dikush tjetër po e përgjon këtë pajisje dhe duhet ta kaloni në listë të zezë. Në të ardhmen, ky proces verifikimi do të jetë më i sofistikuar.
    +    Që të verifikoni se këtij sesioni mund t’i zihet besë, ju lutemi, lidhuni me të zotët e saj përmes ndonjë rruge tjetër (p.sh., personalisht, ose përmes një thirrjeje telefonike) dhe pyetini nëse përputhet apo jo kyçi që shohin te Rregullime të tyret të Përdoruesit për këtë sesion me kyçin më poshtë:
    +    Nëse përputhet, shtypni butonin e verifikimit më poshtë. Nëse jo, atëherë dikush tjetër po e përgjon këtë sesion dhe duhet ta kaloni në listë të zezë. Në të ardhmen, ky proces verifikimi do të jetë më i sofistikuar.
         URL Shërbyesi Home
         
             1 dhomë
    @@ -1205,14 +1205,14 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani
         S’u shfshehtëzua dot kopjeruajtja me këtë kyç: ju lutemi, verifikoni që dhatë kyçin e duhur të rimarrjeve.
     
         Kopjeruajtja u Rikthye %s !
    -    U rikthyen %1$d kyçe sesioni, dhe u shtuan %2$d kyç(e) të rinj që nuk njiheshin nga kjo pajisje
    +    U rikthyen %1$d kyçe sesioni, dhe u shtuan %2$d kyç(e) të rinj që nuk njiheshin nga ky sesion
         
             U rikthye një kopjeruajtje me %d kyç.
             U rikthye një kopjeruajtje me %d kyçe.
         
         
    -        Te kjo pajisje u shtua %d kyç i ri.
    -        Te kjo pajisje u shtuan %d kyçe të rinj.
    +        Te ky sesion u shtua %d kyç i ri.
    +        Te ky sesion u shtuan %d kyçe të rinj.
         
     
         S’u arrit të merrej versioni më i ri i kyçeve të rikthimit (%s).
    @@ -1222,17 +1222,17 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani
         Riktheje
         Fshije
     
    -    Kopjeruajtja e Kyçeve për këtë pajisje është rregulluar si duhet.
    -    Kopjeruajtja e Kyçeve për këtë pajisje s’është aktive.
    -    Kyçet tuaj nuk po kopjeruhen nga kjo pajisje.
    +    Kopjeruajtja e Kyçeve për këtë sesion është rregulluar si duhet.
    +    Kopjeruajtja e Kyçeve për këtë sesion s’është aktive.
    +    Kyçet tuaj nuk po kopjeruhen nga ky sesion.
     
     
    -    Kopjeruajtja ka një nënshkrim nga një pajisje e panjohur, me ID %s.
    -    Kopjeruajtja ka një nënshkrim të vlefshëm prej kësaj pajisjeje.
    -    Kopjeruajtja ka një nënshkrim të vlefshëm prej pajisjes së verifikuar %s.
    -    Kopjeruajtja ka një nënshkrim të vlefshëm prej pajisjes së paverifikuar %s
    -    Kopjeruajtja ka një nënshkrim të pavlefshëm prej pajisjes së verifikuar %s
    -    Kopjeruajtja ka një nënshkrim të pavlefshëm prej pajisjes së paverifikuar %s
    +    Kopjeruajtja ka një nënshkrim nga sesion i panjohur me ID: %s.
    +    Kopjeruajtja ka një nënshkrim të vlefshëm prej këtij sesioni.
    +    Kopjeruajtja ka një nënshkrim të vlefshëm prej sesionit të verifikuar %s.
    +    Kopjeruajtja ka një nënshkrim të vlefshëm prej sesionit të paverifikuar %s
    +    Kopjeruajtja ka një nënshkrim të pavlefshëm prej sesionit të verifikuar %s
    +    Kopjeruajtja ka një nënshkrim të pavlefshëm prej sesionit së paverifikuar %s
         S’u arrit të merreshin të dhëna besimi për kopjeruajtjen (%s).
     
         Po fshihet kopjeruajtje…
    @@ -1313,7 +1313,7 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani
         Po përllogariten kyçe rimarrjesh…
         Po shkarkohen kyçe…
         Po importohen kyçe…
    -    Që të përdorni Kopjeruajtje Kyçesh në këtë pajisje, rikthejeni tani përmes frazëkalimit tuaj ose kyçit të rimarrjeve.
    +    Që të përdorni Kopjeruajtje Kyçesh në këtë sesion, rikthejeni tani përmes frazëkalimit tuaj ose kyçit të rikthimeve.
         Kopjeruajtje e Re Kyçesh
         U pikas një kopjeruajtje e re kyçesh mesazhesh të sigurt. 
     \n 
    @@ -1356,14 +1356,13 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani
         Verifiko sesion
     
         ip e panjohur
    -    Një pajisje e re po kërkon kyçe fshehtëzimi.
    -\nEmër pajisje: %1$s
    +    Një sesion i ri po kërkon emër keys.ession fshehtëzimi: %1$s
     \nParë së fundi më: %2$s
    -\nNëse s’keni bërë hyrje në një tjetër pajisje, shpërfilleni këtë kërkesë.
    -    Një pajisje e paverifikuar po kërkon kyçe fshehtëzimi.
    -\nEmër pajisjeje: %1$s
    -\nParë së fundi: %2$s
    -\nNëse s’keni bërë hyrje që nga një pajisje tjetër, shpërfilleni këtë kërkesë.
    +\nNëse s’keni bërë hyrje në një tjetër sesion, shpërfilleni këtë kërkesë.
    +    Një sesion i paverifikuar po kërkon kyçe fshehtëzimi. 
    +\nEmër sesioni: %1$s 
    +\nParë së fundi më: %2$s 
    +\nNëse s’keni bërë hyrje në një sesion tjetër, shpërfilleni këtë kërkesë.
     
         Verifikoje
         Ndaje
    @@ -1415,7 +1414,7 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani
         Gabim i Panjohur
     
         Ka tashmë një kopjeruajtje te Shërbyesi juaj Home
    -    Duket sikur keni tashmë kopjeruajtje kyçesh nga një pajisje tjetër. Doni të zëvendësohet me atë që po krijoni\?
    +    Duket sikur keni tashmë kopjeruajtje kyçesh nga një sesion tjetër. Doni të zëvendësohet me atë që po krijoni\?
         Zëvendësoje
         Ndale
     
    @@ -1828,7 +1827,7 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani
         Fjalëkalim i ri
     
         Kujdes!
    -    Ndryshimi i fjalëkalimit tuaj do të sjellë ricaktim të çfarëdo kyçesh fshehtëzimi skaj-më-skaj në krejt pajisjet tuaja, duke e bërë të palexueshëm historikun e bisedave të fshehtëzuara. Përpara se të ricaktoni fjalëkalimin tuaj, ujdisni një Kopjeruajtje Kyçesh ose eksportoni kyçet e dhomës tuaj prej një tjetër pajisjeje.
    +    Ndryshimi i fjalëkalimit tuaj do të sjellë zerim të çfarëdo kyçesh fshehtëzimi skaj-më-skaj në krejt sesionet tuaj, duke e bërë të palexueshëm historikun e bisedave të fshehtëzuara. Ujdisni një Kopjeruajtje Kyçesh ose eksportoni kyçet e dhomës tuaj prej një tjetër sesioni, përpara se të ricaktoni fjalëkalimin tuaj.
         Vazhdo
     
         Ky email s’është i lidhur me ndonjë llogari
    @@ -1840,7 +1839,7 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani
     
         Sukses!
         Fjalëkalimi juaj u ricaktua.
    -    Jeni nxjerrë jashtë prej krejt pajisjeve dhe s’do të merrni më njoftime push. Që të riaktivizoni njoftimet, bëni sërish hyrjen në çdo pajisje.
    +    Jeni nxjerrë jashtë krejt sesioneve dhe nuk do të merrni më njoftime push. Që të riaktivizoni njoftimet, bëni sërish hyrjen në çdo pajisje.
         Mbrapsht te Hyrja
     
         Kujdes
    @@ -1871,7 +1870,7 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani
         Numri i telefonit duket se është i vlefshëm. Ju lutemi, kontrollojeni
     
         Regjistrohuni te %1$s
    -    Emër përdoruesi ose email
    +    Emër përdoruesi ose email
         Fjalëkalim
         Pasuesi
         Ai emër përdoruesi është i zënë
    @@ -2080,4 +2079,58 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani
         Kod QR
     
         Po
    +    Përkatësia juaj për email-e s’është e autorizuar të regjistrohet në këtë shërbyes
    +
    +    Hyrje jo e besuar
    +    Verifikojeni këtë përdorues duke ripohuar se emoji unik vijues shfaqet në ekranin e tij, në të njëjtën radhë.
    +    Për sigurinë përfundimtare, përdorni mjete të tjera të besuara komunikimi ose bëjeni këtë personalisht.
    +    Shihni për mburojën e gjelbër, për t’u siguruar se një përdorues është i besuar. Besoni krejt përdoruesit në një dhomë për të garantuar që dhoma është e sigurt.
    +
    +    Një nga sa vijon mund të jetë komprometuar:
    +\n
    +\n - Shërbyesi juaj Home
    +\n - Shërbyesi Home te i cili është lidhur përdoruesi që po verifikoni
    +\n - Lidhja juaj internet ose ajo e përdoruesit tjetër
    +\n - Pajisja juaj ose ajo e përdoruesit tjetër
    +
    +    %s u anulua
    +    %s u pranua
    +    Skanojeni kodin me pajisjen e përdoruesit tjetër, për të verifikuar në mënyrë të sigurt njëri-tjetrin
    +    Nëse s’jeni vetë atje, krahasoni emoji-n
    +
    +    Nëse s’skanoni dot kodin më sipër, verifikojeni duke krahasuar një përzgjedhje të shkurtër, unike, emoji-sh.
    +
    +    Për siguri ekstra, verifikojeni %s duke parë kontrolluar në të dy pajisjet tuaja një kod njëpërdorimsh.
    +\n
    +\nPër sigurinë maksimale, bëjeni këtë ju vetë.
    +    RiotX (ende) nuk trajton akte të llojit \'%1$s\'
    +    RiotX (ende) nuk trajton mesazhe të llojit \'%1$s\'
    +    RiotX ndeshi një problem kur vizatohej lëndë e aktit me ID \'%1$s\'
    +
    +    Ky sesion s’është në gjendje të ndajë këtë verifikim me sesionet tuaj të tjerë.
    +\nVerifikimi do të ruhet lokalisht dhe do të ndahet nën një version të ardhshëm të aplikacionit.
    +
    +    Për të qenë i sigurt, verifikoni %s duke kontrolluar një kod njëpërdorimsh.
    +    Për të qenë i sigurt, bëjeni këtë duke qenë vetë i pranishëm ose për të komunikuar përdorni një rrugë tjetër.
    +
    +    Krahasoni emoji-n unik, duke u siguruar se shfaqen në të njëjtën radhë.
    +    Krahasojeni kodin me atë të shfaqur te ekrani i përdoruesit tjetër.
    +    Cross-Signing është i aktivizuar
    +\nKyçet janë të besuar.
    +\nKyçet privatë nuk njihen
    +    Verifikojeni këtë sesion që t’i vihet shenjë si i besuar & dhe t’i akordohet hyrje te mesazhe të fshehtëzuar. Nëse s’keni bërë hyrjen në këtë sesion, llogaria juaj mund të jetë komprometuar:
    +
    +    Hapni një sesion ekzistues & përdoreni për të verifikuar këtë, duke i akorduar hyrje te mesazhe të fshehtëzuar. Nëse s’hyni dot te një, përdorni kyçin ose fjalëkalimin tuaj për rimarrje.
    +
    +
    +    Jo i Besuar
    +
    +    Ky sesion është i besuar për shkëmbim të sigurt të mesazheve, ngaqë e verifikoi %1$s (%2$s):
    +    Gatit CrossSigning
    +    Zeroji Kyçet
    +
    +    A e skanoi me sukses përdoruesi tjetër kodin QR\?
    +    Jo
    +
    +    Humbi lidhja me shërbyesin
     
    diff --git a/vector/src/main/res/values-v21/theme_common.xml b/vector/src/main/res/values-v21/theme_common.xml
    new file mode 100644
    index 0000000000..dfd3007908
    --- /dev/null
    +++ b/vector/src/main/res/values-v21/theme_common.xml
    @@ -0,0 +1,9 @@
    +
    +
    +
    +    
    +
    +
    \ No newline at end of file
    diff --git a/vector/src/main/res/values-zh-rTW/strings.xml b/vector/src/main/res/values-zh-rTW/strings.xml
    index 9e36a2ec3a..8078ca8efa 100644
    --- a/vector/src/main/res/values-zh-rTW/strings.xml
    +++ b/vector/src/main/res/values-zh-rTW/strings.xml
    @@ -1635,7 +1635,7 @@ Matrix 中的消息可見度類似于電子郵件。我們忘記您的郵件意
         可探索的電子郵件地址
         在您新增電子郵件後,探索選項將會出現。
         在您新增電話號碼後,探索選項將會出現。
    -    與您的身份識別伺服器斷線代鰾您無法被其他使用者探索,且您將無法透過電子郵件或電話邀請其他人。
    +    與您的身份識別伺服器斷線代表您無法被其他使用者探索,且您將無法透過電子郵件或電話邀請其他人。
         可探索的電話號碼
         我們將會傳送確認電子郵件到 %s 給您,請檢查您的電子郵件並在確認連結上點選
         擱置中
    @@ -1866,7 +1866,7 @@ Matrix 中的消息可見度類似于電子郵件。我們忘記您的郵件意
         電話號碼似乎無效。請檢查
     
         註冊至 %1$s
    -    使用者名稱或電子郵件
    +    使用者名稱或電子郵件
         密碼
         下一個
         使用者名稱已被使用
    diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
    index 7693bc8cee..a3d449611b 100644
    --- a/vector/src/main/res/values/strings.xml
    +++ b/vector/src/main/res/values/strings.xml
    @@ -1912,7 +1912,8 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
     
         
         Sign up to %1$s
    -    Username or email
    +    Username or email
    +    Username
         Password
         Next
         That username is taken
    @@ -2125,7 +2126,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
         Other users may not trust it
         Complete Security
     
    -    Open an existing session & use it to verify this one, granting it access to encrypted messages. If you can’t access one, use your recovery key or passphrase.
    +    Open an existing session & use it to verify this one, granting it access to encrypted messages.
     
     
         Verify
    @@ -2152,4 +2153,45 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
         No
     
         Connectivity to the server has been lost
    +
    +    Dev Tools
    +    Account Data
    +    
    +        %d vote
    +        %d votes
    +    
    +    
    +        %d vote - Final results
    +        %d votes - Final results
    +    
    +    Selected Option
    +    Creates a simple poll
    +    Can‘t access an existing session?
    +    Use your recovery key or passphrase
    +
    +    New Sign In
    +
    +    Cannot find secrets in storage
    +    Enter secret storage passphrase
    +    Warning:
    +    You should only access secret storage from a trusted device
    +    Access your secure message history and your cross-signing identity for verifying other sessions by entering your passphrase
    +
    +    Remove…
    +    Do you want to send this attachment to %1$s?
    +    
    +        Send image with the original size
    +        Send images with the original size
    +    
    +
    +    Confirm Removal
    +    Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.
    +    Include a reason
    +    Reason for redacting
    +
    +    Event deleted by user, reason: %1$s
    +    Event moderated by room admin, reason: %1$s
    +
    +    Keys are already up to date!
    +
     
    diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml
    index ee70ac91ff..00bf65e121 100644
    --- a/vector/src/main/res/values/strings_riotX.xml
    +++ b/vector/src/main/res/values/strings_riotX.xml
    @@ -6,36 +6,22 @@
         
     
         
    -    
    -        %d vote
    -        %d votes
    -    
    -    
    -        %d vote - Final results
    -        %d votes - Final results
    -    
    -    Selected Option
    -    Creates a simple poll
    +
         
     
     
    -    
    -    Remove…
    -    
    -
    -
         
     
         
     
    +
    +    
    +
    +    
    +
    +
         
    -    Confirm Removal
    -    Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.
    -    Include a reason
    -    Reason for redacting
     
    -    Event deleted by user, reason: %1$s
    -    Event moderated by room admin, reason: %1$s
         
     
     
    diff --git a/vector/src/main/res/values/styles_riot.xml b/vector/src/main/res/values/styles_riot.xml
    index 76d1988f61..c4b42fe4fe 100644
    --- a/vector/src/main/res/values/styles_riot.xml
    +++ b/vector/src/main/res/values/styles_riot.xml
    @@ -30,6 +30,10 @@
             "sans-serif"
         
     
    +    
    +
         
    diff --git a/vector/src/main/res/values/theme_common.xml b/vector/src/main/res/values/theme_common.xml
    index 159b2ecb55..151d97c097 100644
    --- a/vector/src/main/res/values/theme_common.xml
    +++ b/vector/src/main/res/values/theme_common.xml
    @@ -8,4 +8,6 @@
             @color/primary_color_dark
         
     
    +