diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml index a34f4219d9..10c12796c0 100644 --- a/.idea/dictionaries/bmarty.xml +++ b/.idea/dictionaries/bmarty.xml @@ -20,6 +20,7 @@ signin signout signup + threepid \ No newline at end of file diff --git a/CHANGES.md b/CHANGES.md index 3a33a8a940..9f9d49d0a0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,20 +1,17 @@ -Changes in RiotX 0.12.0 (2019-XX-XX) +Changes in RiotX 0.13.0 (2020-XX-XX) =================================================== Features ✨: - Improvements 🙌: - - The initial sync is now handled by a foreground service - - Render aliases and canonical alias change in the timeline - - Fix autocompletion issues and add support for rooms and groups + - Other changes: - Bugfix 🐛: - - Fix avatar image disappearing (#777) - - Fix read marker banner when permalink + - Translations 🗣: - @@ -22,6 +19,32 @@ Translations 🗣: Build 🧱: - +Changes in RiotX 0.12.0 (2020-01-09) +=================================================== + +Improvements 🙌: + - The initial sync is now handled by a foreground service + - Render aliases and canonical alias change in the timeline + - Introduce developer mode in the settings (#745, #796) + - Improve devices list screen + - Add settings for rageshake sensibility + - Fix autocompletion issues and add support for rooms, groups, and emoji (#780) + - Show skip to bottom FAB while scrolling down (#752) + - Enable encryption on a room, SDK part (#212) + +Other changes: + - Change the way RiotX identifies a session to allow the SDK to support several sessions with the same user (#800) + - Exclude play-services-oss-licenses library from F-Droid build (#814) + - Email domain can be limited on some homeservers, i18n of the displayed error (#754) + +Bugfix 🐛: + - Fix crash when opening room creation screen from the room filtering screen + - Fix avatar image disappearing (#777) + - Fix read marker banner when permalink + - Fix joining upgraded rooms (#697) + - Fix matrix.org room directory not being browsable (#807) + - Hide non working settings (#751) + Changes in RiotX 0.11.0 (2019-12-19) =================================================== @@ -281,7 +304,7 @@ Mode details here: https://medium.com/@RiotChat/introducing-the-riotx-beta-for-a ======================================================= -Changes in RiotX 0.0.0 (2019-XX-XX) +Changes in RiotX 0.0.0 (2020-XX-XX) =================================================== Features ✨: diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt index e5ebc536ff..bbf0e76823 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt @@ -17,13 +17,16 @@ package im.vector.matrix.rx import im.vector.matrix.android.api.session.room.Room +import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.model.ReadReceipt +import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.notification.RoomNotificationState import im.vector.matrix.android.api.session.room.send.UserDraft import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.util.Optional +import im.vector.matrix.android.api.util.toOptional import io.reactivex.Observable import io.reactivex.Single @@ -31,18 +34,22 @@ class RxRoom(private val room: Room) { fun liveRoomSummary(): Observable> { return room.getRoomSummaryLive().asObservable() + .startWith(room.roomSummary().toOptional()) } - fun liveRoomMemberIds(): Observable> { - return room.getRoomMemberIdsLive().asObservable() + fun liveRoomMembers(queryParams: RoomMemberQueryParams): Observable> { + return room.getRoomMembersLive(queryParams).asObservable() + .startWith(room.getRoomMembers(queryParams)) } fun liveAnnotationSummary(eventId: String): Observable> { - return room.getEventSummaryLive(eventId).asObservable() + return room.getEventAnnotationsSummaryLive(eventId).asObservable() + .startWith(room.getEventAnnotationsSummary(eventId).toOptional()) } fun liveTimelineEvent(eventId: String): Observable> { return room.getTimeLineEventLive(eventId).asObservable() + .startWith(room.getTimeLineEvent(eventId).toOptional()) } fun liveReadMarker(): Observable> { diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt index c9381b861d..084f497de5 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt @@ -18,8 +18,10 @@ package im.vector.matrix.rx import androidx.paging.PagedList import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.group.GroupSummaryQueryParams import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.pushers.Pusher +import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.sync.SyncState @@ -30,40 +32,43 @@ import io.reactivex.Single class RxSession(private val session: Session) { - fun liveRoomSummaries(): Observable> { - return session.liveRoomSummaries().asObservable() + fun liveRoomSummaries(queryParams: RoomSummaryQueryParams): Observable> { + return session.getRoomSummariesLive(queryParams).asObservable() + .startWith(session.getRoomSummaries(queryParams)) } - fun liveGroupSummaries(): Observable> { - return session.liveGroupSummaries().asObservable() + fun liveGroupSummaries(queryParams: GroupSummaryQueryParams): Observable> { + return session.getGroupSummariesLive(queryParams).asObservable() + .startWith(session.getGroupSummaries(queryParams)) } fun liveBreadcrumbs(): Observable> { - return session.liveBreadcrumbs().asObservable() + return session.getBreadcrumbsLive().asObservable() + .startWith(session.getBreadcrumbs()) } fun liveSyncState(): Observable { - return session.syncState().asObservable() + return session.getSyncStateLive().asObservable() } fun livePushers(): Observable> { - return session.livePushers().asObservable() + return session.getPushersLive().asObservable() } fun liveUser(userId: String): Observable> { - return session.liveUser(userId).asObservable().distinctUntilChanged() + return session.getUserLive(userId).asObservable().distinctUntilChanged() } fun liveUsers(): Observable> { - return session.liveUsers().asObservable() + return session.getUsersLive().asObservable() } fun liveIgnoredUsers(): Observable> { - return session.liveIgnoredUsers().asObservable() + return session.getIgnoredUsersLive().asObservable() } fun livePagedUsers(filter: String? = null): Observable> { - return session.livePagedUsers(filter).asObservable() + return session.getPagedUsersLive(filter).asObservable() } fun createRoom(roomParams: CreateRoomParams): Single = singleBuilder { diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index ab5f122dbc..7a1348a54c 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -10,7 +10,7 @@ buildscript { jcenter() } dependencies { - classpath "io.realm:realm-gradle-plugin:5.12.0" + classpath "io.realm:realm-gradle-plugin:6.0.2" } } @@ -102,7 +102,6 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation "androidx.appcompat:appcompat:1.1.0" - implementation "androidx.recyclerview:recyclerview:1.1.0-beta05" implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version" @@ -119,14 +118,14 @@ dependencies { implementation "ru.noties.markwon:core:$markwon_version" // Image - implementation 'androidx.exifinterface:exifinterface:1.0.0' + implementation 'androidx.exifinterface:exifinterface:1.1.0' // Database implementation 'com.github.Zhuinden:realm-monarchy:0.5.1' kapt 'dk.ilios:realmfieldnameshelper:1.1.1' // Work - implementation "androidx.work:work-runtime-ktx:2.3.0-alpha01" + implementation "androidx.work:work-runtime-ktx:2.3.0-beta02" // FP implementation "io.arrow-kt:arrow-core:$arrow_version" diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/account/AccountCreationTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/account/AccountCreationTest.kt new file mode 100644 index 0000000000..c44ac9c47b --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/account/AccountCreationTest.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.account + +import im.vector.matrix.android.InstrumentedTest +import im.vector.matrix.android.common.CommonTestHelper +import im.vector.matrix.android.common.CryptoTestHelper +import im.vector.matrix.android.common.SessionTestParams +import im.vector.matrix.android.common.TestConstants +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class AccountCreationTest : InstrumentedTest { + + private val commonTestHelper = CommonTestHelper(context()) + private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + + @Test + fun createAccountTest() { + val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true)) + + commonTestHelper.signout(session) + + session.close() + } + + @Test + fun createAccountAndLoginAgainTest() { + val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true)) + + // Log again to the same account + val session2 = commonTestHelper.logIntoAccount(session.myUserId, SessionTestParams(withInitialSync = true)) + + session.close() + session2.close() + } + + @Test + fun simpleE2eTest() { + val res = cryptoTestHelper.doE2ETestWithAliceInARoom() + + res.close() + } +} diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt new file mode 100644 index 0000000000..b16c865765 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt @@ -0,0 +1,278 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.common + +import android.content.Context +import android.net.Uri +import im.vector.matrix.android.api.Matrix +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.MatrixConfiguration +import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig +import im.vector.matrix.android.api.auth.data.LoginFlowResult +import im.vector.matrix.android.api.auth.registration.RegistrationResult +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.room.Room +import im.vector.matrix.android.api.session.room.timeline.Timeline +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.api.session.room.timeline.TimelineSettings +import org.junit.Assert.* +import java.util.* +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +/** + * This class exposes methods to be used in common cases + * Registration, login, Sync, Sending messages... + */ +class CommonTestHelper(context: Context) { + + val matrix: Matrix + + init { + Matrix.initialize(context, MatrixConfiguration("TestFlavor")) + + matrix = Matrix.getInstance(context) + } + + fun createAccount(userNamePrefix: String, testParams: SessionTestParams): Session { + return createAccount(userNamePrefix, TestConstants.PASSWORD, testParams) + } + + fun logIntoAccount(userId: String, testParams: SessionTestParams): Session { + return logIntoAccount(userId, TestConstants.PASSWORD, testParams) + } + + /** + * Create a Home server configuration, with Http connection allowed for test + */ + fun createHomeServerConfig(): HomeServerConnectionConfig { + return HomeServerConnectionConfig.Builder() + .withHomeServerUri(Uri.parse(TestConstants.TESTS_HOME_SERVER_URL)) + .build() + } + + /** + * This methods init the event stream and check for initial sync + * + * @param session the session to sync + */ + fun syncSession(session: Session) { + // val lock = CountDownLatch(1) + + // val observer = androidx.lifecycle.Observer { syncState -> + // if (syncState is SyncState.Idle) { + // lock.countDown() + // } + // } + + // TODO observe? + // while (session.syncState().value !is SyncState.Idle) { + // sleep(100) + // } + + session.open() + session.startSync(true) + // await(lock) + // session.syncState().removeObserver(observer) + } + + /** + * Sends text messages in a room + * + * @param room the room where to send the messages + * @param message the message to send + * @param nbOfMessages the number of time the message will be sent + */ + fun sendTextMessage(room: Room, message: String, nbOfMessages: Int): List { + val sentEvents = ArrayList(nbOfMessages) + val latch = CountDownLatch(nbOfMessages) + val onEventSentListener = object : Timeline.Listener { + override fun onTimelineFailure(throwable: Throwable) { + } + + override fun onTimelineUpdated(snapshot: List) { + // TODO Count only new messages? + if (snapshot.count { it.root.type == EventType.MESSAGE } == nbOfMessages) { + sentEvents.addAll(snapshot.filter { it.root.type == EventType.MESSAGE }) + latch.countDown() + } + } + } + val timeline = room.createTimeline(null, TimelineSettings(10)) + timeline.addListener(onEventSentListener) + for (i in 0 until nbOfMessages) { + room.sendTextMessage(message + " #" + (i + 1)) + } + await(latch) + timeline.removeListener(onEventSentListener) + + // Check that all events has been created + assertEquals(nbOfMessages.toLong(), sentEvents.size.toLong()) + + return sentEvents + } + + // PRIVATE METHODS ***************************************************************************** + + /** + * Creates a unique account + * + * @param userNamePrefix the user name prefix + * @param password the password + * @param testParams test params about the session + * @return the session associated with the newly created account + */ + private fun createAccount(userNamePrefix: String, + password: String, + testParams: SessionTestParams): Session { + val session = createAccountAndSync( + userNamePrefix + "_" + System.currentTimeMillis() + UUID.randomUUID(), + password, + testParams + ) + assertNotNull(session) + return session + } + + /** + * Logs into an existing account + * + * @param userId the userId to log in + * @param password the password to log in + * @param testParams test params about the session + * @return the session associated with the existing account + */ + private fun logIntoAccount(userId: String, + password: String, + testParams: SessionTestParams): Session { + val session = logAccountAndSync(userId, password, testParams) + assertNotNull(session) + return session + } + + /** + * Create an account and a dedicated session + * + * @param userName the account username + * @param password the password + * @param sessionTestParams parameters for the test + */ + private fun createAccountAndSync(userName: String, + password: String, + sessionTestParams: SessionTestParams): Session { + val hs = createHomeServerConfig() + + doSync { + matrix.authenticationService + .getLoginFlow(hs, it) + } + + doSync { + matrix.authenticationService + .getRegistrationWizard() + .createAccount(userName, password, null, it) + } + + // Preform dummy step + val registrationResult = doSync { + matrix.authenticationService + .getRegistrationWizard() + .dummy(it) + } + + assertTrue(registrationResult is RegistrationResult.Success) + val session = (registrationResult as RegistrationResult.Success).session + if (sessionTestParams.withInitialSync) { + syncSession(session) + } + + return session + } + + /** + * Start an account login + * + * @param userName the account username + * @param password the password + * @param sessionTestParams session test params + */ + private fun logAccountAndSync(userName: String, + password: String, + sessionTestParams: SessionTestParams): Session { + val hs = createHomeServerConfig() + + doSync { + matrix.authenticationService + .getLoginFlow(hs, it) + } + + val session = doSync { + matrix.authenticationService + .getLoginWizard() + .login(userName, password, "myDevice", it) + } + + if (sessionTestParams.withInitialSync) { + syncSession(session) + } + + return session + } + + /** + * Await for a latch and ensure the result is true + * + * @param latch + * @throws InterruptedException + */ + fun await(latch: CountDownLatch) { + assertTrue(latch.await(TestConstants.timeOutMillis, TimeUnit.MILLISECONDS)) + } + + // Transform a method with a MatrixCallback to a synchronous method + inline fun doSync(block: (MatrixCallback) -> Unit): T { + val lock = CountDownLatch(1) + var result: T? = null + + val callback = object : TestMatrixCallback(lock) { + override fun onSuccess(data: T) { + result = data + super.onSuccess(data) + } + } + + block.invoke(callback) + + await(lock) + + assertNotNull(result) + return result!! + } + + /** + * Clear all provided sessions + */ + fun Iterable.close() = forEach { it.close() } + + fun signout(session: Session) { + val lock = CountDownLatch(1) + session.signOut(true, object : TestMatrixCallback(lock) {}) + await(lock) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestData.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestData.kt new file mode 100644 index 0000000000..8ad9f1ec6f --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestData.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.common + +import im.vector.matrix.android.api.session.Session + +data class CryptoTestData(val firstSession: Session, + val roomId: String, + val secondSession: Session? = null, + val thirdSession: Session? = null) { + + fun close() { + firstSession.close() + secondSession?.close() + secondSession?.close() + } +} diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt new file mode 100644 index 0000000000..df45249265 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt @@ -0,0 +1,335 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.common + +import android.os.SystemClock +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.toContent +import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams +import im.vector.matrix.android.api.session.room.timeline.Timeline +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.api.session.room.timeline.TimelineSettings +import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP +import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupAuthData +import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo +import org.junit.Assert.* +import java.util.* +import java.util.concurrent.CountDownLatch + +class CryptoTestHelper(val mTestHelper: CommonTestHelper) { + + val messagesFromAlice: List = Arrays.asList("0 - Hello I'm Alice!", "4 - Go!") + val messagesFromBob: List = Arrays.asList("1 - Hello I'm Bob!", "2 - Isn't life grand?", "3 - Let's go to the opera.") + + val defaultSessionParams = SessionTestParams(true) + + /** + * @return alice session + */ + fun doE2ETestWithAliceInARoom(): CryptoTestData { + val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams) + + var roomId: String? = null + val lock1 = CountDownLatch(1) + + aliceSession.createRoom(CreateRoomParams().apply { name = "MyRoom" }, object : TestMatrixCallback(lock1) { + override fun onSuccess(data: String) { + roomId = data + super.onSuccess(data) + } + }) + + mTestHelper.await(lock1) + assertNotNull(roomId) + + val room = aliceSession.getRoom(roomId!!)!! + + val lock2 = CountDownLatch(1) + room.enableEncryptionWithAlgorithm(MXCRYPTO_ALGORITHM_MEGOLM, object : TestMatrixCallback(lock2) {}) + mTestHelper.await(lock2) + + return CryptoTestData(aliceSession, roomId!!) + } + + /** + * @return alice and bob sessions + */ + fun doE2ETestWithAliceAndBobInARoom(): CryptoTestData { + val statuses = HashMap() + + val cryptoTestData = doE2ETestWithAliceInARoom() + val aliceSession = cryptoTestData.firstSession + val aliceRoomId = cryptoTestData.roomId + + val room = aliceSession.getRoom(aliceRoomId)!! + + val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, defaultSessionParams) + + val lock1 = CountDownLatch(2) + +// val bobEventListener = object : MXEventListener() { +// override fun onNewRoom(roomId: String) { +// if (TextUtils.equals(roomId, aliceRoomId)) { +// if (!statuses.containsKey("onNewRoom")) { +// statuses["onNewRoom"] = "onNewRoom" +// lock1.countDown() +// } +// } +// } +// } +// +// bobSession.dataHandler.addListener(bobEventListener) + + room.invite(bobSession.myUserId, callback = object : TestMatrixCallback(lock1) { + override fun onSuccess(data: Unit) { + statuses["invite"] = "invite" + super.onSuccess(data) + } + }) + + mTestHelper.await(lock1) + + assertTrue(statuses.containsKey("invite") && statuses.containsKey("onNewRoom")) + +// bobSession.dataHandler.removeListener(bobEventListener) + + val lock2 = CountDownLatch(2) + + bobSession.joinRoom(aliceRoomId, callback = TestMatrixCallback(lock2)) + +// room.addEventListener(object : MXEventListener() { +// override fun onLiveEvent(event: Event, roomState: RoomState) { +// if (TextUtils.equals(event.getType(), Event.EVENT_TYPE_STATE_ROOM_MEMBER)) { +// val contentToConsider = event.contentAsJsonObject +// val member = JsonUtils.toRoomMember(contentToConsider) +// +// if (TextUtils.equals(member.membership, RoomMember.MEMBERSHIP_JOIN)) { +// statuses["AliceJoin"] = "AliceJoin" +// lock2.countDown() +// } +// } +// } +// }) + + mTestHelper.await(lock2) + + // Ensure bob can send messages to the room +// val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!! +// assertNotNull(roomFromBobPOV.powerLevels) +// assertTrue(roomFromBobPOV.powerLevels.maySendMessage(bobSession.myUserId)) + + assertTrue(statuses.toString() + "", statuses.containsKey("AliceJoin")) + +// bobSession.dataHandler.removeListener(bobEventListener) + + return CryptoTestData(aliceSession, aliceRoomId, bobSession) + } + + /** + * @return Alice, Bob and Sam session + */ + fun doE2ETestWithAliceAndBobAndSamInARoom(): CryptoTestData { + val statuses = HashMap() + + val cryptoTestData = doE2ETestWithAliceAndBobInARoom() + val aliceSession = cryptoTestData.firstSession + val aliceRoomId = cryptoTestData.roomId + + val room = aliceSession.getRoom(aliceRoomId)!! + + val samSession = mTestHelper.createAccount(TestConstants.USER_SAM, defaultSessionParams) + + val lock1 = CountDownLatch(2) + +// val samEventListener = object : MXEventListener() { +// override fun onNewRoom(roomId: String) { +// if (TextUtils.equals(roomId, aliceRoomId)) { +// if (!statuses.containsKey("onNewRoom")) { +// statuses["onNewRoom"] = "onNewRoom" +// lock1.countDown() +// } +// } +// } +// } +// +// samSession.dataHandler.addListener(samEventListener) + + room.invite(samSession.myUserId, null, object : TestMatrixCallback(lock1) { + override fun onSuccess(data: Unit) { + statuses["invite"] = "invite" + super.onSuccess(data) + } + }) + + mTestHelper.await(lock1) + + assertTrue(statuses.containsKey("invite") && statuses.containsKey("onNewRoom")) + +// samSession.dataHandler.removeListener(samEventListener) + + val lock2 = CountDownLatch(1) + + samSession.joinRoom(aliceRoomId, null, object : TestMatrixCallback(lock2) { + override fun onSuccess(data: Unit) { + statuses["joinRoom"] = "joinRoom" + super.onSuccess(data) + } + }) + + mTestHelper.await(lock2) + assertTrue(statuses.containsKey("joinRoom")) + + // wait the initial sync + SystemClock.sleep(1000) + +// samSession.dataHandler.removeListener(samEventListener) + + return CryptoTestData(aliceSession, aliceRoomId, cryptoTestData.secondSession, samSession) + } + + /** + * @return Alice and Bob sessions + */ + fun doE2ETestWithAliceAndBobInARoomWithEncryptedMessages(): CryptoTestData { + val cryptoTestData = doE2ETestWithAliceAndBobInARoom() + val aliceSession = cryptoTestData.firstSession + val aliceRoomId = cryptoTestData.roomId + val bobSession = cryptoTestData.secondSession!! + + bobSession.setWarnOnUnknownDevices(false) + + aliceSession.setWarnOnUnknownDevices(false) + + val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!! + val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!! + + var lock = CountDownLatch(1) + + val bobEventsListener = object : Timeline.Listener { + override fun onTimelineFailure(throwable: Throwable) { + } + + override fun onTimelineUpdated(snapshot: List) { + val size = snapshot.filter { it.root.senderId != bobSession.myUserId && it.root.getClearType() == EventType.MESSAGE } + .size + + if (size == 3) { + lock.countDown() + } + } + } + + val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(10)) + bobTimeline.addListener(bobEventsListener) + + val results = HashMap() + + // bobSession.dataHandler.addListener(object : MXEventListener() { + // override fun onToDeviceEvent(event: Event) { + // results["onToDeviceEvent"] = event + // lock.countDown() + // } + // }) + + // Alice sends a message + roomFromAlicePOV.sendTextMessage(messagesFromAlice[0]) + assertTrue(results.containsKey("onToDeviceEvent")) +// assertEquals(1, messagesReceivedByBobCount) + + // Bob send a message + lock = CountDownLatch(1) + roomFromBobPOV.sendTextMessage(messagesFromBob[0]) + // android does not echo the messages sent from itself +// messagesReceivedByBobCount++ + mTestHelper.await(lock) +// assertEquals(2, messagesReceivedByBobCount) + + // Bob send a message + lock = CountDownLatch(1) + roomFromBobPOV.sendTextMessage(messagesFromBob[1]) + // android does not echo the messages sent from itself +// messagesReceivedByBobCount++ + mTestHelper.await(lock) +// assertEquals(3, messagesReceivedByBobCount) + + // Bob send a message + lock = CountDownLatch(1) + roomFromBobPOV.sendTextMessage(messagesFromBob[2]) + // android does not echo the messages sent from itself +// messagesReceivedByBobCount++ + mTestHelper.await(lock) +// assertEquals(4, messagesReceivedByBobCount) + + // Alice sends a message + lock = CountDownLatch(2) + roomFromAlicePOV.sendTextMessage(messagesFromAlice[1]) + mTestHelper.await(lock) +// assertEquals(5, messagesReceivedByBobCount) + + bobTimeline.removeListener(bobEventsListener) + + return cryptoTestData + } + + fun checkEncryptedEvent(event: Event, roomId: String, clearMessage: String, senderSession: Session) { + assertEquals(EventType.ENCRYPTED, event.type) + assertNotNull(event.content) + + val eventWireContent = event.content.toContent() + assertNotNull(eventWireContent) + + assertNull(eventWireContent.get("body")) + assertEquals(MXCRYPTO_ALGORITHM_MEGOLM, eventWireContent.get("algorithm")) + + assertNotNull(eventWireContent.get("ciphertext")) + assertNotNull(eventWireContent.get("session_id")) + assertNotNull(eventWireContent.get("sender_key")) + + assertEquals(senderSession.sessionParams.credentials.deviceId, eventWireContent.get("device_id")) + + assertNotNull(event.eventId) + assertEquals(roomId, event.roomId) + assertEquals(EventType.MESSAGE, event.getClearType()) + // TODO assertTrue(event.getAge() < 10000) + + val eventContent = event.toContent() + assertNotNull(eventContent) + assertEquals(clearMessage, eventContent.get("body")) + assertEquals(senderSession.myUserId, event.senderId) + } + + fun createFakeMegolmBackupAuthData(): MegolmBackupAuthData { + return MegolmBackupAuthData( + publicKey = "abcdefg", + signatures = HashMap>().apply { + this["something"] = HashMap().apply { + this["ed25519:something"] = "hijklmnop" + } + } + ) + } + + fun createFakeMegolmBackupCreationInfo(): MegolmBackupCreationInfo { + return MegolmBackupCreationInfo().apply { + algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP + authData = createFakeMegolmBackupAuthData() + } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/MockOkHttpInterceptor.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/MockOkHttpInterceptor.kt new file mode 100644 index 0000000000..a1f95424a6 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/MockOkHttpInterceptor.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.common + +import okhttp3.Interceptor +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.Response +import okhttp3.ResponseBody.Companion.toResponseBody +import javax.net.ssl.HttpsURLConnection + +/** + * Allows to intercept network requests for test purpose by + * - re-writing the response + * - changing the response code (200/404/etc..). + * - Test delays.. + * + * Basic usage: + * + * val mockInterceptor = MockOkHttpInterceptor() + * mockInterceptor.addRule(MockOkHttpInterceptor.SimpleRule(".well-known/matrix/client", 200, "{}")) + * + * RestHttpClientFactoryProvider.defaultProvider = RestClientHttpClientFactory(mockInterceptor) + * AutoDiscovery().findClientConfig("matrix.org", ) + * + */ +class MockOkHttpInterceptor : Interceptor { + + private var rules: ArrayList = ArrayList() + + fun addRule(rule: Rule) { + rules.add(rule) + } + + override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest = chain.request() + + rules.forEach { rule -> + if (originalRequest.url.toString().contains(rule.match)) { + rule.process(originalRequest)?.let { + return it + } + } + } + + return chain.proceed(originalRequest) + } + + abstract class Rule(val match: String) { + abstract fun process(originalRequest: Request): Response? + } + + /** + * Simple rule that reply with the given body for any request that matches the match param + */ + class SimpleRule(match: String, + private val code: Int = HttpsURLConnection.HTTP_OK, + private val body: String = "{}") : Rule(match) { + + override fun process(originalRequest: Request): Response? { + return Response.Builder() + .protocol(Protocol.HTTP_1_1) + .request(originalRequest) + .message("mocked answer") + .body(body.toResponseBody(null)) + .code(code) + .build() + } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/SessionTestParams.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/SessionTestParams.kt new file mode 100644 index 0000000000..7d1d23e951 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/SessionTestParams.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.common + +data class SessionTestParams @JvmOverloads constructor(val withInitialSync: Boolean = false) diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestAssertUtil.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestAssertUtil.kt new file mode 100644 index 0000000000..2a62165210 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestAssertUtil.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.common + +import org.junit.Assert.* + +/** + * Compare two lists and their content + */ +fun assertListEquals(list1: List?, list2: List?) { + if (list1 == null) { + assertNull(list2) + } else { + assertNotNull(list2) + + assertEquals("List sizes must match", list1.size, list2!!.size) + + for (i in list1.indices) { + assertEquals("Elements at index $i are not equal", list1[i], list2[i]) + } + } +} + +/** + * Compare two maps and their content + */ +fun assertDictEquals(dict1: Map?, dict2: Map?) { + if (dict1 == null) { + assertNull(dict2) + } else { + assertNotNull(dict2) + + assertEquals("Map sizes must match", dict1.size, dict2!!.size) + + for (i in dict1.keys) { + assertEquals("Values for key $i are not equal", dict1[i], dict2[i]) + } + } +} + +/** + * Compare two byte arrays content. + * Note that if the arrays have not the same size, it also fails. + */ +fun assertByteArrayNotEqual(a1: ByteArray, a2: ByteArray) { + if (a1.size != a2.size) { + fail("Arrays have not the same size.") + } + + for (index in a1.indices) { + if (a1[index] != a2[index]) { + // Difference found! + return + } + } + + fail("Arrays are equals.") +} diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestConstants.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestConstants.kt new file mode 100644 index 0000000000..60cc87d330 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestConstants.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.common + +import android.os.Debug + +object TestConstants { + + const val TESTS_HOME_SERVER_URL = "http://10.0.2.2:8080" + + // Time out to use when waiting for server response. 60s + private const val AWAIT_TIME_OUT_MILLIS = 60000 + + // Time out to use when waiting for server response, when the debugger is connected. 10 minutes + private const val AWAIT_TIME_OUT_WITH_DEBUGGER_MILLIS = 10 * 60000 + + const val USER_ALICE = "Alice" + const val USER_BOB = "Bob" + const val USER_SAM = "Sam" + + const val PASSWORD = "password" + + val timeOutMillis: Long + get() = if (Debug.isDebuggerConnected()) { + // Wait more + AWAIT_TIME_OUT_WITH_DEBUGGER_MILLIS.toLong() + } else { + AWAIT_TIME_OUT_MILLIS.toLong() + } +} diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestMatrixCallback.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestMatrixCallback.kt new file mode 100644 index 0000000000..c04777440b --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestMatrixCallback.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.common + +import androidx.annotation.CallSuper +import im.vector.matrix.android.api.MatrixCallback +import org.junit.Assert.fail +import timber.log.Timber +import java.util.concurrent.CountDownLatch + +/** + * Simple implementation of MatrixCallback, which count down the CountDownLatch on each API callback + * @param onlySuccessful true to fail if an error occurs. This is the default behavior + * @param + */ +open class TestMatrixCallback(private val countDownLatch: CountDownLatch, + private val onlySuccessful: Boolean = true) : MatrixCallback { + + @CallSuper + override fun onSuccess(data: T) { + countDownLatch.countDown() + } + + @CallSuper + override fun onFailure(failure: Throwable) { + Timber.e(failure, "TestApiCallback") + + if (onlySuccessful) { + fail("onFailure " + failure.localizedMessage) + } + + countDownLatch.countDown() + } +} diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/AttachmentEncryptionTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/AttachmentEncryptionTest.kt new file mode 100644 index 0000000000..84b3f24191 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/AttachmentEncryptionTest.kt @@ -0,0 +1,148 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto + +import android.os.MemoryFile +import android.util.Base64 +import androidx.test.ext.junit.runners.AndroidJUnit4 +import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments +import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo +import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileKey +import org.junit.Assert.* +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import java.io.ByteArrayInputStream +import java.io.InputStream + +/** + * Unit tests AttachmentEncryptionTest. + */ +@Suppress("SpellCheckingInspection") +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class AttachmentEncryptionTest { + + private fun checkDecryption(input: String, encryptedFileInfo: EncryptedFileInfo): String { + val `in` = Base64.decode(input, Base64.DEFAULT) + + val inputStream: InputStream + + inputStream = if (`in`.isEmpty()) { + ByteArrayInputStream(`in`) + } else { + val memoryFile = MemoryFile("file" + System.currentTimeMillis(), `in`.size) + memoryFile.outputStream.write(`in`) + memoryFile.inputStream + } + + val decryptedStream = MXEncryptedAttachments.decryptAttachment(inputStream, encryptedFileInfo) + + assertNotNull(decryptedStream) + + inputStream.close() + + val buffer = ByteArray(100) + + val len = decryptedStream!!.read(buffer) + + decryptedStream.close() + + return Base64.encodeToString(buffer, 0, len, Base64.DEFAULT).replace("\n".toRegex(), "").replace("=".toRegex(), "") + } + + @Test + fun checkDecrypt1() { + val encryptedFileInfo = EncryptedFileInfo( + v = "v2", + hashes = mapOf("sha256" to "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU"), + key = EncryptedFileKey( + alg = "A256CTR", + k = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + key_ops = listOf("encrypt", "decrypt"), + kty = "oct", + ext = true + ), + iv = "AAAAAAAAAAAAAAAAAAAAAA", + url = "dummyUrl" + ) + + assertEquals("", checkDecryption("", encryptedFileInfo)) + } + + @Test + fun checkDecrypt2() { + val encryptedFileInfo = EncryptedFileInfo( + v = "v2", + hashes = mapOf("sha256" to "YzF08lARDdOCzJpzuSwsjTNlQc4pHxpdHcXiD/wpK6k"), + key = EncryptedFileKey( + alg = "A256CTR", + k = "__________________________________________8", + key_ops = listOf("encrypt", "decrypt"), + kty = "oct", + ext = true + ), + iv = "//////////8AAAAAAAAAAA", + url = "dummyUrl" + ) + + assertEquals("SGVsbG8sIFdvcmxk", checkDecryption("5xJZTt5cQicm+9f4", encryptedFileInfo)) + } + + @Test + fun checkDecrypt3() { + val encryptedFileInfo = EncryptedFileInfo( + v = "v2", + hashes = mapOf("sha256" to "IOq7/dHHB+mfHfxlRY5XMeCWEwTPmlf4cJcgrkf6fVU"), + key = EncryptedFileKey( + alg = "A256CTR", + k = "__________________________________________8", + key_ops = listOf("encrypt", "decrypt"), + kty = "oct", + ext = true + ), + iv = "//////////8AAAAAAAAAAA", + url = "dummyUrl" + ) + + assertEquals("YWxwaGFudW1lcmljYWxseWFscGhhbnVtZXJpY2FsbHlhbHBoYW51bWVyaWNhbGx5YWxwaGFudW1lcmljYWxseQ", + checkDecryption("zhtFStAeFx0s+9L/sSQO+WQMtldqYEHqTxMduJrCIpnkyer09kxJJuA4K+adQE4w+7jZe/vR9kIcqj9rOhDR8Q", + encryptedFileInfo)) + } + + @Test + fun checkDecrypt4() { + val encryptedFileInfo = EncryptedFileInfo( + v = "v2", + hashes = mapOf("sha256" to "LYG/orOViuFwovJpv2YMLSsmVKwLt7pY3f8SYM7KU5E"), + key = EncryptedFileKey( + alg = "A256CTR", + k = "__________________________________________8", + key_ops = listOf("encrypt", "decrypt"), + kty = "oct", + ext = true + ), + iv = "/////////////////////w", + url = "dummyUrl" + ) + + assertNotEquals("YWxwaGFudW1lcmljYWxseWFscGhhbnVtZXJpY2FsbHlhbHBoYW51bWVyaWNhbGx5YWxwaGFudW1lcmljYWxseQ", + checkDecryption("tJVNBVJ/vl36UQt4Y5e5m84bRUrQHhcdLPvS/7EkDvlkDLZXamBB6k8THbiawiKZ5Mnq9PZMSSbgOCvmnUBOMA", + encryptedFileInfo)) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/ExportEncryptionTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/ExportEncryptionTest.kt new file mode 100644 index 0000000000..89ed3c2e65 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/ExportEncryptionTest.kt @@ -0,0 +1,206 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.* +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters + +/** + * Unit tests ExportEncryptionTest. + */ +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class ExportEncryptionTest { + + @Test + fun checkExportError1() { + val password = "password" + val input = "-----" + var failed = false + + try { + MXMegolmExportEncryption.decryptMegolmKeyFile(input.toByteArray(charset("UTF-8")), password) + } catch (e: Exception) { + failed = true + } + + assertTrue(failed) + } + + @Test + fun checkExportError2() { + val password = "password" + val input = "-----BEGIN MEGOLM SESSION DATA-----\n" + "-----" + var failed = false + + try { + MXMegolmExportEncryption.decryptMegolmKeyFile(input.toByteArray(charset("UTF-8")), password) + } catch (e: Exception) { + failed = true + } + + assertTrue(failed) + } + + @Test + fun checkExportError3() { + val password = "password" + val input = "-----BEGIN MEGOLM SESSION DATA-----\n" + + " AXNhbHRzYWx0c2FsdHNhbHSIiIiIiIiIiIiIiIiIiIiIAAAACmIRUW2OjZ3L2l6j9h0lHlV3M2dx\n" + + " cissyYBxjsfsAn\n" + + " -----END MEGOLM SESSION DATA-----" + var failed = false + + try { + MXMegolmExportEncryption.decryptMegolmKeyFile(input.toByteArray(charset("UTF-8")), password) + } catch (e: Exception) { + failed = true + } + + assertTrue(failed) + } + + @Test + fun checkExportDecrypt1() { + val password = "password" + val input = "-----BEGIN MEGOLM SESSION DATA-----\nAXNhbHRzYWx0c2FsdHNhbHSIiIiIiIiIiIiIiIiIiIiIAAAACmIRUW2OjZ3L2l6j9h0lHlV3M2dx\n" + "cissyYBxjsfsAndErh065A8=\n-----END MEGOLM SESSION DATA-----" + val expectedString = "plain" + + var decodedString: String? = null + try { + decodedString = MXMegolmExportEncryption.decryptMegolmKeyFile(input.toByteArray(charset("UTF-8")), password) + } catch (e: Exception) { + fail("## checkExportDecrypt1() failed : " + e.message) + } + + assertEquals("## checkExportDecrypt1() : expectedString $expectedString -- decodedString $decodedString", + expectedString, + decodedString) + } + + @Test + fun checkExportDecrypt2() { + val password = "betterpassword" + val input = "-----BEGIN MEGOLM SESSION DATA-----\nAW1vcmVzYWx0bW9yZXNhbHT//////////wAAAAAAAAAAAAAD6KyBpe1Niv5M5NPm4ZATsJo5nghk\n" + "KYu63a0YQ5DRhUWEKk7CcMkrKnAUiZny\n-----END MEGOLM SESSION DATA-----" + val expectedString = "Hello, World" + + var decodedString: String? = null + try { + decodedString = MXMegolmExportEncryption.decryptMegolmKeyFile(input.toByteArray(charset("UTF-8")), password) + } catch (e: Exception) { + fail("## checkExportDecrypt2() failed : " + e.message) + } + + assertEquals("## checkExportDecrypt2() : expectedString $expectedString -- decodedString $decodedString", + expectedString, + decodedString) + } + + @Test + fun checkExportDecrypt3() { + val password = "SWORDFISH" + val input = "-----BEGIN MEGOLM SESSION DATA-----\nAXllc3NhbHR5Z29vZG5lc3P//////////wAAAAAAAAAAAAAD6OIW+Je7gwvjd4kYrb+49gKCfExw\n" + "MgJBMD4mrhLkmgAngwR1pHjbWXaoGybtiAYr0moQ93GrBQsCzPbvl82rZhaXO3iH5uHo/RCEpOqp\nPgg29363BGR+/Ripq/VCLKGNbw==\n-----END MEGOLM SESSION DATA-----" + val expectedString = "alphanumericallyalphanumericallyalphanumericallyalphanumerically" + + var decodedString: String? = null + try { + decodedString = MXMegolmExportEncryption.decryptMegolmKeyFile(input.toByteArray(charset("UTF-8")), password) + } catch (e: Exception) { + fail("## checkExportDecrypt3() failed : " + e.message) + } + + assertEquals("## checkExportDecrypt3() : expectedString $expectedString -- decodedString $decodedString", + expectedString, + decodedString) + } + + @Test + fun checkExportEncrypt1() { + val password = "password" + val expectedString = "plain" + var decodedString: String? = null + + try { + decodedString = MXMegolmExportEncryption + .decryptMegolmKeyFile(MXMegolmExportEncryption.encryptMegolmKeyFile(expectedString, password, 1000), password) + } catch (e: Exception) { + fail("## checkExportEncrypt1() failed : " + e.message) + } + + assertEquals("## checkExportEncrypt1() : expectedString $expectedString -- decodedString $decodedString", + expectedString, + decodedString) + } + + @Test + fun checkExportEncrypt2() { + val password = "betterpassword" + val expectedString = "Hello, World" + var decodedString: String? = null + + try { + decodedString = MXMegolmExportEncryption + .decryptMegolmKeyFile(MXMegolmExportEncryption.encryptMegolmKeyFile(expectedString, password, 1000), password) + } catch (e: Exception) { + fail("## checkExportEncrypt2() failed : " + e.message) + } + + assertEquals("## checkExportEncrypt2() : expectedString $expectedString -- decodedString $decodedString", + expectedString, + decodedString) + } + + @Test + fun checkExportEncrypt3() { + val password = "SWORDFISH" + val expectedString = "alphanumericallyalphanumericallyalphanumericallyalphanumerically" + var decodedString: String? = null + + try { + decodedString = MXMegolmExportEncryption + .decryptMegolmKeyFile(MXMegolmExportEncryption.encryptMegolmKeyFile(expectedString, password, 1000), password) + } catch (e: Exception) { + fail("## checkExportEncrypt3() failed : " + e.message) + } + + assertEquals("## checkExportEncrypt3() : expectedString $expectedString -- decodedString $decodedString", + expectedString, + decodedString) + } + + @Test + fun checkExportEncrypt4() { + val password = "passwordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpassword" + "passwordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpassword" + val expectedString = "alphanumericallyalphanumericallyalphanumericallyalphanumerically" + var decodedString: String? = null + + try { + decodedString = MXMegolmExportEncryption + .decryptMegolmKeyFile(MXMegolmExportEncryption.encryptMegolmKeyFile(expectedString, password, 1000), password) + } catch (e: Exception) { + fail("## checkExportEncrypt4() failed : " + e.message) + } + + assertEquals("## checkExportEncrypt4() : expectedString $expectedString -- decodedString $decodedString", + expectedString, + decodedString) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupPasswordTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupPasswordTest.kt new file mode 100644 index 0000000000..53e68383ee --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupPasswordTest.kt @@ -0,0 +1,178 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.keysbackup + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import im.vector.matrix.android.InstrumentedTest +import im.vector.matrix.android.api.listeners.ProgressListener +import im.vector.matrix.android.common.assertByteArrayNotEqual +import org.junit.Assert.* +import org.junit.Before +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.matrix.olm.OlmManager +import org.matrix.olm.OlmPkDecryption + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class KeysBackupPasswordTest : InstrumentedTest { + + @Before + fun ensureLibLoaded() { + OlmManager() + } + + /** + * Check KeysBackupPassword utilities + */ + @Test + fun passwordConverter_ok() { + val generatePrivateKeyResult = generatePrivateKeyWithPassword(PASSWORD, null) + + assertEquals(32, generatePrivateKeyResult.salt.length) + assertEquals(500_000, generatePrivateKeyResult.iterations) + assertEquals(OlmPkDecryption.privateKeyLength(), generatePrivateKeyResult.privateKey.size) + + // Reverse operation + val retrievedPrivateKey = retrievePrivateKeyWithPassword(PASSWORD, + generatePrivateKeyResult.salt, + generatePrivateKeyResult.iterations) + + assertEquals(OlmPkDecryption.privateKeyLength(), retrievedPrivateKey.size) + assertArrayEquals(generatePrivateKeyResult.privateKey, retrievedPrivateKey) + } + + /** + * Check generatePrivateKeyWithPassword progress listener behavior + */ + @Test + fun passwordConverter_progress_ok() { + val progressValues = ArrayList(101) + var lastTotal = 0 + + generatePrivateKeyWithPassword(PASSWORD, object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + if (!progressValues.contains(progress)) { + progressValues.add(progress) + } + + lastTotal = total + } + }) + + assertEquals(100, lastTotal) + + // Ensure all values are here + assertEquals(101, progressValues.size) + + for (i in 0..100) { + assertTrue(progressValues[i] == i) + } + } + + /** + * Check KeysBackupPassword utilities, with bad password + */ + @Test + fun passwordConverter_badPassword_ok() { + val generatePrivateKeyResult = generatePrivateKeyWithPassword(PASSWORD, null) + + assertEquals(32, generatePrivateKeyResult.salt.length) + assertEquals(500_000, generatePrivateKeyResult.iterations) + assertEquals(OlmPkDecryption.privateKeyLength(), generatePrivateKeyResult.privateKey.size) + + // Reverse operation, with bad password + val retrievedPrivateKey = retrievePrivateKeyWithPassword(BAD_PASSWORD, + generatePrivateKeyResult.salt, + generatePrivateKeyResult.iterations) + + assertEquals(OlmPkDecryption.privateKeyLength(), retrievedPrivateKey.size) + assertByteArrayNotEqual(generatePrivateKeyResult.privateKey, retrievedPrivateKey) + } + + /** + * Check KeysBackupPassword utilities, with bad password + */ + @Test + fun passwordConverter_badIteration_ok() { + val generatePrivateKeyResult = generatePrivateKeyWithPassword(PASSWORD, null) + + assertEquals(32, generatePrivateKeyResult.salt.length) + assertEquals(500_000, generatePrivateKeyResult.iterations) + assertEquals(OlmPkDecryption.privateKeyLength(), generatePrivateKeyResult.privateKey.size) + + // Reverse operation, with bad iteration + val retrievedPrivateKey = retrievePrivateKeyWithPassword(PASSWORD, + generatePrivateKeyResult.salt, + 500_001) + + assertEquals(OlmPkDecryption.privateKeyLength(), retrievedPrivateKey.size) + assertByteArrayNotEqual(generatePrivateKeyResult.privateKey, retrievedPrivateKey) + } + + /** + * Check KeysBackupPassword utilities, with bad salt + */ + @Test + fun passwordConverter_badSalt_ok() { + val generatePrivateKeyResult = generatePrivateKeyWithPassword(PASSWORD, null) + + assertEquals(32, generatePrivateKeyResult.salt.length) + assertEquals(500_000, generatePrivateKeyResult.iterations) + assertEquals(OlmPkDecryption.privateKeyLength(), generatePrivateKeyResult.privateKey.size) + + // Reverse operation, with bad iteration + val retrievedPrivateKey = retrievePrivateKeyWithPassword(PASSWORD, + BAD_SALT, + generatePrivateKeyResult.iterations) + + assertEquals(OlmPkDecryption.privateKeyLength(), retrievedPrivateKey.size) + assertByteArrayNotEqual(generatePrivateKeyResult.privateKey, retrievedPrivateKey) + } + + /** + * Check [retrievePrivateKeyWithPassword] with data coming from another platform (RiotWeb). + */ + @Test + fun passwordConverter_crossPlatform_ok() { + val password = "This is a passphrase!" + val salt = "TO0lxhQ9aYgGfMsclVWPIAublg8h9Nlu" + val iteration = 500_000 + + val retrievedPrivateKey = retrievePrivateKeyWithPassword(password, salt, iteration) + + assertEquals(OlmPkDecryption.privateKeyLength(), retrievedPrivateKey.size) + + // Data from RiotWeb + val privateKeyBytes = byteArrayOf( + 116.toByte(), 224.toByte(), 229.toByte(), 224.toByte(), 9.toByte(), 3.toByte(), 178.toByte(), 162.toByte(), + 120.toByte(), 23.toByte(), 108.toByte(), 218.toByte(), 22.toByte(), 61.toByte(), 241.toByte(), 200.toByte(), + 235.toByte(), 173.toByte(), 236.toByte(), 100.toByte(), 115.toByte(), 247.toByte(), 33.toByte(), 132.toByte(), + 195.toByte(), 154.toByte(), 64.toByte(), 158.toByte(), 184.toByte(), 148.toByte(), 20.toByte(), 85.toByte()) + + assertArrayEquals(privateKeyBytes, retrievedPrivateKey) + } + + companion object { + private const val PASSWORD = "password" + private const val BAD_PASSWORD = "passw0rd" + + private const val BAD_SALT = "AA0lxhQ9aYgGfMsclVWPIAublg8h9Nlu" + } +} diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt new file mode 100644 index 0000000000..15deebdab1 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt @@ -0,0 +1,1417 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.keysbackup + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import im.vector.matrix.android.InstrumentedTest +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.listeners.ProgressListener +import im.vector.matrix.android.api.listeners.StepProgressListener +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener +import im.vector.matrix.android.common.* +import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP +import im.vector.matrix.android.internal.crypto.MegolmSessionData +import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest +import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrust +import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult +import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult +import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper +import org.junit.Assert.* +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import java.util.* +import java.util.concurrent.CountDownLatch + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class KeysBackupTest : InstrumentedTest { + + private val mTestHelper = CommonTestHelper(context()) + private val mCryptoTestHelper = CryptoTestHelper(mTestHelper) + + private val defaultSessionParams = SessionTestParams(withInitialSync = false) + private val defaultSessionParamsWithInitialSync = SessionTestParams(withInitialSync = true) + + /** + * - From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys + * - Check backup keys after having marked one as backed up + * - Reset keys backup markers + */ + @Test + fun roomKeysTest_testBackupStore_ok() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() + + // From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys + val cryptoStore = (cryptoTestData.firstSession.getKeysBackupService() as KeysBackup).store + val sessions = cryptoStore.inboundGroupSessionsToBackup(100) + val sessionsCount = sessions.size + + assertFalse(sessions.isEmpty()) + assertEquals(sessionsCount, cryptoTestData.firstSession.inboundGroupSessionsCount(false)) + assertEquals(0, cryptoTestData.firstSession.inboundGroupSessionsCount(true)) + + // - Check backup keys after having marked one as backed up + val session = sessions[0] + + cryptoStore.markBackupDoneForInboundGroupSessions(Collections.singletonList(session)) + + assertEquals(sessionsCount, cryptoTestData.firstSession.inboundGroupSessionsCount(false)) + assertEquals(1, cryptoTestData.firstSession.inboundGroupSessionsCount(true)) + + val sessions2 = cryptoStore.inboundGroupSessionsToBackup(100) + assertEquals(sessionsCount - 1, sessions2.size) + + // - Reset keys backup markers + cryptoStore.resetBackupMarkers() + + val sessions3 = cryptoStore.inboundGroupSessionsToBackup(100) + assertEquals(sessionsCount, sessions3.size) + assertEquals(sessionsCount, cryptoTestData.firstSession.inboundGroupSessionsCount(false)) + assertEquals(0, cryptoTestData.firstSession.inboundGroupSessionsCount(true)) + } + + /** + * Check that prepareKeysBackupVersionWithPassword returns valid data + */ + @Test + fun prepareKeysBackupVersionTest() { + val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, defaultSessionParams) + + assertNotNull(bobSession.getKeysBackupService()) + + val keysBackup = bobSession.getKeysBackupService() + + val stateObserver = StateObserver(keysBackup) + + assertFalse(keysBackup.isEnabled) + + val latch = CountDownLatch(1) + + keysBackup.prepareKeysBackupVersion(null, null, object : MatrixCallback { + override fun onSuccess(data: MegolmBackupCreationInfo) { + assertNotNull(data) + + assertEquals(MXCRYPTO_ALGORITHM_MEGOLM_BACKUP, data.algorithm) + assertNotNull(data.authData) + assertNotNull(data.authData!!.publicKey) + assertNotNull(data.authData!!.signatures) + assertNotNull(data.recoveryKey) + + latch.countDown() + } + + override fun onFailure(failure: Throwable) { + fail(failure.localizedMessage) + + latch.countDown() + } + }) + mTestHelper.await(latch) + + stateObserver.stopAndCheckStates(null) + bobSession.close() + } + + /** + * Test creating a keys backup version and check that createKeysBackupVersion() returns valid data + */ + @Test + fun createKeysBackupVersionTest() { + val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, defaultSessionParams) + + val keysBackup = bobSession.getKeysBackupService() + + val stateObserver = StateObserver(keysBackup) + + assertFalse(keysBackup.isEnabled) + + var megolmBackupCreationInfo: MegolmBackupCreationInfo? = null + val latch = CountDownLatch(1) + keysBackup.prepareKeysBackupVersion(null, null, object : MatrixCallback { + override fun onSuccess(data: MegolmBackupCreationInfo) { + megolmBackupCreationInfo = data + + latch.countDown() + } + + override fun onFailure(failure: Throwable) { + fail(failure.localizedMessage) + + latch.countDown() + } + }) + mTestHelper.await(latch) + + assertNotNull(megolmBackupCreationInfo) + + assertFalse(keysBackup.isEnabled) + + val latch2 = CountDownLatch(1) + + // Create the version + keysBackup.createKeysBackupVersion(megolmBackupCreationInfo!!, object : TestMatrixCallback(latch2) { + override fun onSuccess(data: KeysVersion) { + assertNotNull(data) + assertNotNull(data.version) + + super.onSuccess(data) + } + }) + mTestHelper.await(latch2) + + // Backup must be enable now + assertTrue(keysBackup.isEnabled) + + stateObserver.stopAndCheckStates(null) + bobSession.close() + } + + /** + * - Check that createKeysBackupVersion() launches the backup + * - Check the backup completes + */ + @Test + fun backupAfterCreateKeysBackupVersionTest() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() + + val keysBackup = cryptoTestData.firstSession.getKeysBackupService() + + val latch = CountDownLatch(1) + + assertEquals(2, cryptoTestData.firstSession.inboundGroupSessionsCount(false)) + assertEquals(0, cryptoTestData.firstSession.inboundGroupSessionsCount(true)) + + val stateObserver = StateObserver(keysBackup, latch, 5) + + prepareAndCreateKeysBackupData(keysBackup) + + mTestHelper.await(latch) + + val nbOfKeys = cryptoTestData.firstSession.inboundGroupSessionsCount(false) + val backedUpKeys = cryptoTestData.firstSession.inboundGroupSessionsCount(true) + + assertEquals(2, nbOfKeys) + assertEquals("All keys must have been marked as backed up", nbOfKeys, backedUpKeys) + + // Check the several backup state changes + stateObserver.stopAndCheckStates( + listOf( + KeysBackupState.Enabling, + KeysBackupState.ReadyToBackUp, + KeysBackupState.WillBackUp, + KeysBackupState.BackingUp, + KeysBackupState.ReadyToBackUp + ) + ) + cryptoTestData.close() + } + + /** + * Check that backupAllGroupSessions() returns valid data + */ + @Test + fun backupAllGroupSessionsTest() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() + + val keysBackup = cryptoTestData.firstSession.getKeysBackupService() + + val stateObserver = StateObserver(keysBackup) + + prepareAndCreateKeysBackupData(keysBackup) + + // Check that backupAllGroupSessions returns valid data + val nbOfKeys = cryptoTestData.firstSession.inboundGroupSessionsCount(false) + + assertEquals(2, nbOfKeys) + + val latch = CountDownLatch(1) + + var lastBackedUpKeysProgress = 0 + + keysBackup.backupAllGroupSessions(object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + assertEquals(nbOfKeys, total) + lastBackedUpKeysProgress = progress + } + }, TestMatrixCallback(latch)) + + mTestHelper.await(latch) + assertEquals(nbOfKeys, lastBackedUpKeysProgress) + + val backedUpKeys = cryptoTestData.firstSession.inboundGroupSessionsCount(true) + + assertEquals("All keys must have been marked as backed up", nbOfKeys, backedUpKeys) + + stateObserver.stopAndCheckStates(null) + cryptoTestData.close() + } + + /** + * Check encryption and decryption of megolm keys in the backup. + * - Pick a megolm key + * - Check [MXKeyBackup encryptGroupSession] returns stg + * - Check [MXKeyBackup pkDecryptionFromRecoveryKey] is able to create a OLMPkDecryption + * - Check [MXKeyBackup decryptKeyBackupData] returns stg + * - Compare the decrypted megolm key with the original one + */ + @Test + fun testEncryptAndDecryptKeysBackupData() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() + + val keysBackup = cryptoTestData.firstSession.getKeysBackupService() as KeysBackup + + val stateObserver = StateObserver(keysBackup) + + // - Pick a megolm key + val session = keysBackup.store.inboundGroupSessionsToBackup(1)[0] + + val keyBackupCreationInfo = prepareAndCreateKeysBackupData(keysBackup).megolmBackupCreationInfo + + // - Check encryptGroupSession() returns stg + val keyBackupData = keysBackup.encryptGroupSession(session) + assertNotNull(keyBackupData) + assertNotNull(keyBackupData.sessionData) + + // - Check pkDecryptionFromRecoveryKey() is able to create a OlmPkDecryption + val decryption = keysBackup.pkDecryptionFromRecoveryKey(keyBackupCreationInfo.recoveryKey) + assertNotNull(decryption) + // - Check decryptKeyBackupData() returns stg + val sessionData = keysBackup.decryptKeyBackupData(keyBackupData, session.olmInboundGroupSession!!.sessionIdentifier(), cryptoTestData.roomId, decryption!!) + assertNotNull(sessionData) + // - Compare the decrypted megolm key with the original one + assertKeysEquals(session.exportKeys(), sessionData) + + stateObserver.stopAndCheckStates(null) + cryptoTestData.close() + } + + /** + * - Do an e2e backup to the homeserver with a recovery key + * - Log Alice on a new device + * - Restore the e2e backup from the homeserver with the recovery key + * - Restore must be successful + */ + @Test + fun restoreKeysBackupTest() { + val testData = createKeysBackupScenarioWithPassword(null) + + // - Restore the e2e backup from the homeserver + val latch2 = CountDownLatch(1) + var importRoomKeysResult: ImportRoomKeysResult? = null + testData.aliceSession2.getKeysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.getKeysBackupService().keysBackupVersion!!, + testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, + null, + null, + null, + object : TestMatrixCallback(latch2) { + override fun onSuccess(data: ImportRoomKeysResult) { + importRoomKeysResult = data + super.onSuccess(data) + } + } + ) + mTestHelper.await(latch2) + + checkRestoreSuccess(testData, importRoomKeysResult!!.totalNumberOfKeys, importRoomKeysResult!!.successfullyNumberOfImportedKeys) + + testData.cryptoTestData.close() + } + + /** + * + * This is the same as `testRestoreKeyBackup` but this test checks that pending key + * share requests are cancelled. + * + * - Do an e2e backup to the homeserver with a recovery key + * - Log Alice on a new device + * - *** Check the SDK sent key share requests + * - Restore the e2e backup from the homeserver with the recovery key + * - Restore must be successful + * - *** There must be no more pending key share requests + */ + @Test + fun restoreKeysBackupAndKeyShareRequestTest() { + val testData = createKeysBackupScenarioWithPassword(null) + + // - Check the SDK sent key share requests + val cryptoStore2 = (testData.aliceSession2.getKeysBackupService() as KeysBackup).store + val unsentRequest = cryptoStore2 + .getOutgoingRoomKeyRequestByState(setOf(OutgoingRoomKeyRequest.RequestState.UNSENT)) + val sentRequest = cryptoStore2 + .getOutgoingRoomKeyRequestByState(setOf(OutgoingRoomKeyRequest.RequestState.SENT)) + + // Request is either sent or unsent + assertTrue(unsentRequest != null || sentRequest != null) + + // - Restore the e2e backup from the homeserver + val latch2 = CountDownLatch(1) + var importRoomKeysResult: ImportRoomKeysResult? = null + testData.aliceSession2.getKeysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.getKeysBackupService().keysBackupVersion!!, + testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, + null, + null, + null, + object : TestMatrixCallback(latch2) { + override fun onSuccess(data: ImportRoomKeysResult) { + importRoomKeysResult = data + super.onSuccess(data) + } + } + ) + mTestHelper.await(latch2) + + checkRestoreSuccess(testData, importRoomKeysResult!!.totalNumberOfKeys, importRoomKeysResult!!.successfullyNumberOfImportedKeys) + + // - There must be no more pending key share requests + val unsentRequestAfterRestoration = cryptoStore2 + .getOutgoingRoomKeyRequestByState(setOf(OutgoingRoomKeyRequest.RequestState.UNSENT)) + val sentRequestAfterRestoration = cryptoStore2 + .getOutgoingRoomKeyRequestByState(setOf(OutgoingRoomKeyRequest.RequestState.SENT)) + + // Request is either sent or unsent + assertTrue(unsentRequestAfterRestoration == null && sentRequestAfterRestoration == null) + + testData.cryptoTestData.close() + } + + /** + * - Do an e2e backup to the homeserver with a recovery key + * - And log Alice on a new device + * - The new device must see the previous backup as not trusted + * - Trust the backup from the new device + * - Backup must be enabled on the new device + * - Retrieve the last version from the server + * - It must be the same + * - It must be trusted and must have with 2 signatures now + */ + @Test + fun trustKeyBackupVersionTest() { + // - Do an e2e backup to the homeserver with a recovery key + // - And log Alice on a new device + val testData = createKeysBackupScenarioWithPassword(null) + + val stateObserver = StateObserver(testData.aliceSession2.getKeysBackupService()) + + // - The new device must see the previous backup as not trusted + assertNotNull(testData.aliceSession2.getKeysBackupService().keysBackupVersion) + assertFalse(testData.aliceSession2.getKeysBackupService().isEnabled) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.getKeysBackupService().state) + + // - Trust the backup from the new device + val latch = CountDownLatch(1) + testData.aliceSession2.getKeysBackupService().trustKeysBackupVersion( + testData.aliceSession2.getKeysBackupService().keysBackupVersion!!, + true, + TestMatrixCallback(latch) + ) + mTestHelper.await(latch) + + // Wait for backup state to be ReadyToBackUp + waitForKeysBackupToBeInState(testData.aliceSession2, KeysBackupState.ReadyToBackUp) + + // - Backup must be enabled on the new device, on the same version + assertEquals(testData.prepareKeysBackupDataResult.version, testData.aliceSession2.getKeysBackupService().keysBackupVersion?.version) + assertTrue(testData.aliceSession2.getKeysBackupService().isEnabled) + + // - Retrieve the last version from the server + val latch2 = CountDownLatch(1) + var keysVersionResult: KeysVersionResult? = null + testData.aliceSession2.getKeysBackupService().getCurrentVersion( + object : TestMatrixCallback(latch2) { + override fun onSuccess(data: KeysVersionResult?) { + keysVersionResult = data + super.onSuccess(data) + } + } + ) + mTestHelper.await(latch2) + + // - It must be the same + assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version) + + val latch3 = CountDownLatch(1) + var keysBackupVersionTrust: KeysBackupVersionTrust? = null + testData.aliceSession2.getKeysBackupService().getKeysBackupTrust(keysVersionResult!!, + object : TestMatrixCallback(latch3) { + override fun onSuccess(data: KeysBackupVersionTrust) { + keysBackupVersionTrust = data + super.onSuccess(data) + } + }) + mTestHelper.await(latch3) + + // - It must be trusted and must have 2 signatures now + assertTrue(keysBackupVersionTrust!!.usable) + assertEquals(2, keysBackupVersionTrust!!.signatures.size) + + stateObserver.stopAndCheckStates(null) + testData.cryptoTestData.close() + } + + /** + * - Do an e2e backup to the homeserver with a recovery key + * - And log Alice on a new device + * - The new device must see the previous backup as not trusted + * - Trust the backup from the new device with the recovery key + * - Backup must be enabled on the new device + * - Retrieve the last version from the server + * - It must be the same + * - It must be trusted and must have with 2 signatures now + */ + @Test + fun trustKeyBackupVersionWithRecoveryKeyTest() { + // - Do an e2e backup to the homeserver with a recovery key + // - And log Alice on a new device + val testData = createKeysBackupScenarioWithPassword(null) + + val stateObserver = StateObserver(testData.aliceSession2.getKeysBackupService()) + + // - The new device must see the previous backup as not trusted + assertNotNull(testData.aliceSession2.getKeysBackupService().keysBackupVersion) + assertFalse(testData.aliceSession2.getKeysBackupService().isEnabled) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.getKeysBackupService().state) + + // - Trust the backup from the new device with the recovery key + val latch = CountDownLatch(1) + testData.aliceSession2.getKeysBackupService().trustKeysBackupVersionWithRecoveryKey( + testData.aliceSession2.getKeysBackupService().keysBackupVersion!!, + testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, + TestMatrixCallback(latch) + ) + mTestHelper.await(latch) + + // Wait for backup state to be ReadyToBackUp + waitForKeysBackupToBeInState(testData.aliceSession2, KeysBackupState.ReadyToBackUp) + + // - Backup must be enabled on the new device, on the same version + assertEquals(testData.prepareKeysBackupDataResult.version, testData.aliceSession2.getKeysBackupService().keysBackupVersion?.version) + assertTrue(testData.aliceSession2.getKeysBackupService().isEnabled) + + // - Retrieve the last version from the server + val latch2 = CountDownLatch(1) + var keysVersionResult: KeysVersionResult? = null + testData.aliceSession2.getKeysBackupService().getCurrentVersion( + object : TestMatrixCallback(latch2) { + override fun onSuccess(data: KeysVersionResult?) { + keysVersionResult = data + super.onSuccess(data) + } + } + ) + mTestHelper.await(latch2) + + // - It must be the same + assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version) + + val latch3 = CountDownLatch(1) + var keysBackupVersionTrust: KeysBackupVersionTrust? = null + testData.aliceSession2.getKeysBackupService().getKeysBackupTrust(keysVersionResult!!, + object : TestMatrixCallback(latch3) { + override fun onSuccess(data: KeysBackupVersionTrust) { + keysBackupVersionTrust = data + super.onSuccess(data) + } + }) + mTestHelper.await(latch3) + + // - It must be trusted and must have 2 signatures now + assertTrue(keysBackupVersionTrust!!.usable) + assertEquals(2, keysBackupVersionTrust!!.signatures.size) + + stateObserver.stopAndCheckStates(null) + testData.cryptoTestData.close() + } + + /** + * - Do an e2e backup to the homeserver with a recovery key + * - And log Alice on a new device + * - The new device must see the previous backup as not trusted + * - Try to trust the backup from the new device with a wrong recovery key + * - It must fail + * - The backup must still be untrusted and disabled + */ + @Test + fun trustKeyBackupVersionWithWrongRecoveryKeyTest() { + // - Do an e2e backup to the homeserver with a recovery key + // - And log Alice on a new device + val testData = createKeysBackupScenarioWithPassword(null) + + val stateObserver = StateObserver(testData.aliceSession2.getKeysBackupService()) + + // - The new device must see the previous backup as not trusted + assertNotNull(testData.aliceSession2.getKeysBackupService().keysBackupVersion) + assertFalse(testData.aliceSession2.getKeysBackupService().isEnabled) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.getKeysBackupService().state) + + // - Try to trust the backup from the new device with a wrong recovery key + val latch = CountDownLatch(1) + testData.aliceSession2.getKeysBackupService().trustKeysBackupVersionWithRecoveryKey( + testData.aliceSession2.getKeysBackupService().keysBackupVersion!!, + "Bad recovery key", + TestMatrixCallback(latch, false) + ) + mTestHelper.await(latch) + + // - The new device must still see the previous backup as not trusted + assertNotNull(testData.aliceSession2.getKeysBackupService().keysBackupVersion) + assertFalse(testData.aliceSession2.getKeysBackupService().isEnabled) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.getKeysBackupService().state) + + stateObserver.stopAndCheckStates(null) + testData.cryptoTestData.close() + } + + /** + * - Do an e2e backup to the homeserver with a password + * - And log Alice on a new device + * - The new device must see the previous backup as not trusted + * - Trust the backup from the new device with the password + * - Backup must be enabled on the new device + * - Retrieve the last version from the server + * - It must be the same + * - It must be trusted and must have with 2 signatures now + */ + @Test + fun trustKeyBackupVersionWithPasswordTest() { + val password = "Password" + + // - Do an e2e backup to the homeserver with a password + // - And log Alice on a new device + val testData = createKeysBackupScenarioWithPassword(password) + + val stateObserver = StateObserver(testData.aliceSession2.getKeysBackupService()) + + // - The new device must see the previous backup as not trusted + assertNotNull(testData.aliceSession2.getKeysBackupService().keysBackupVersion) + assertFalse(testData.aliceSession2.getKeysBackupService().isEnabled) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.getKeysBackupService().state) + + // - Trust the backup from the new device with the password + val latch = CountDownLatch(1) + testData.aliceSession2.getKeysBackupService().trustKeysBackupVersionWithPassphrase( + testData.aliceSession2.getKeysBackupService().keysBackupVersion!!, + password, + TestMatrixCallback(latch) + ) + mTestHelper.await(latch) + + // Wait for backup state to be ReadyToBackUp + waitForKeysBackupToBeInState(testData.aliceSession2, KeysBackupState.ReadyToBackUp) + + // - Backup must be enabled on the new device, on the same version + assertEquals(testData.prepareKeysBackupDataResult.version, testData.aliceSession2.getKeysBackupService().keysBackupVersion?.version) + assertTrue(testData.aliceSession2.getKeysBackupService().isEnabled) + + // - Retrieve the last version from the server + val latch2 = CountDownLatch(1) + var keysVersionResult: KeysVersionResult? = null + testData.aliceSession2.getKeysBackupService().getCurrentVersion( + object : TestMatrixCallback(latch2) { + override fun onSuccess(data: KeysVersionResult?) { + keysVersionResult = data + super.onSuccess(data) + } + } + ) + mTestHelper.await(latch2) + + // - It must be the same + assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version) + + val latch3 = CountDownLatch(1) + var keysBackupVersionTrust: KeysBackupVersionTrust? = null + testData.aliceSession2.getKeysBackupService().getKeysBackupTrust(keysVersionResult!!, + object : TestMatrixCallback(latch3) { + override fun onSuccess(data: KeysBackupVersionTrust) { + keysBackupVersionTrust = data + super.onSuccess(data) + } + }) + mTestHelper.await(latch3) + + // - It must be trusted and must have 2 signatures now + assertTrue(keysBackupVersionTrust!!.usable) + assertEquals(2, keysBackupVersionTrust!!.signatures.size) + + stateObserver.stopAndCheckStates(null) + testData.cryptoTestData.close() + } + + /** + * - Do an e2e backup to the homeserver with a password + * - And log Alice on a new device + * - The new device must see the previous backup as not trusted + * - Try to trust the backup from the new device with a wrong password + * - It must fail + * - The backup must still be untrusted and disabled + */ + @Test + fun trustKeyBackupVersionWithWrongPasswordTest() { + val password = "Password" + val badPassword = "Bad Password" + + // - Do an e2e backup to the homeserver with a password + // - And log Alice on a new device + val testData = createKeysBackupScenarioWithPassword(password) + + val stateObserver = StateObserver(testData.aliceSession2.getKeysBackupService()) + + // - The new device must see the previous backup as not trusted + assertNotNull(testData.aliceSession2.getKeysBackupService().keysBackupVersion) + assertFalse(testData.aliceSession2.getKeysBackupService().isEnabled) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.getKeysBackupService().state) + + // - Try to trust the backup from the new device with a wrong password + val latch = CountDownLatch(1) + testData.aliceSession2.getKeysBackupService().trustKeysBackupVersionWithPassphrase( + testData.aliceSession2.getKeysBackupService().keysBackupVersion!!, + badPassword, + TestMatrixCallback(latch, false) + ) + mTestHelper.await(latch) + + // - The new device must still see the previous backup as not trusted + assertNotNull(testData.aliceSession2.getKeysBackupService().keysBackupVersion) + assertFalse(testData.aliceSession2.getKeysBackupService().isEnabled) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.getKeysBackupService().state) + + stateObserver.stopAndCheckStates(null) + testData.cryptoTestData.close() + } + + /** + * - Do an e2e backup to the homeserver with a recovery key + * - Log Alice on a new device + * - Try to restore the e2e backup with a wrong recovery key + * - It must fail + */ + @Test + fun restoreKeysBackupWithAWrongRecoveryKeyTest() { + val testData = createKeysBackupScenarioWithPassword(null) + + // - Try to restore the e2e backup with a wrong recovery key + val latch2 = CountDownLatch(1) + var importRoomKeysResult: ImportRoomKeysResult? = null + testData.aliceSession2.getKeysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.getKeysBackupService().keysBackupVersion!!, + "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", + null, + null, + null, + object : TestMatrixCallback(latch2, false) { + override fun onSuccess(data: ImportRoomKeysResult) { + importRoomKeysResult = data + super.onSuccess(data) + } + } + ) + mTestHelper.await(latch2) + + // onSuccess may not have been called + assertNull(importRoomKeysResult) + + testData.cryptoTestData.close() + } + + /** + * - Do an e2e backup to the homeserver with a password + * - Log Alice on a new device + * - Restore the e2e backup with the password + * - Restore must be successful + */ + @Test + fun testBackupWithPassword() { + val password = "password" + + val testData = createKeysBackupScenarioWithPassword(password) + + // - Restore the e2e backup with the password + val latch2 = CountDownLatch(1) + var importRoomKeysResult: ImportRoomKeysResult? = null + val steps = ArrayList() + + testData.aliceSession2.getKeysBackupService().restoreKeyBackupWithPassword(testData.aliceSession2.getKeysBackupService().keysBackupVersion!!, + password, + null, + null, + object : StepProgressListener { + override fun onStepProgress(step: StepProgressListener.Step) { + steps.add(step) + } + }, + object : TestMatrixCallback(latch2) { + override fun onSuccess(data: ImportRoomKeysResult) { + importRoomKeysResult = data + super.onSuccess(data) + } + } + ) + mTestHelper.await(latch2) + + // Check steps + assertEquals(105, steps.size) + + for (i in 0..100) { + assertTrue(steps[i] is StepProgressListener.Step.ComputingKey) + assertEquals(i, (steps[i] as StepProgressListener.Step.ComputingKey).progress) + assertEquals(100, (steps[i] as StepProgressListener.Step.ComputingKey).total) + } + + assertTrue(steps[101] is StepProgressListener.Step.DownloadingKey) + + // 2 Keys to import, value will be 0%, 50%, 100% + for (i in 102..104) { + assertTrue(steps[i] is StepProgressListener.Step.ImportingKey) + assertEquals(100, (steps[i] as StepProgressListener.Step.ImportingKey).total) + } + + assertEquals(0, (steps[102] as StepProgressListener.Step.ImportingKey).progress) + assertEquals(50, (steps[103] as StepProgressListener.Step.ImportingKey).progress) + assertEquals(100, (steps[104] as StepProgressListener.Step.ImportingKey).progress) + + checkRestoreSuccess(testData, importRoomKeysResult!!.totalNumberOfKeys, importRoomKeysResult!!.successfullyNumberOfImportedKeys) + + testData.cryptoTestData.close() + } + + /** + * - Do an e2e backup to the homeserver with a password + * - Log Alice on a new device + * - Try to restore the e2e backup with a wrong password + * - It must fail + */ + @Test + fun restoreKeysBackupWithAWrongPasswordTest() { + val password = "password" + val wrongPassword = "passw0rd" + + val testData = createKeysBackupScenarioWithPassword(password) + + // - Try to restore the e2e backup with a wrong password + val latch2 = CountDownLatch(1) + var importRoomKeysResult: ImportRoomKeysResult? = null + testData.aliceSession2.getKeysBackupService().restoreKeyBackupWithPassword(testData.aliceSession2.getKeysBackupService().keysBackupVersion!!, + wrongPassword, + null, + null, + null, + object : TestMatrixCallback(latch2, false) { + override fun onSuccess(data: ImportRoomKeysResult) { + importRoomKeysResult = data + super.onSuccess(data) + } + } + ) + mTestHelper.await(latch2) + + // onSuccess may not have been called + assertNull(importRoomKeysResult) + + testData.cryptoTestData.close() + } + + /** + * - Do an e2e backup to the homeserver with a password + * - Log Alice on a new device + * - Restore the e2e backup with the recovery key. + * - Restore must be successful + */ + @Test + fun testUseRecoveryKeyToRestoreAPasswordBasedKeysBackup() { + val password = "password" + + val testData = createKeysBackupScenarioWithPassword(password) + + // - Restore the e2e backup with the recovery key. + val latch2 = CountDownLatch(1) + var importRoomKeysResult: ImportRoomKeysResult? = null + testData.aliceSession2.getKeysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.getKeysBackupService().keysBackupVersion!!, + testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, + null, + null, + null, + object : TestMatrixCallback(latch2) { + override fun onSuccess(data: ImportRoomKeysResult) { + importRoomKeysResult = data + super.onSuccess(data) + } + } + ) + mTestHelper.await(latch2) + + checkRestoreSuccess(testData, importRoomKeysResult!!.totalNumberOfKeys, importRoomKeysResult!!.successfullyNumberOfImportedKeys) + + testData.cryptoTestData.close() + } + + /** + * - Do an e2e backup to the homeserver with a recovery key + * - And log Alice on a new device + * - Try to restore the e2e backup with a password + * - It must fail + */ + @Test + fun testUsePasswordToRestoreARecoveryKeyBasedKeysBackup() { + val testData = createKeysBackupScenarioWithPassword(null) + + // - Try to restore the e2e backup with a password + val latch2 = CountDownLatch(1) + var importRoomKeysResult: ImportRoomKeysResult? = null + testData.aliceSession2.getKeysBackupService().restoreKeyBackupWithPassword(testData.aliceSession2.getKeysBackupService().keysBackupVersion!!, + "password", + null, + null, + null, + object : TestMatrixCallback(latch2, false) { + override fun onSuccess(data: ImportRoomKeysResult) { + importRoomKeysResult = data + super.onSuccess(data) + } + } + ) + mTestHelper.await(latch2) + + // onSuccess may not have been called + assertNull(importRoomKeysResult) + + testData.cryptoTestData.close() + } + + /** + * - Create a backup version + * - Check the returned KeysVersionResult is trusted + */ + @Test + fun testIsKeysBackupTrusted() { + // - Create a backup version + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() + + val keysBackup = cryptoTestData.firstSession.getKeysBackupService() + + val stateObserver = StateObserver(keysBackup) + + // - Do an e2e backup to the homeserver + prepareAndCreateKeysBackupData(keysBackup) + + // Get key backup version from the home server + var keysVersionResult: KeysVersionResult? = null + val lock = CountDownLatch(1) + keysBackup.getCurrentVersion(object : TestMatrixCallback(lock) { + override fun onSuccess(data: KeysVersionResult?) { + keysVersionResult = data + super.onSuccess(data) + } + }) + mTestHelper.await(lock) + + assertNotNull(keysVersionResult) + + // - Check the returned KeyBackupVersion is trusted + val latch = CountDownLatch(1) + var keysBackupVersionTrust: KeysBackupVersionTrust? = null + keysBackup.getKeysBackupTrust(keysVersionResult!!, object : MatrixCallback { + override fun onSuccess(data: KeysBackupVersionTrust) { + keysBackupVersionTrust = data + latch.countDown() + } + + override fun onFailure(failure: Throwable) { + super.onFailure(failure) + latch.countDown() + } + }) + mTestHelper.await(latch) + + assertNotNull(keysBackupVersionTrust) + assertTrue(keysBackupVersionTrust!!.usable) + assertEquals(1, keysBackupVersionTrust!!.signatures.size) + + val signature = keysBackupVersionTrust!!.signatures[0] + assertTrue(signature.valid) + assertNotNull(signature.device) + assertEquals(cryptoTestData.firstSession.getMyDevice().deviceId, signature.deviceId) + assertEquals(signature.device!!.deviceId, cryptoTestData.firstSession.sessionParams.credentials.deviceId) + + stateObserver.stopAndCheckStates(null) + cryptoTestData.close() + } + + /** + * Check backup starts automatically if there is an existing and compatible backup + * version on the homeserver. + * - Create a backup version + * - Restart alice session + * -> The new alice session must back up to the same version + */ + @Test + fun testCheckAndStartKeysBackupWhenRestartingAMatrixSession() { + // - Create a backup version + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() + + val keysBackup = cryptoTestData.firstSession.getKeysBackupService() + + val stateObserver = StateObserver(keysBackup) + + assertFalse(keysBackup.isEnabled) + + val keyBackupCreationInfo = prepareAndCreateKeysBackupData(keysBackup) + + assertTrue(keysBackup.isEnabled) + + // - Restart alice session + // - Log Alice on a new device + val aliceSession2 = mTestHelper.logIntoAccount(cryptoTestData.firstSession.myUserId, defaultSessionParamsWithInitialSync) + + cryptoTestData.close() + + val keysBackup2 = aliceSession2.getKeysBackupService() + + val stateObserver2 = StateObserver(keysBackup2) + + // -> The new alice session must back up to the same version + val latch = CountDownLatch(1) + var count = 0 + keysBackup2.addListener(object : KeysBackupStateListener { + override fun onStateChange(newState: KeysBackupState) { + // Check the backup completes + if (keysBackup.state == KeysBackupState.ReadyToBackUp) { + count++ + + if (count == 2) { + // Remove itself from the list of listeners + keysBackup.removeListener(this) + + latch.countDown() + } + } + } + }) + mTestHelper.await(latch) + + assertEquals(keyBackupCreationInfo.version, keysBackup2.currentBackupVersion) + + stateObserver.stopAndCheckStates(null) + stateObserver2.stopAndCheckStates(null) + aliceSession2.close() + } + + /** + * Check WrongBackUpVersion state + * + * - Make alice back up her keys to her homeserver + * - Create a new backup with fake data on the homeserver + * - Make alice back up all her keys again + * -> That must fail and her backup state must be WrongBackUpVersion + */ + @Test + fun testBackupWhenAnotherBackupWasCreated() { + // - Create a backup version + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() + + val keysBackup = cryptoTestData.firstSession.getKeysBackupService() + + val stateObserver = StateObserver(keysBackup) + + assertFalse(keysBackup.isEnabled) + + // Wait for keys backup to be finished + val latch0 = CountDownLatch(1) + var count = 0 + keysBackup.addListener(object : KeysBackupStateListener { + override fun onStateChange(newState: KeysBackupState) { + // Check the backup completes + if (newState == KeysBackupState.ReadyToBackUp) { + count++ + + if (count == 2) { + // Remove itself from the list of listeners + keysBackup.removeListener(this) + + latch0.countDown() + } + } + } + }) + + // - Make alice back up her keys to her homeserver + prepareAndCreateKeysBackupData(keysBackup) + + assertTrue(keysBackup.isEnabled) + + mTestHelper.await(latch0) + + // - Create a new backup with fake data on the homeserver, directly using the rest client + val latch = CountDownLatch(1) + + val megolmBackupCreationInfo = mCryptoTestHelper.createFakeMegolmBackupCreationInfo() + (keysBackup as KeysBackup).createFakeKeysBackupVersion(megolmBackupCreationInfo, TestMatrixCallback(latch)) + mTestHelper.await(latch) + + // Reset the store backup status for keys + (cryptoTestData.firstSession.getKeysBackupService() as KeysBackup).store.resetBackupMarkers() + + // - Make alice back up all her keys again + val latch2 = CountDownLatch(1) + keysBackup.backupAllGroupSessions(object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + } + }, TestMatrixCallback(latch2, false)) + mTestHelper.await(latch2) + + // -> That must fail and her backup state must be WrongBackUpVersion + assertEquals(KeysBackupState.WrongBackUpVersion, keysBackup.state) + assertFalse(keysBackup.isEnabled) + + stateObserver.stopAndCheckStates(null) + cryptoTestData.close() + } + + /** + * - Do an e2e backup to the homeserver + * - Log Alice on a new device + * - Post a message to have a new megolm session + * - Try to backup all + * -> It must fail. Backup state must be NotTrusted + * - Validate the old device from the new one + * -> Backup should automatically enable on the new device + * -> It must use the same backup version + * - Try to backup all again + * -> It must success + */ + @Test + fun testBackupAfterVerifyingADevice() { + // - Create a backup version + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() + + val keysBackup = cryptoTestData.firstSession.getKeysBackupService() + + val stateObserver = StateObserver(keysBackup) + + // - Make alice back up her keys to her homeserver + prepareAndCreateKeysBackupData(keysBackup) + + // Wait for keys backup to finish by asking again to backup keys. + val latch = CountDownLatch(1) + keysBackup.backupAllGroupSessions(object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + } + }, TestMatrixCallback(latch)) + mTestHelper.await(latch) + + val oldDeviceId = cryptoTestData.firstSession.sessionParams.credentials.deviceId!! + val oldKeyBackupVersion = keysBackup.currentBackupVersion + val aliceUserId = cryptoTestData.firstSession.myUserId + + // Close first Alice session, else they will share the same Crypto store and the test fails. + cryptoTestData.firstSession.close() + + // - Log Alice on a new device + val aliceSession2 = mTestHelper.logIntoAccount(aliceUserId, defaultSessionParamsWithInitialSync) + + // - Post a message to have a new megolm session + aliceSession2.setWarnOnUnknownDevices(false) + + val room2 = aliceSession2.getRoom(cryptoTestData.roomId)!! + + mTestHelper.sendTextMessage(room2, "New key", 1) + + // - Try to backup all in aliceSession2, it must fail + val keysBackup2 = aliceSession2.getKeysBackupService() + + val stateObserver2 = StateObserver(keysBackup2) + + var isSuccessful = false + val latch2 = CountDownLatch(1) + keysBackup2.backupAllGroupSessions(object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + } + }, object : TestMatrixCallback(latch2, false) { + override fun onSuccess(data: Unit) { + isSuccessful = true + super.onSuccess(data) + } + }) + mTestHelper.await(latch2) + + assertFalse(isSuccessful) + + // Backup state must be NotTrusted + assertEquals(KeysBackupState.NotTrusted, keysBackup2.state) + assertFalse(keysBackup2.isEnabled) + + // - Validate the old device from the new one + aliceSession2.setDeviceVerification(MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED, oldDeviceId, aliceSession2.myUserId) + + // -> Backup should automatically enable on the new device + val latch4 = CountDownLatch(1) + keysBackup2.addListener(object : KeysBackupStateListener { + override fun onStateChange(newState: KeysBackupState) { + // Check the backup completes + if (keysBackup2.state == KeysBackupState.ReadyToBackUp) { + // Remove itself from the list of listeners + keysBackup2.removeListener(this) + + latch4.countDown() + } + } + }) + mTestHelper.await(latch4) + + // -> It must use the same backup version + assertEquals(oldKeyBackupVersion, aliceSession2.getKeysBackupService().currentBackupVersion) + + val latch5 = CountDownLatch(1) + aliceSession2.getKeysBackupService().backupAllGroupSessions(null, TestMatrixCallback(latch5)) + mTestHelper.await(latch5) + + // -> It must success + assertTrue(aliceSession2.getKeysBackupService().isEnabled) + + stateObserver.stopAndCheckStates(null) + stateObserver2.stopAndCheckStates(null) + aliceSession2.close() + cryptoTestData.close() + } + + /** + * - Do an e2e backup to the homeserver with a recovery key + * - Delete the backup + */ + @Test + fun deleteKeysBackupTest() { + // - Create a backup version + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() + + val keysBackup = cryptoTestData.firstSession.getKeysBackupService() + + val stateObserver = StateObserver(keysBackup) + + assertFalse(keysBackup.isEnabled) + + val keyBackupCreationInfo = prepareAndCreateKeysBackupData(keysBackup) + + assertTrue(keysBackup.isEnabled) + + val latch = CountDownLatch(1) + + // Delete the backup + keysBackup.deleteBackup(keyBackupCreationInfo.version, TestMatrixCallback(latch)) + + mTestHelper.await(latch) + + // Backup is now disabled + assertFalse(keysBackup.isEnabled) + + stateObserver.stopAndCheckStates(null) + cryptoTestData.close() + } + + /* ========================================================================================== + * Private + * ========================================================================================== */ + + /** + * As KeysBackup is doing asynchronous call to update its internal state, this method help to wait for the + * KeysBackup object to be in the specified state + */ + private fun waitForKeysBackupToBeInState(session: Session, state: KeysBackupState) { + // If already in the wanted state, return + if (session.getKeysBackupService().state == state) { + return + } + + // Else observe state changes + val latch = CountDownLatch(1) + + session.getKeysBackupService().addListener(object : KeysBackupStateListener { + override fun onStateChange(newState: KeysBackupState) { + if (newState == state) { + session.getKeysBackupService().removeListener(this) + latch.countDown() + } + } + }) + + mTestHelper.await(latch) + } + + private data class PrepareKeysBackupDataResult(val megolmBackupCreationInfo: MegolmBackupCreationInfo, + val version: String) + + private fun prepareAndCreateKeysBackupData(keysBackup: KeysBackupService, + password: String? = null): PrepareKeysBackupDataResult { + val stateObserver = StateObserver(keysBackup) + + var megolmBackupCreationInfo: MegolmBackupCreationInfo? = null + val latch = CountDownLatch(1) + keysBackup.prepareKeysBackupVersion(password, null, object : MatrixCallback { + override fun onSuccess(data: MegolmBackupCreationInfo) { + megolmBackupCreationInfo = data + + latch.countDown() + } + + override fun onFailure(failure: Throwable) { + fail(failure.localizedMessage) + + latch.countDown() + } + }) + mTestHelper.await(latch) + + assertNotNull(megolmBackupCreationInfo) + + assertFalse(keysBackup.isEnabled) + + val latch2 = CountDownLatch(1) + + // Create the version + var version: String? = null + keysBackup.createKeysBackupVersion(megolmBackupCreationInfo!!, object : TestMatrixCallback(latch2) { + override fun onSuccess(data: KeysVersion) { + assertNotNull(data) + assertNotNull(data.version) + + version = data.version + + super.onSuccess(data) + } + }) + mTestHelper.await(latch2) + + // Backup must be enable now + assertTrue(keysBackup.isEnabled) + assertNotNull(version) + + stateObserver.stopAndCheckStates(null) + return PrepareKeysBackupDataResult(megolmBackupCreationInfo!!, version!!) + } + + private fun assertKeysEquals(keys1: MegolmSessionData?, keys2: MegolmSessionData?) { + assertNotNull(keys1) + assertNotNull(keys2) + + assertEquals(keys1?.algorithm, keys2?.algorithm) + assertEquals(keys1?.roomId, keys2?.roomId) + // No need to compare the shortcut + // assertEquals(keys1?.sender_claimed_ed25519_key, keys2?.sender_claimed_ed25519_key) + assertEquals(keys1?.senderKey, keys2?.senderKey) + assertEquals(keys1?.sessionId, keys2?.sessionId) + assertEquals(keys1?.sessionKey, keys2?.sessionKey) + + assertListEquals(keys1?.forwardingCurve25519KeyChain, keys2?.forwardingCurve25519KeyChain) + assertDictEquals(keys1?.senderClaimedKeys, keys2?.senderClaimedKeys) + } + + /** + * Data class to store result of [createKeysBackupScenarioWithPassword] + */ + private data class KeysBackupScenarioData(val cryptoTestData: CryptoTestData, + val aliceKeys: List, + val prepareKeysBackupDataResult: PrepareKeysBackupDataResult, + val aliceSession2: Session) + + /** + * Common initial condition + * - Do an e2e backup to the homeserver + * - Log Alice on a new device, and wait for its keysBackup object to be ready (in state NotTrusted) + * + * @param password optional password + */ + private fun createKeysBackupScenarioWithPassword(password: String?): KeysBackupScenarioData { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() + + val cryptoStore = (cryptoTestData.firstSession.getKeysBackupService() as KeysBackup).store + val keysBackup = cryptoTestData.firstSession.getKeysBackupService() + + val stateObserver = StateObserver(keysBackup) + + val aliceKeys = cryptoStore.inboundGroupSessionsToBackup(100) + + // - Do an e2e backup to the homeserver + val prepareKeysBackupDataResult = prepareAndCreateKeysBackupData(keysBackup, password) + + val latch = CountDownLatch(1) + var lastProgress = 0 + var lastTotal = 0 + keysBackup.backupAllGroupSessions(object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + lastProgress = progress + lastTotal = total + } + }, TestMatrixCallback(latch)) + mTestHelper.await(latch) + + assertEquals(2, lastProgress) + assertEquals(2, lastTotal) + + val aliceUserId = cryptoTestData.firstSession.myUserId + + // Logout first Alice session, else they will share the same Crypto store and some tests may fail. + val latch2 = CountDownLatch(1) + cryptoTestData.firstSession.signOut(true, TestMatrixCallback(latch2)) + mTestHelper.await(latch2) + + // - Log Alice on a new device + val aliceSession2 = mTestHelper.logIntoAccount(aliceUserId, defaultSessionParamsWithInitialSync) + + // Test check: aliceSession2 has no keys at login + assertEquals(0, aliceSession2.inboundGroupSessionsCount(false)) + + // Wait for backup state to be NotTrusted + waitForKeysBackupToBeInState(aliceSession2, KeysBackupState.NotTrusted) + + stateObserver.stopAndCheckStates(null) + + return KeysBackupScenarioData(cryptoTestData, + aliceKeys, + prepareKeysBackupDataResult, + aliceSession2) + } + + /** + * Common restore success check after [createKeysBackupScenarioWithPassword]: + * - Imported keys number must be correct + * - The new device must have the same count of megolm keys + * - Alice must have the same keys on both devices + */ + private fun checkRestoreSuccess(testData: KeysBackupScenarioData, + total: Int, + imported: Int) { + // - Imported keys number must be correct + assertEquals(testData.aliceKeys.size, total) + assertEquals(total, imported) + + // - The new device must have the same count of megolm keys + assertEquals(testData.aliceKeys.size, testData.aliceSession2.inboundGroupSessionsCount(false)) + + // - Alice must have the same keys on both devices + for (aliceKey1 in testData.aliceKeys) { + val aliceKey2 = (testData.aliceSession2.getKeysBackupService() as KeysBackup).store + .getInboundGroupSession(aliceKey1.olmInboundGroupSession!!.sessionIdentifier(), aliceKey1.senderKey!!) + assertNotNull(aliceKey2) + assertKeysEquals(aliceKey1.exportKeys(), aliceKey2!!.exportKeys()) + } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/StateObserver.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/StateObserver.kt new file mode 100644 index 0000000000..3f2e33d73b --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/StateObserver.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.keysbackup + +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import java.util.concurrent.CountDownLatch + +/** + * This class observe the state change of a KeysBackup object and provide a method to check the several state change + * It checks all state transitions and detected forbidden transition + */ +internal class StateObserver(private val keysBackup: KeysBackupService, + private val latch: CountDownLatch? = null, + private val expectedStateChange: Int = -1) : KeysBackupStateListener { + + private val allowedStateTransitions = listOf( + KeysBackupState.BackingUp to KeysBackupState.ReadyToBackUp, + KeysBackupState.BackingUp to KeysBackupState.WrongBackUpVersion, + + KeysBackupState.CheckingBackUpOnHomeserver to KeysBackupState.Disabled, + KeysBackupState.CheckingBackUpOnHomeserver to KeysBackupState.NotTrusted, + KeysBackupState.CheckingBackUpOnHomeserver to KeysBackupState.ReadyToBackUp, + KeysBackupState.CheckingBackUpOnHomeserver to KeysBackupState.Unknown, + KeysBackupState.CheckingBackUpOnHomeserver to KeysBackupState.WrongBackUpVersion, + + KeysBackupState.Disabled to KeysBackupState.Enabling, + + KeysBackupState.Enabling to KeysBackupState.Disabled, + KeysBackupState.Enabling to KeysBackupState.ReadyToBackUp, + + KeysBackupState.NotTrusted to KeysBackupState.CheckingBackUpOnHomeserver, + // This transition happens when we trust the device + KeysBackupState.NotTrusted to KeysBackupState.ReadyToBackUp, + + KeysBackupState.ReadyToBackUp to KeysBackupState.WillBackUp, + + KeysBackupState.Unknown to KeysBackupState.CheckingBackUpOnHomeserver, + + KeysBackupState.WillBackUp to KeysBackupState.BackingUp, + + KeysBackupState.WrongBackUpVersion to KeysBackupState.CheckingBackUpOnHomeserver, + + // FIXME These transitions are observed during test, and I'm not sure they should occur. Don't have time to investigate now + KeysBackupState.ReadyToBackUp to KeysBackupState.BackingUp, + KeysBackupState.ReadyToBackUp to KeysBackupState.ReadyToBackUp, + KeysBackupState.WillBackUp to KeysBackupState.ReadyToBackUp, + KeysBackupState.WillBackUp to KeysBackupState.Unknown + ) + + private val stateList = ArrayList() + private var lastTransitionError: String? = null + + init { + keysBackup.addListener(this) + } + + // TODO Make expectedStates mandatory to enforce test + fun stopAndCheckStates(expectedStates: List?) { + keysBackup.removeListener(this) + + expectedStates?.let { + assertEquals(it.size, stateList.size) + + for (i in it.indices) { + assertEquals("The state $i is not correct. states: " + stateList.joinToString(separator = " "), it[i], stateList[i]) + } + } + + assertNull("states: " + stateList.joinToString(separator = " "), lastTransitionError) + } + + override fun onStateChange(newState: KeysBackupState) { + stateList.add(newState) + + // Check that state transition is valid + if (stateList.size >= 2 + && !allowedStateTransitions.contains(stateList[stateList.size - 2] to newState)) { + // Forbidden transition detected + lastTransitionError = "Forbidden transition detected from " + stateList[stateList.size - 2] + " to " + newState + } + + if (expectedStateChange == stateList.size) { + latch?.countDown() + } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/SASTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/SASTest.kt new file mode 100644 index 0000000000..c05523f009 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/SASTest.kt @@ -0,0 +1,525 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.verification + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import im.vector.matrix.android.InstrumentedTest +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.crypto.sas.* +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.common.CommonTestHelper +import im.vector.matrix.android.common.CryptoTestHelper +import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap +import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationAccept +import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationCancel +import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart +import org.junit.Assert.* +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import java.util.* +import java.util.concurrent.CountDownLatch + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class SASTest : InstrumentedTest { + private val mTestHelper = CommonTestHelper(context()) + private val mCryptoTestHelper = CryptoTestHelper(mTestHelper) + + @Test + fun test_aliceStartThenAliceCancel() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession + + val aliceSasMgr = aliceSession.getSasVerificationService() + val bobSasMgr = bobSession!!.getSasVerificationService() + + val bobTxCreatedLatch = CountDownLatch(1) + val bobListener = object : SasVerificationService.SasVerificationListener { + override fun transactionCreated(tx: SasVerificationTransaction) {} + + override fun transactionUpdated(tx: SasVerificationTransaction) { + bobTxCreatedLatch.countDown() + } + + override fun markedAsManuallyVerified(userId: String, deviceId: String) {} + } + bobSasMgr.addListener(bobListener) + + val txID = aliceSasMgr.beginKeyVerificationSAS(bobSession.myUserId, bobSession.getMyDevice().deviceId) + assertNotNull("Alice should have a started transaction", txID) + + val aliceKeyTx = aliceSasMgr.getExistingTransaction(bobSession.myUserId, txID!!) + assertNotNull("Alice should have a started transaction", aliceKeyTx) + + mTestHelper.await(bobTxCreatedLatch) + bobSasMgr.removeListener(bobListener) + + val bobKeyTx = bobSasMgr.getExistingTransaction(aliceSession.myUserId, txID) + + assertNotNull("Bob should have started verif transaction", bobKeyTx) + assertTrue(bobKeyTx is SASVerificationTransaction) + assertNotNull("Bob should have starting a SAS transaction", bobKeyTx) + assertTrue(aliceKeyTx is SASVerificationTransaction) + assertEquals("Alice and Bob have same transaction id", aliceKeyTx!!.transactionId, bobKeyTx!!.transactionId) + + val aliceSasTx = aliceKeyTx as SASVerificationTransaction? + val bobSasTx = bobKeyTx as SASVerificationTransaction? + + assertEquals("Alice state should be started", SasVerificationTxState.Started, aliceSasTx!!.state) + assertEquals("Bob state should be started by alice", SasVerificationTxState.OnStarted, bobSasTx!!.state) + + // Let's cancel from alice side + val cancelLatch = CountDownLatch(1) + + val bobListener2 = object : SasVerificationService.SasVerificationListener { + override fun transactionCreated(tx: SasVerificationTransaction) {} + + override fun transactionUpdated(tx: SasVerificationTransaction) { + if (tx.transactionId == txID) { + if ((tx as SASVerificationTransaction).state === SasVerificationTxState.OnCancelled) { + cancelLatch.countDown() + } + } + } + + override fun markedAsManuallyVerified(userId: String, deviceId: String) {} + } + bobSasMgr.addListener(bobListener2) + + aliceSasTx.cancel(CancelCode.User) + mTestHelper.await(cancelLatch) + + assertEquals("Should be cancelled on alice side", + SasVerificationTxState.Cancelled, aliceSasTx.state) + assertEquals("Should be cancelled on bob side", + SasVerificationTxState.OnCancelled, bobSasTx.state) + + assertEquals("Should be User cancelled on alice side", + CancelCode.User, aliceSasTx.cancelledReason) + assertEquals("Should be User cancelled on bob side", + CancelCode.User, aliceSasTx.cancelledReason) + + assertNull(bobSasMgr.getExistingTransaction(aliceSession.myUserId, txID)) + assertNull(aliceSasMgr.getExistingTransaction(bobSession.myUserId, txID)) + + cryptoTestData.close() + } + + @Test + fun test_key_agreement_protocols_must_include_curve25519() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val bobSession = cryptoTestData.secondSession!! + + val protocols = listOf("meh_dont_know") + val tid = "00000000" + + // Bob should receive a cancel + var canceledToDeviceEvent: Event? = null + val cancelLatch = CountDownLatch(1) + // TODO bobSession!!.dataHandler.addListener(object : MXEventListener() { + // TODO override fun onToDeviceEvent(event: Event?) { + // TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) { + // TODO if (event.contentAsJsonObject?.get("transaction_id")?.asString == tid) { + // TODO canceledToDeviceEvent = event + // TODO cancelLatch.countDown() + // TODO } + // TODO } + // TODO } + // TODO }) + + val aliceSession = cryptoTestData.firstSession + val aliceUserID = aliceSession.myUserId + val aliceDevice = aliceSession.getMyDevice().deviceId + + val aliceListener = object : SasVerificationService.SasVerificationListener { + override fun transactionCreated(tx: SasVerificationTransaction) {} + + override fun transactionUpdated(tx: SasVerificationTransaction) { + if ((tx as IncomingSASVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) { + (tx as IncomingSASVerificationTransaction).performAccept() + } + } + + override fun markedAsManuallyVerified(userId: String, deviceId: String) {} + } + aliceSession.getSasVerificationService().addListener(aliceListener) + + fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, protocols = protocols) + + mTestHelper.await(cancelLatch) + + val cancelReq = canceledToDeviceEvent!!.content.toModel()!! + assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code) + + cryptoTestData.close() + } + + @Test + fun test_key_agreement_macs_Must_include_hmac_sha256() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val bobSession = cryptoTestData.secondSession!! + + val mac = listOf("shaBit") + val tid = "00000000" + + // Bob should receive a cancel + var canceledToDeviceEvent: Event? = null + val cancelLatch = CountDownLatch(1) + // TODO bobSession!!.dataHandler.addListener(object : MXEventListener() { + // TODO override fun onToDeviceEvent(event: Event?) { + // TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) { + // TODO if (event.contentAsJsonObject?.get("transaction_id")?.asString == tid) { + // TODO canceledToDeviceEvent = event + // TODO cancelLatch.countDown() + // TODO } + // TODO } + // TODO } + // TODO }) + + val aliceSession = cryptoTestData.firstSession + val aliceUserID = aliceSession.myUserId + val aliceDevice = aliceSession.getMyDevice().deviceId + + fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, mac = mac) + + mTestHelper.await(cancelLatch) + + val cancelReq = canceledToDeviceEvent!!.content.toModel()!! + assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code) + + cryptoTestData.close() + } + + @Test + fun test_key_agreement_short_code_include_decimal() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val bobSession = cryptoTestData.secondSession!! + + val codes = listOf("bin", "foo", "bar") + val tid = "00000000" + + // Bob should receive a cancel + var canceledToDeviceEvent: Event? = null + val cancelLatch = CountDownLatch(1) + // TODO bobSession!!.dataHandler.addListener(object : MXEventListener() { + // TODO override fun onToDeviceEvent(event: Event?) { + // TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) { + // TODO if (event.contentAsJsonObject?.get("transaction_id")?.asString == tid) { + // TODO canceledToDeviceEvent = event + // TODO cancelLatch.countDown() + // TODO } + // TODO } + // TODO } + // TODO }) + + val aliceSession = cryptoTestData.firstSession + val aliceUserID = aliceSession.myUserId + val aliceDevice = aliceSession.getMyDevice().deviceId + + fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, codes = codes) + + mTestHelper.await(cancelLatch) + + val cancelReq = canceledToDeviceEvent!!.content.toModel()!! + assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code) + + cryptoTestData.close() + } + + private fun fakeBobStart(bobSession: Session, + aliceUserID: String?, + aliceDevice: String?, + tid: String, + protocols: List = SASVerificationTransaction.KNOWN_AGREEMENT_PROTOCOLS, + hashes: List = SASVerificationTransaction.KNOWN_HASHES, + mac: List = SASVerificationTransaction.KNOWN_MACS, + codes: List = SASVerificationTransaction.KNOWN_SHORT_CODES) { + val startMessage = KeyVerificationStart() + startMessage.fromDevice = bobSession.getMyDevice().deviceId + startMessage.method = KeyVerificationStart.VERIF_METHOD_SAS + startMessage.transactionID = tid + startMessage.keyAgreementProtocols = protocols + startMessage.hashes = hashes + startMessage.messageAuthenticationCodes = mac + startMessage.shortAuthenticationStrings = codes + + val contentMap = MXUsersDevicesMap() + contentMap.setObject(aliceUserID, aliceDevice, startMessage) + + // TODO val sendLatch = CountDownLatch(1) + // TODO bobSession.cryptoRestClient.sendToDevice( + // TODO EventType.KEY_VERIFICATION_START, + // TODO contentMap, + // TODO tid, + // TODO TestMatrixCallback(sendLatch) + // TODO ) + } + + // any two devices may only have at most one key verification in flight at a time. + // If a device has two verifications in progress with the same device, then it should cancel both verifications. + @Test + fun test_aliceStartTwoRequests() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession + + val aliceSasMgr = aliceSession.getSasVerificationService() + + val aliceCreatedLatch = CountDownLatch(2) + val aliceCancelledLatch = CountDownLatch(2) + val createdTx = ArrayList() + val aliceListener = object : SasVerificationService.SasVerificationListener { + override fun transactionCreated(tx: SasVerificationTransaction) { + createdTx.add(tx as SASVerificationTransaction) + aliceCreatedLatch.countDown() + } + + override fun transactionUpdated(tx: SasVerificationTransaction) { + if ((tx as SASVerificationTransaction).state === SasVerificationTxState.OnCancelled) { + aliceCancelledLatch.countDown() + } + } + + override fun markedAsManuallyVerified(userId: String, deviceId: String) {} + } + aliceSasMgr.addListener(aliceListener) + + val bobUserId = bobSession!!.myUserId + val bobDeviceId = bobSession.getMyDevice().deviceId + aliceSasMgr.beginKeyVerificationSAS(bobUserId, bobDeviceId) + aliceSasMgr.beginKeyVerificationSAS(bobUserId, bobDeviceId) + + mTestHelper.await(aliceCreatedLatch) + mTestHelper.await(aliceCancelledLatch) + + cryptoTestData.close() + } + + /** + * Test that when alice starts a 'correct' request, bob agrees. + */ + @Test + fun test_aliceAndBobAgreement() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession + + val aliceSasMgr = aliceSession.getSasVerificationService() + val bobSasMgr = bobSession!!.getSasVerificationService() + + var accepted: KeyVerificationAccept? = null + var startReq: KeyVerificationStart? = null + + val aliceAcceptedLatch = CountDownLatch(1) + val aliceListener = object : SasVerificationService.SasVerificationListener { + override fun markedAsManuallyVerified(userId: String, deviceId: String) {} + + override fun transactionCreated(tx: SasVerificationTransaction) {} + + override fun transactionUpdated(tx: SasVerificationTransaction) { + if ((tx as SASVerificationTransaction).state === SasVerificationTxState.OnAccepted) { + val at = tx as SASVerificationTransaction + accepted = at.accepted + startReq = at.startReq + aliceAcceptedLatch.countDown() + } + } + } + aliceSasMgr.addListener(aliceListener) + + val bobListener = object : SasVerificationService.SasVerificationListener { + override fun transactionCreated(tx: SasVerificationTransaction) {} + + override fun transactionUpdated(tx: SasVerificationTransaction) { + if ((tx as IncomingSASVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) { + val at = tx as IncomingSASVerificationTransaction + at.performAccept() + } + } + + override fun markedAsManuallyVerified(userId: String, deviceId: String) {} + } + bobSasMgr.addListener(bobListener) + + val bobUserId = bobSession.myUserId + val bobDeviceId = bobSession.getMyDevice().deviceId + aliceSasMgr.beginKeyVerificationSAS(bobUserId, bobDeviceId) + mTestHelper.await(aliceAcceptedLatch) + + assertTrue("Should have receive a commitment", accepted!!.commitment?.trim()?.isEmpty() == false) + + // check that agreement is valid + assertTrue("Agreed Protocol should be Valid", accepted!!.isValid()) + assertTrue("Agreed Protocol should be known by alice", startReq!!.keyAgreementProtocols!!.contains(accepted!!.keyAgreementProtocol)) + assertTrue("Hash should be known by alice", startReq!!.hashes!!.contains(accepted!!.hash)) + assertTrue("Hash should be known by alice", startReq!!.messageAuthenticationCodes!!.contains(accepted!!.messageAuthenticationCode)) + + accepted!!.shortAuthenticationStrings?.forEach { + assertTrue("all agreed Short Code should be known by alice", startReq!!.shortAuthenticationStrings!!.contains(it)) + } + + cryptoTestData.close() + } + + @Test + fun test_aliceAndBobSASCode() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession + + val aliceSasMgr = aliceSession.getSasVerificationService() + val bobSasMgr = bobSession!!.getSasVerificationService() + + val aliceSASLatch = CountDownLatch(1) + val aliceListener = object : SasVerificationService.SasVerificationListener { + override fun transactionCreated(tx: SasVerificationTransaction) {} + + override fun transactionUpdated(tx: SasVerificationTransaction) { + val uxState = (tx as OutgoingSASVerificationRequest).uxState + when (uxState) { + OutgoingSasVerificationRequest.UxState.SHOW_SAS -> { + aliceSASLatch.countDown() + } + else -> Unit + } + } + + override fun markedAsManuallyVerified(userId: String, deviceId: String) {} + } + aliceSasMgr.addListener(aliceListener) + + val bobSASLatch = CountDownLatch(1) + val bobListener = object : SasVerificationService.SasVerificationListener { + override fun transactionCreated(tx: SasVerificationTransaction) {} + + override fun transactionUpdated(tx: SasVerificationTransaction) { + val uxState = (tx as IncomingSASVerificationTransaction).uxState + when (uxState) { + IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> { + tx.performAccept() + } + else -> Unit + } + if (uxState === IncomingSasVerificationTransaction.UxState.SHOW_SAS) { + bobSASLatch.countDown() + } + } + + override fun markedAsManuallyVerified(userId: String, deviceId: String) {} + } + bobSasMgr.addListener(bobListener) + + val bobUserId = bobSession.myUserId + val bobDeviceId = bobSession.getMyDevice().deviceId + val verificationSAS = aliceSasMgr.beginKeyVerificationSAS(bobUserId, bobDeviceId) + mTestHelper.await(aliceSASLatch) + mTestHelper.await(bobSASLatch) + + val aliceTx = aliceSasMgr.getExistingTransaction(bobUserId, verificationSAS!!) as SASVerificationTransaction + val bobTx = bobSasMgr.getExistingTransaction(aliceSession.myUserId, verificationSAS) as SASVerificationTransaction + + assertEquals("Should have same SAS", aliceTx.getShortCodeRepresentation(SasMode.DECIMAL), + bobTx.getShortCodeRepresentation(SasMode.DECIMAL)) + + cryptoTestData.close() + } + + @Test + fun test_happyPath() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession + + val aliceSasMgr = aliceSession.getSasVerificationService() + val bobSasMgr = bobSession!!.getSasVerificationService() + + val aliceSASLatch = CountDownLatch(1) + val aliceListener = object : SasVerificationService.SasVerificationListener { + override fun transactionCreated(tx: SasVerificationTransaction) {} + + override fun transactionUpdated(tx: SasVerificationTransaction) { + val uxState = (tx as OutgoingSASVerificationRequest).uxState + when (uxState) { + OutgoingSasVerificationRequest.UxState.SHOW_SAS -> { + tx.userHasVerifiedShortCode() + } + OutgoingSasVerificationRequest.UxState.VERIFIED -> { + aliceSASLatch.countDown() + } + else -> Unit + } + } + + override fun markedAsManuallyVerified(userId: String, deviceId: String) {} + } + aliceSasMgr.addListener(aliceListener) + + val bobSASLatch = CountDownLatch(1) + val bobListener = object : SasVerificationService.SasVerificationListener { + override fun transactionCreated(tx: SasVerificationTransaction) {} + + override fun transactionUpdated(tx: SasVerificationTransaction) { + val uxState = (tx as IncomingSASVerificationTransaction).uxState + when (uxState) { + IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> { + tx.performAccept() + } + IncomingSasVerificationTransaction.UxState.SHOW_SAS -> { + tx.userHasVerifiedShortCode() + } + IncomingSasVerificationTransaction.UxState.VERIFIED -> { + bobSASLatch.countDown() + } + else -> Unit + } + } + + override fun markedAsManuallyVerified(userId: String, deviceId: String) {} + } + bobSasMgr.addListener(bobListener) + + val bobUserId = bobSession.myUserId + val bobDeviceId = bobSession.getMyDevice().deviceId + aliceSasMgr.beginKeyVerificationSAS(bobUserId, bobDeviceId) + mTestHelper.await(aliceSASLatch) + mTestHelper.await(bobSASLatch) + + // Assert that devices are verified + val bobDeviceInfoFromAlicePOV: MXDeviceInfo? = aliceSession.getDeviceInfo(bobUserId, bobDeviceId) + val aliceDeviceInfoFromBobPOV: MXDeviceInfo? = bobSession.getDeviceInfo(aliceSession.myUserId, aliceSession.getMyDevice().deviceId) + + // latch wait a bit again + Thread.sleep(1000) + + assertTrue("alice device should be verified from bob point of view", aliceDeviceInfoFromBobPOV!!.isVerified) + assertTrue("bob device should be verified from alice point of view", bobDeviceInfoFromAlicePOV!!.isVerified) + cryptoTestData.close() + } +} diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt index 592086b0ec..3980094175 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt @@ -19,7 +19,10 @@ package im.vector.matrix.android.session.room.timeline import androidx.test.ext.junit.runners.AndroidJUnit4 import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.InstrumentedTest -import im.vector.matrix.android.internal.database.helper.* +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.internal.database.helper.add +import im.vector.matrix.android.internal.database.helper.lastStateIndex +import im.vector.matrix.android.internal.database.helper.merge import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.SessionRealmModule import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection @@ -29,7 +32,6 @@ import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeR import io.realm.Realm import io.realm.RealmConfiguration import io.realm.kotlin.createObject -import org.amshove.kluent.shouldBeFalse import org.amshove.kluent.shouldBeTrue import org.amshove.kluent.shouldEqual import org.junit.Before @@ -146,30 +148,6 @@ internal class ChunkEntityTest : InstrumentedTest { } } - @Test - fun merge_shouldEventsBeLinked_whenMergingLinkedWithUnlinked() { - monarchy.runTransactionSync { realm -> - val chunk1: ChunkEntity = realm.createObject() - val chunk2: ChunkEntity = realm.createObject() - chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) - chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = false) - chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS) - chunk1.isUnlinked().shouldBeFalse() - } - } - - @Test - fun merge_shouldEventsBeUnlinked_whenMergingUnlinkedWithUnlinked() { - monarchy.runTransactionSync { realm -> - val chunk1: ChunkEntity = realm.createObject() - val chunk2: ChunkEntity = realm.createObject() - chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) - chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) - chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS) - chunk1.isUnlinked().shouldBeTrue() - } - } - @Test fun merge_shouldPrevTokenMerged_whenMergingForwards() { monarchy.runTransactionSync { realm -> @@ -177,8 +155,8 @@ internal class ChunkEntityTest : InstrumentedTest { val chunk2: ChunkEntity = realm.createObject() val prevToken = "prev_token" chunk1.prevToken = prevToken - chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) - chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) + chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS) chunk1.merge("roomId", chunk2, PaginationDirection.FORWARDS) chunk1.prevToken shouldEqual prevToken } @@ -191,10 +169,19 @@ internal class ChunkEntityTest : InstrumentedTest { val chunk2: ChunkEntity = realm.createObject() val nextToken = "next_token" chunk1.nextToken = nextToken - chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) - chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) + chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS) chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS) chunk1.nextToken shouldEqual nextToken } } + + private fun ChunkEntity.addAll(roomId: String, + events: List, + direction: PaginationDirection, + stateIndexOffset: Int = 0) { + events.forEach { event -> + add(roomId, event, direction, stateIndexOffset) + } + } } diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt index 8a8ee11854..dd4daee9cd 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.session.room.timeline -import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType @@ -25,12 +24,6 @@ import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.matrix.android.api.session.room.model.message.MessageType -import im.vector.matrix.android.internal.database.helper.addAll -import im.vector.matrix.android.internal.database.helper.addOrUpdate -import im.vector.matrix.android.internal.database.model.ChunkEntity -import im.vector.matrix.android.internal.database.model.RoomEntity -import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection -import io.realm.kotlin.createObject import kotlin.random.Random object RoomDataHelper { @@ -73,19 +66,4 @@ object RoomDataHelper { val roomMember = RoomMember(Membership.JOIN, "Fake name #${Random.nextLong()}").toContent() return createFakeEvent(EventType.STATE_ROOM_MEMBER, roomMember) } - - fun fakeInitialSync(monarchy: Monarchy, roomId: String) { - monarchy.runTransactionSync { realm -> - val roomEntity = realm.createObject(roomId) - roomEntity.membership = Membership.JOIN - val eventList = createFakeListOfEvents(10) - val chunkEntity = realm.createObject().apply { - nextToken = null - prevToken = Random.nextLong(System.currentTimeMillis()).toString() - isLastForward = true - } - chunkEntity.addAll(roomId, eventList, PaginationDirection.FORWARDS) - roomEntity.addOrUpdate(chunkEntity) - } - } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt index cf0302166f..72affe24bb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.api.auth.data import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.util.md5 /** * This data class hold credentials user data. @@ -34,3 +35,7 @@ data class Credentials( // Optional data that may contain info to override home server and/or identity server @Json(name = "well_known") val wellKnown: WellKnown? = null ) + +internal fun Credentials.sessionId(): String { + return (if (deviceId.isNullOrBlank()) userId else "$userId|$deviceId").md5() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/MatrixSdkExtensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/MatrixSdkExtensions.kt index 685a522f60..bada3f86a1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/MatrixSdkExtensions.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/MatrixSdkExtensions.kt @@ -28,6 +28,12 @@ fun MXDeviceInfo.getFingerprintHumanReadable() = fingerprint() ?.chunked(4) ?.joinToString(separator = " ") -fun MutableList.sortByLastSeen() { - sortWith(DatedObjectComparators.descComparator) +/* ========================================================================================== + * DeviceInfo + * ========================================================================================== */ + +fun List.sortByLastSeen(): List { + val list = toMutableList() + list.sortWith(DatedObjectComparators.descComparator) + return list } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/query/QueryStringValue.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/query/QueryStringValue.kt new file mode 100644 index 0000000000..5d3e76f1d3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/query/QueryStringValue.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.query + +/** + * Basic query language. All these cases are mutually exclusive. + */ +sealed class QueryStringValue { + object NoCondition : QueryStringValue() + object IsNull : QueryStringValue() + object IsNotNull : QueryStringValue() + object IsEmpty : QueryStringValue() + object IsNotEmpty : QueryStringValue() + data class Equals(val string: String, val case: Case) : QueryStringValue() + data class Contains(val string: String, val case: Case) : QueryStringValue() + + enum class Case { + SENSITIVE, + INSENSITIVE + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt index ab545dbce6..339e6ac4a8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt @@ -73,6 +73,11 @@ interface Session : val myUserId: String get() = sessionParams.credentials.userId + /** + * The sessionId + */ + val sessionId: String + /** * This method allow to open a session. It does start some service on the background. */ @@ -107,7 +112,7 @@ interface Session : * This method allows to listen the sync state. * @return a [LiveData] of [SyncState]. */ - fun syncState(): LiveData + fun getSyncStateLive(): LiveData /** * This methods return true if an initial sync has been processed diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt index 706f89dfc9..986cbb698b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt @@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap +import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody @@ -89,6 +90,8 @@ interface CryptoService { fun getDevicesList(callback: MatrixCallback) + fun getDeviceInfo(deviceId: String, callback: MatrixCallback) + fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int fun isRoomEncrypted(roomId: String): Boolean diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt index 1939b1f0e0..41548710eb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt @@ -25,7 +25,6 @@ object EventType { const val MESSAGE = "m.room.message" const val STICKER = "m.sticker" const val ENCRYPTED = "m.room.encrypted" - const val ENCRYPTION = "m.room.encryption" const val FEEDBACK = "m.room.message.feedback" const val TYPING = "m.typing" const val REDACTION = "m.room.redaction" @@ -54,6 +53,7 @@ object EventType { const val STATE_ROOM_HISTORY_VISIBILITY = "m.room.history_visibility" const val STATE_ROOM_RELATED_GROUPS = "m.room.related_groups" const val STATE_ROOM_PINNED_EVENT = "m.room.pinned_events" + const val STATE_ROOM_ENCRYPTION = "m.room.encryption" // Call Events diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupService.kt index 2d55d0be57..c01e5b5cd8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupService.kt @@ -38,9 +38,15 @@ interface GroupService { */ fun getGroupSummary(groupId: String): GroupSummary? + /** + * Get a list of group summaries. This list is a snapshot of the data. + * @return the list of [GroupSummary] + */ + fun getGroupSummaries(groupSummaryQueryParams: GroupSummaryQueryParams): List + /** * Get a live list of group summaries. This list is refreshed as soon as the data changes. * @return the [LiveData] of [GroupSummary] */ - fun liveGroupSummaries(): LiveData> + fun getGroupSummariesLive(groupSummaryQueryParams: GroupSummaryQueryParams): LiveData> } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupSummaryQueryParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupSummaryQueryParams.kt new file mode 100644 index 0000000000..702b8c2523 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupSummaryQueryParams.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.group + +import im.vector.matrix.android.api.query.QueryStringValue +import im.vector.matrix.android.api.session.room.model.Membership + +fun groupSummaryQueryParams(init: (GroupSummaryQueryParams.Builder.() -> Unit) = {}): GroupSummaryQueryParams { + return GroupSummaryQueryParams.Builder().apply(init).build() +} + +/** + * This class can be used to filter group summaries + */ +data class GroupSummaryQueryParams( + val displayName: QueryStringValue, + val memberships: List +) { + + class Builder { + + var displayName: QueryStringValue = QueryStringValue.IsNotEmpty + var memberships: List = Membership.all() + + fun build() = GroupSummaryQueryParams( + displayName = displayName, + memberships = memberships + ) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/pushers/PushersService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/pushers/PushersService.kt index d082faa7c7..129bfa3011 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/pushers/PushersService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/pushers/PushersService.kt @@ -58,7 +58,7 @@ interface PushersService { const val EVENT_ID_ONLY = "event_id_only" } - fun livePushers(): LiveData> + fun getPushersLive(): LiveData> fun pushers() : List } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt index 90790a6ab0..3221c355e8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt @@ -56,5 +56,8 @@ interface Room : */ fun getRoomSummaryLive(): LiveData> + /** + * A current snapshot of [RoomSummary] associated with the room + */ fun roomSummary(): RoomSummary? } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt index fe110b7b9c..9fec605bd9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt @@ -60,16 +60,28 @@ interface RoomService { fun getRoomSummary(roomIdOrAlias: String): RoomSummary? /** - * Get a live list of room summaries. This list is refreshed as soon as the data changes. - * @return the [LiveData] of [RoomSummary] + * Get a snapshot list of room summaries. + * @return the immutable list of [RoomSummary] */ - fun liveRoomSummaries(): LiveData> + fun getRoomSummaries(queryParams: RoomSummaryQueryParams): List + + /** + * Get a live list of room summaries. This list is refreshed as soon as the data changes. + * @return the [LiveData] of List[RoomSummary] + */ + fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams): LiveData> + + /** + * Get a snapshot list of Breadcrumbs + * @return the immutable list of [RoomSummary] + */ + fun getBreadcrumbs(): List /** * Get a live list of Breadcrumbs * @return the [LiveData] of [RoomSummary] */ - fun liveBreadcrumbs(): LiveData> + fun getBreadcrumbsLive(): LiveData> /** * Inform the Matrix SDK that a room is displayed. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomSummaryQueryParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomSummaryQueryParams.kt new file mode 100644 index 0000000000..6983bda225 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomSummaryQueryParams.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.room + +import im.vector.matrix.android.api.query.QueryStringValue +import im.vector.matrix.android.api.session.room.model.Membership + +fun roomSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): RoomSummaryQueryParams { + return RoomSummaryQueryParams.Builder().apply(init).build() +} + +/** + * This class can be used to filter room summaries to use with: + * [im.vector.matrix.android.api.session.room.Room] and [im.vector.matrix.android.api.session.room.RoomService] + */ +data class RoomSummaryQueryParams( + val displayName: QueryStringValue, + val canonicalAlias: QueryStringValue, + val memberships: List +) { + + class Builder { + + var displayName: QueryStringValue = QueryStringValue.IsNotEmpty + var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition + var memberships: List = Membership.all() + + fun build() = RoomSummaryQueryParams( + displayName = displayName, + canonicalAlias = canonicalAlias, + memberships = memberships + ) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/crypto/RoomCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/crypto/RoomCryptoService.kt index f8c15fde47..124b2aef17 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/crypto/RoomCryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/crypto/RoomCryptoService.kt @@ -16,6 +16,8 @@ package im.vector.matrix.android.api.session.room.crypto +import im.vector.matrix.android.api.MatrixCallback + interface RoomCryptoService { fun isEncrypted(): Boolean @@ -23,4 +25,6 @@ interface RoomCryptoService { fun encryptionAlgorithm(): String? fun shouldEncryptForInvitedMembers(): Boolean + + fun enableEncryptionWithAlgorithm(algorithm: String, callback: MatrixCallback) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt index 34af2cf572..6c117d3be7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt @@ -41,11 +41,18 @@ interface MembershipService { fun getRoomMember(userId: String): RoomMember? /** - * Return all the roomMembers ids of the room - * + * Return all the roomMembers of the room with params + * @param queryParams the params to query for + * @return a roomMember list. + */ + fun getRoomMembers(queryParams: RoomMemberQueryParams): List + + /** + * Return all the roomMembers of the room filtered by memberships + * @param queryParams the params to query for * @return a [LiveData] of roomMember list. */ - fun getRoomMemberIdsLive(): LiveData> + fun getRoomMembersLive(queryParams: RoomMemberQueryParams): LiveData> fun getNumberOfJoinedMembers(): Int diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMemberQueryParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMemberQueryParams.kt new file mode 100644 index 0000000000..19003632ca --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMemberQueryParams.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.room.members + +import im.vector.matrix.android.api.query.QueryStringValue +import im.vector.matrix.android.api.session.room.model.Membership + +fun roomMemberQueryParams(init: (RoomMemberQueryParams.Builder.() -> Unit) = {}): RoomMemberQueryParams { + return RoomMemberQueryParams.Builder().apply(init).build() +} + +/** + * This class can be used to filter room members + */ +data class RoomMemberQueryParams( + val displayName: QueryStringValue, + val memberships: List +) { + + class Builder { + + var displayName: QueryStringValue = QueryStringValue.IsNotEmpty + var memberships: List = Membership.all() + + fun build() = RoomMemberQueryParams( + displayName = displayName, + memberships = memberships + ) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt index 1894effc7a..7c6a931373 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt @@ -43,4 +43,14 @@ enum class Membership(val value: String) { fun isLeft(): Boolean { return this == KNOCK || this == LEAVE || this == BAN } + + companion object { + fun activeMemberships(): List { + return listOf(INVITE, JOIN) + } + + fun all(): List { + return values().asList() + } + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMember.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMember.kt index 6a4d8e3c94..994c27be4d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMember.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMember.kt @@ -16,23 +16,12 @@ package im.vector.matrix.android.api.session.room.model -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import im.vector.matrix.android.api.session.events.model.UnsignedData - /** - * Class representing the EventType.STATE_ROOM_MEMBER state event content + * Class representing a simplified version of EventType.STATE_ROOM_MEMBER state event content */ -@JsonClass(generateAdapter = true) data class RoomMember( - @Json(name = "membership") val membership: Membership, - @Json(name = "reason") val reason: String? = null, - @Json(name = "displayname") val displayName: String? = null, - @Json(name = "avatar_url") val avatarUrl: String? = null, - @Json(name = "is_direct") val isDirect: Boolean = false, - @Json(name = "third_party_invite") val thirdPartyInvite: Invite? = null, - @Json(name = "unsigned") val unsignedData: UnsignedData? = null -) { - val safeReason - get() = reason?.takeIf { it.isNotBlank() } -} + val membership: Membership, + val userId: String, + val displayName: String? = null, + val avatarUrl: String? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMemberContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMemberContent.kt new file mode 100644 index 0000000000..deeeb8ba52 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMemberContent.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.events.model.UnsignedData + +/** + * Class representing the EventType.STATE_ROOM_MEMBER state event content + */ +@JsonClass(generateAdapter = true) +data class RoomMemberContent( + @Json(name = "membership") val membership: Membership, + @Json(name = "reason") val reason: String? = null, + @Json(name = "displayname") val displayName: String? = null, + @Json(name = "avatar_url") val avatarUrl: String? = null, + @Json(name = "is_direct") val isDirect: Boolean = false, + @Json(name = "third_party_invite") val thirdPartyInvite: Invite? = null, + @Json(name = "unsigned") val unsignedData: UnsignedData? = null +) { + val safeReason + get() = reason?.takeIf { it.isNotBlank() } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt index 129c35a17e..c18645ddbd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt @@ -41,7 +41,8 @@ data class RoomSummary( val membership: Membership = Membership.NONE, val versioningState: VersioningState = VersioningState.NONE, val readMarkerId: String? = null, - val userDrafts: List = emptyList() + val userDrafts: List = emptyList(), + var isEncrypted: Boolean ) { val isVersioned: Boolean diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt index bc1e941698..dbdd5b5a34 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt @@ -125,7 +125,7 @@ class CreateRoomParams { val contentMap = HashMap() contentMap["algorithm"] = algorithm - val algoEvent = Event(type = EventType.ENCRYPTION, + val algoEvent = Event(type = EventType.STATE_ROOM_ENCRYPTION, stateKey = "", content = contentMap.toContent() ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt index 7d8f2f0bc1..31ed4e9986 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt @@ -108,5 +108,17 @@ interface RelationService { replyText: CharSequence, autoMarkdown: Boolean = false): Cancelable? - fun getEventSummaryLive(eventId: String): LiveData> + /** + * Get the current EventAnnotationsSummary + * @param eventId the eventId to look for EventAnnotationsSummary + * @return the EventAnnotationsSummary found + */ + fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary? + + /** + * Get a LiveData of EventAnnotationsSummary for the specified eventId + * @param eventId the eventId to look for EventAnnotationsSummary + * @return the LiveData of EventAnnotationsSummary + */ + fun getEventAnnotationsSummaryLive(eventId: String): LiveData> } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt index 06f4a9c7ee..3372eb874c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt @@ -26,5 +26,10 @@ interface StateService { */ fun updateTopic(topic: String, callback: MatrixCallback) + /** + * Enable encryption of the room + */ + fun enableEncryption(algorithm: String, callback: MatrixCallback) + fun getStateEvent(eventType: String): Event? } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/signout/SignOutService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/signout/SignOutService.kt index 76ca9291ec..3fb086ac45 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/signout/SignOutService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/signout/SignOutService.kt @@ -40,8 +40,8 @@ interface SignOutService { /** * Sign out, and release the session, clear all the session data, including crypto data - * @param sigOutFromHomeserver true if the sign out request has to be done + * @param signOutFromHomeserver true if the sign out request has to be done */ - fun signOut(sigOutFromHomeserver: Boolean, + fun signOut(signOutFromHomeserver: Boolean, callback: MatrixCallback): Cancelable } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt index 2a93a876f6..453400bc99 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt @@ -50,25 +50,25 @@ interface UserService { * @param userId the userId to look for. * @return a LiveData of user with userId */ - fun liveUser(userId: String): LiveData> + fun getUserLive(userId: String): LiveData> /** * Observe a live list of users sorted alphabetically * @return a Livedata of users */ - fun liveUsers(): LiveData> + fun getUsersLive(): LiveData> /** * Observe a live [PagedList] of users sorted alphabetically. You can filter the users. * @param filter the filter. It will look into userId and displayName. * @return a Livedata of users */ - fun livePagedUsers(filter: String? = null): LiveData> + fun getPagedUsersLive(filter: String? = null): LiveData> /** * Get list of ignored users */ - fun liveIgnoredUsers(): LiveData> + fun getIgnoredUsersLive(): LiveData> /** * Ignore users diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt index 4c8082b77e..d6ef522f41 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.api.util import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.api.session.group.model.GroupSummary +import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.matrix.android.api.session.user.model.User @@ -146,3 +147,4 @@ fun GroupSummary.toMatrixItem() = MatrixItem.GroupItem(groupId, displayName, ava fun RoomSummary.toMatrixItem() = MatrixItem.RoomItem(roomId, displayName, avatarUrl) fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlias ?: roomId, displayName, avatarUrl) fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name, avatarUrl) +fun RoomMember.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/SessionManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/SessionManager.kt index c813a6813f..918f5f2f55 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/SessionManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/SessionManager.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.internal import im.vector.matrix.android.api.auth.data.SessionParams +import im.vector.matrix.android.api.auth.data.sessionId import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.internal.auth.SessionParamsStore import im.vector.matrix.android.internal.di.MatrixComponent @@ -29,10 +30,11 @@ import javax.inject.Inject internal class SessionManager @Inject constructor(private val matrixComponent: MatrixComponent, private val sessionParamsStore: SessionParamsStore) { + // SessionId -> SessionComponent private val sessionComponents = HashMap() - fun getSessionComponent(userId: String): SessionComponent? { - val sessionParams = sessionParamsStore.get(userId) ?: return null + fun getSessionComponent(sessionId: String): SessionComponent? { + val sessionParams = sessionParamsStore.get(sessionId) ?: return null return getOrCreateSessionComponent(sessionParams) } @@ -40,17 +42,17 @@ internal class SessionManager @Inject constructor(private val matrixComponent: M return getOrCreateSessionComponent(sessionParams).session() } - fun releaseSession(userId: String) { - if (sessionComponents.containsKey(userId).not()) { - throw RuntimeException("You don't have a session for the user $userId") + fun releaseSession(sessionId: String) { + if (sessionComponents.containsKey(sessionId).not()) { + throw RuntimeException("You don't have a session for id $sessionId") } - sessionComponents.remove(userId)?.also { + sessionComponents.remove(sessionId)?.also { it.session().close() } } private fun getOrCreateSessionComponent(sessionParams: SessionParams): SessionComponent { - return sessionComponents.getOrPut(sessionParams.credentials.userId) { + return sessionComponents.getOrPut(sessionParams.credentials.sessionId()) { DaggerSessionComponent .factory() .create(matrixComponent, sessionParams) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt index 93349f4bbc..d5dd7e2959 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt @@ -45,14 +45,15 @@ import okhttp3.OkHttpClient import javax.inject.Inject import javax.net.ssl.HttpsURLConnection -internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated - private val okHttpClient: Lazy, - private val retrofitFactory: RetrofitFactory, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val sessionParamsStore: SessionParamsStore, - private val sessionManager: SessionManager, - private val sessionCreator: SessionCreator, - private val pendingSessionStore: PendingSessionStore +internal class DefaultAuthenticationService @Inject constructor( + @Unauthenticated + private val okHttpClient: Lazy, + private val retrofitFactory: RetrofitFactory, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val sessionParamsStore: SessionParamsStore, + private val sessionManager: SessionManager, + private val sessionCreator: SessionCreator, + private val pendingSessionStore: PendingSessionStore ) : AuthenticationService { private var pendingSessionData: PendingSessionData? = pendingSessionStore.getPendingSessionData() @@ -112,7 +113,7 @@ internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated // First check the homeserver version runCatching { - executeRequest { + executeRequest(null) { apiCall = authAPI.versions() } } @@ -141,7 +142,7 @@ internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated val authAPI = buildAuthAPI(homeServerConnectionConfig) // Ok, try to get the config.json file of a RiotWeb client - val riotConfig = executeRequest { + val riotConfig = executeRequest(null) { apiCall = authAPI.getRiotConfig() } @@ -153,7 +154,7 @@ internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated val newAuthAPI = buildAuthAPI(newHomeServerConnectionConfig) - val versions = executeRequest { + val versions = executeRequest(null) { apiCall = newAuthAPI.versions() } @@ -167,7 +168,7 @@ internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated private suspend fun getLoginFlowResult(authAPI: AuthAPI, versions: Versions, homeServerUrl: String): LoginFlowResult { return if (versions.isSupportedBySdk()) { // Get the login flow - val loginFlowResponse = executeRequest { + val loginFlowResponse = executeRequest(null) { apiCall = authAPI.getLoginFlows() } LoginFlowResult.Success(loginFlowResponse, versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionParamsStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionParamsStore.kt index 57c22b0053..f99b95c2b3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionParamsStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionParamsStore.kt @@ -21,7 +21,7 @@ import im.vector.matrix.android.api.auth.data.SessionParams internal interface SessionParamsStore { - fun get(userId: String): SessionParams? + fun get(sessionId: String): SessionParams? fun getLast(): SessionParams? @@ -29,11 +29,11 @@ internal interface SessionParamsStore { suspend fun save(sessionParams: SessionParams) - suspend fun setTokenInvalid(userId: String) + suspend fun setTokenInvalid(sessionId: String) suspend fun updateCredentials(newCredentials: Credentials) - suspend fun delete(userId: String) + suspend fun delete(sessionId: String) suspend fun deleteAll() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt index 83bf7b7822..e5e77cb14a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt @@ -16,6 +16,9 @@ package im.vector.matrix.android.internal.auth.db +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.auth.data.sessionId +import im.vector.matrix.android.internal.di.MoshiProvider import io.realm.DynamicRealm import io.realm.RealmMigration import timber.log.Timber @@ -23,35 +26,59 @@ import timber.log.Timber internal object AuthRealmMigration : RealmMigration { // Current schema version - const val SCHEMA_VERSION = 2L + const val SCHEMA_VERSION = 3L override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { Timber.d("Migrating Auth Realm from $oldVersion to $newVersion") - if (oldVersion <= 0) { - Timber.d("Step 0 -> 1") - Timber.d("Create PendingSessionEntity") + if (oldVersion <= 0) migrateTo1(realm) + if (oldVersion <= 1) migrateTo2(realm) + if (oldVersion <= 2) migrateTo3(realm) + } - realm.schema.create("PendingSessionEntity") - .addField(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, String::class.java) - .setRequired(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, true) - .addField(PendingSessionEntityFields.CLIENT_SECRET, String::class.java) - .setRequired(PendingSessionEntityFields.CLIENT_SECRET, true) - .addField(PendingSessionEntityFields.SEND_ATTEMPT, Integer::class.java) - .setRequired(PendingSessionEntityFields.SEND_ATTEMPT, true) - .addField(PendingSessionEntityFields.RESET_PASSWORD_DATA_JSON, String::class.java) - .addField(PendingSessionEntityFields.CURRENT_SESSION, String::class.java) - .addField(PendingSessionEntityFields.IS_REGISTRATION_STARTED, Boolean::class.java) - .addField(PendingSessionEntityFields.CURRENT_THREE_PID_DATA_JSON, String::class.java) - } + private fun migrateTo1(realm: DynamicRealm) { + Timber.d("Step 0 -> 1") + Timber.d("Create PendingSessionEntity") - if (oldVersion <= 1) { - Timber.d("Step 1 -> 2") - Timber.d("Add boolean isTokenValid in SessionParamsEntity, with value true") + realm.schema.create("PendingSessionEntity") + .addField(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, String::class.java) + .setRequired(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, true) + .addField(PendingSessionEntityFields.CLIENT_SECRET, String::class.java) + .setRequired(PendingSessionEntityFields.CLIENT_SECRET, true) + .addField(PendingSessionEntityFields.SEND_ATTEMPT, Integer::class.java) + .setRequired(PendingSessionEntityFields.SEND_ATTEMPT, true) + .addField(PendingSessionEntityFields.RESET_PASSWORD_DATA_JSON, String::class.java) + .addField(PendingSessionEntityFields.CURRENT_SESSION, String::class.java) + .addField(PendingSessionEntityFields.IS_REGISTRATION_STARTED, Boolean::class.java) + .addField(PendingSessionEntityFields.CURRENT_THREE_PID_DATA_JSON, String::class.java) + } - realm.schema.get("SessionParamsEntity") - ?.addField(SessionParamsEntityFields.IS_TOKEN_VALID, Boolean::class.java) - ?.transform { it.set(SessionParamsEntityFields.IS_TOKEN_VALID, true) } - } + private fun migrateTo2(realm: DynamicRealm) { + Timber.d("Step 1 -> 2") + Timber.d("Add boolean isTokenValid in SessionParamsEntity, with value true") + + realm.schema.get("SessionParamsEntity") + ?.addField(SessionParamsEntityFields.IS_TOKEN_VALID, Boolean::class.java) + ?.transform { it.set(SessionParamsEntityFields.IS_TOKEN_VALID, true) } + } + + private fun migrateTo3(realm: DynamicRealm) { + Timber.d("Step 2 -> 3") + Timber.d("Update SessionParamsEntity primary key, to allow several sessions with the same userId") + + realm.schema.get("SessionParamsEntity") + ?.removePrimaryKey() + ?.addField(SessionParamsEntityFields.SESSION_ID, String::class.java) + ?.setRequired(SessionParamsEntityFields.SESSION_ID, true) + ?.transform { + val credentialsJson = it.getString(SessionParamsEntityFields.CREDENTIALS_JSON) + + val credentials = MoshiProvider.providesMoshi() + .adapter(Credentials::class.java) + .fromJson(credentialsJson) + + it.set(SessionParamsEntityFields.SESSION_ID, credentials!!.sessionId()) + } + ?.addPrimaryKey(SessionParamsEntityFields.SESSION_ID) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmSessionParamsStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmSessionParamsStore.kt index a4774c632a..9491d5737c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmSessionParamsStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmSessionParamsStore.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.auth.db import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.SessionParams +import im.vector.matrix.android.api.auth.data.sessionId import im.vector.matrix.android.internal.auth.SessionParamsStore import im.vector.matrix.android.internal.database.awaitTransaction import im.vector.matrix.android.internal.di.AuthDatabase @@ -42,11 +43,11 @@ internal class RealmSessionParamsStore @Inject constructor(private val mapper: S } } - override fun get(userId: String): SessionParams? { + override fun get(sessionId: String): SessionParams? { return Realm.getInstance(realmConfiguration).use { realm -> realm .where(SessionParamsEntity::class.java) - .equalTo(SessionParamsEntityFields.USER_ID, userId) + .equalTo(SessionParamsEntityFields.SESSION_ID, sessionId) .findAll() .map { mapper.map(it) } .firstOrNull() @@ -76,17 +77,17 @@ internal class RealmSessionParamsStore @Inject constructor(private val mapper: S } } - override suspend fun setTokenInvalid(userId: String) { + override suspend fun setTokenInvalid(sessionId: String) { awaitTransaction(realmConfiguration) { realm -> val currentSessionParams = realm .where(SessionParamsEntity::class.java) - .equalTo(SessionParamsEntityFields.USER_ID, userId) + .equalTo(SessionParamsEntityFields.SESSION_ID, sessionId) .findAll() .firstOrNull() if (currentSessionParams == null) { // Should not happen - "Session param not found for user $userId" + "Session param not found for id $sessionId" .let { Timber.w(it) } .also { error(it) } } else { @@ -99,14 +100,14 @@ internal class RealmSessionParamsStore @Inject constructor(private val mapper: S awaitTransaction(realmConfiguration) { realm -> val currentSessionParams = realm .where(SessionParamsEntity::class.java) - .equalTo(SessionParamsEntityFields.USER_ID, newCredentials.userId) + .equalTo(SessionParamsEntityFields.SESSION_ID, newCredentials.sessionId()) .findAll() .map { mapper.map(it) } .firstOrNull() if (currentSessionParams == null) { // Should not happen - "Session param not found for user ${newCredentials.userId}" + "Session param not found for id ${newCredentials.sessionId()}" .let { Timber.w(it) } .also { error(it) } } else { @@ -123,10 +124,10 @@ internal class RealmSessionParamsStore @Inject constructor(private val mapper: S } } - override suspend fun delete(userId: String) { + override suspend fun delete(sessionId: String) { awaitTransaction(realmConfiguration) { it.where(SessionParamsEntity::class.java) - .equalTo(SessionParamsEntityFields.USER_ID, userId) + .equalTo(SessionParamsEntityFields.SESSION_ID, sessionId) .findAll() .deleteAllFromRealm() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsEntity.kt index 92511dccf7..72eed95fcc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsEntity.kt @@ -20,7 +20,8 @@ import io.realm.RealmObject import io.realm.annotations.PrimaryKey internal open class SessionParamsEntity( - @PrimaryKey var userId: String = "", + @PrimaryKey var sessionId: String = "", + var userId: String = "", var credentialsJson: String = "", var homeServerConnectionConfigJson: String = "", // Set to false when the token is invalid and the user has been soft logged out diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt index 72e8087f3f..ebd50a6924 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt @@ -20,6 +20,7 @@ import com.squareup.moshi.Moshi import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.SessionParams +import im.vector.matrix.android.api.auth.data.sessionId import javax.inject.Inject internal class SessionParamsMapper @Inject constructor(moshi: Moshi) { @@ -49,6 +50,7 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) { return null } return SessionParamsEntity( + sessionParams.credentials.sessionId(), sessionParams.credentials.userId, credentialsJson, homeServerConnectionConfigJson, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DefaultLoginWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DefaultLoginWizard.kt index b847773682..4d98ddcf08 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DefaultLoginWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DefaultLoginWizard.kt @@ -72,7 +72,7 @@ internal class DefaultLoginWizard( } else { PasswordLoginParams.userIdentifier(login, password, deviceName) } - val credentials = executeRequest { + val credentials = executeRequest(null) { apiCall = authAPI.login(loginParams) } @@ -95,7 +95,7 @@ internal class DefaultLoginWizard( pendingSessionData = pendingSessionData.copy(sendAttempt = pendingSessionData.sendAttempt + 1) .also { pendingSessionStore.savePendingSessionData(it) } - val result = executeRequest { + val result = executeRequest(null) { apiCall = authAPI.resetPassword(AddThreePidRegistrationParams.from(param)) } @@ -120,7 +120,7 @@ internal class DefaultLoginWizard( resetPasswordData.newPassword ) - executeRequest { + executeRequest(null) { apiCall = authAPI.resetPasswordMailConfirmed(param) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegisterAddThreePidTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegisterAddThreePidTask.kt index 0246075153..c455ccf48c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegisterAddThreePidTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegisterAddThreePidTask.kt @@ -29,11 +29,12 @@ internal interface RegisterAddThreePidTask : Task { ) } -internal class DefaultRegisterTask(private val authAPI: AuthAPI) - : RegisterTask { +internal class DefaultRegisterTask( + private val authAPI: AuthAPI +) : RegisterTask { override suspend fun execute(params: RegisterTask.Params): Credentials { try { - return executeRequest { + return executeRequest(null) { apiCall = authAPI.register(params.registrationParams) } } catch (throwable: Throwable) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/ValidateCodeTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/ValidateCodeTask.kt index da75b839a6..30f9aaa705 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/ValidateCodeTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/ValidateCodeTask.kt @@ -27,11 +27,12 @@ internal interface ValidateCodeTask : Task onRoomEncryptionEvent(roomId, event) + event.getClearType() == EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) event.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) } @@ -157,7 +159,7 @@ internal class DefaultCryptoService @Inject constructor( fun onLiveEvent(roomId: String, event: Event) { when { - event.getClearType() == EventType.ENCRYPTION -> onRoomEncryptionEvent(roomId, event) + event.getClearType() == EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) event.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) } @@ -203,6 +205,14 @@ internal class DefaultCryptoService @Inject constructor( .executeBy(taskExecutor) } + override fun getDeviceInfo(deviceId: String, callback: MatrixCallback) { + getDeviceInfoTask + .configureWith(GetDeviceInfoTask.Params(deviceId)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + override fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int { return cryptoStore.inboundGroupSessionsCount(onlyBackedUp) } @@ -476,7 +486,7 @@ internal class DefaultCryptoService @Inject constructor( */ override fun isRoomEncrypted(roomId: String): Boolean { val encryptionEvent = monarchy.fetchCopied { - EventEntity.where(it, roomId = roomId, type = EventType.ENCRYPTION).findFirst() + EventEntity.where(it, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION).findFirst() } return encryptionEvent != null } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/SetDeviceVerificationAction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/SetDeviceVerificationAction.kt index 3eafa73fab..2d0c77c768 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/SetDeviceVerificationAction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/SetDeviceVerificationAction.kt @@ -22,9 +22,10 @@ import im.vector.matrix.android.internal.di.UserId import timber.log.Timber import javax.inject.Inject -internal class SetDeviceVerificationAction @Inject constructor(private val cryptoStore: IMXCryptoStore, - @UserId private val userId: String, - private val keysBackup: KeysBackup) { +internal class SetDeviceVerificationAction @Inject constructor( + private val cryptoStore: IMXCryptoStore, + @UserId private val userId: String, + private val keysBackup: KeysBackup) { fun handle(verificationStatus: Int, deviceId: String, userId: String) { val device = cryptoStore.getUserDevice(deviceId, userId) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/api/CryptoApi.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/api/CryptoApi.kt index f4821f8ef3..b2e880c2f3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/api/CryptoApi.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/api/CryptoApi.kt @@ -25,11 +25,18 @@ internal interface CryptoApi { /** * Get the devices list - * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-devices + * Doc: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-devices */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices") fun getDevices(): Call + /** + * Get the device info by id + * Doc: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-devices-deviceid + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices/{deviceId}") + fun getDeviceInfo(@Path("deviceId") deviceId: String): Call + /** * Upload device and/or one-time keys. * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-upload diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt index 91b3d6b056..99267ee89c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt @@ -1221,7 +1221,7 @@ internal class KeysBackup @Inject constructor( // Do not stay in KeysBackupState.WrongBackUpVersion but check what is available on the homeserver checkAndStartKeysBackup() } - else -> + else -> // Come back to the ready state so that we will retry on the next received key keysBackupStateManager.state = KeysBackupState.ReadyToBackUp } @@ -1339,6 +1339,31 @@ internal class KeysBackup @Inject constructor( return sessionBackupData } + /* ========================================================================================== + * For test only + * ========================================================================================== */ + + // Direct access for test only + @VisibleForTesting + val store + get() = cryptoStore + + @VisibleForTesting + fun createFakeKeysBackupVersion(keysBackupCreationInfo: MegolmBackupCreationInfo, + callback: MatrixCallback) { + val createKeysBackupVersionBody = CreateKeysBackupVersionBody() + createKeysBackupVersionBody.algorithm = keysBackupCreationInfo.algorithm + @Suppress("UNCHECKED_CAST") + createKeysBackupVersionBody.authData = MoshiProvider.providesMoshi().adapter(Map::class.java) + .fromJson(keysBackupCreationInfo.authData?.toJsonString() ?: "") as JsonDict? + + createKeysBackupVersionTask + .configureWith(createKeysBackupVersionBody) { + this.callback = callback + } + .executeBy(taskExecutor) + } + companion object { // Maximum delay in ms in {@link maybeBackupKeys} private const val KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS = 10_000L diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/CreateKeysBackupVersionTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/CreateKeysBackupVersionTask.kt index 78ae0f7ea6..9b8183bd02 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/CreateKeysBackupVersionTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/CreateKeysBackupVersionTask.kt @@ -21,15 +21,18 @@ import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.CreateKeys import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus import javax.inject.Inject internal interface CreateKeysBackupVersionTask : Task -internal class DefaultCreateKeysBackupVersionTask @Inject constructor(private val roomKeysApi: RoomKeysApi) - : CreateKeysBackupVersionTask { +internal class DefaultCreateKeysBackupVersionTask @Inject constructor( + private val roomKeysApi: RoomKeysApi, + private val eventBus: EventBus +) : CreateKeysBackupVersionTask { override suspend fun execute(params: CreateKeysBackupVersionBody): KeysVersion { - return executeRequest { + return executeRequest(eventBus) { apiCall = roomKeysApi.createKeysBackupVersion(params) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteBackupTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteBackupTask.kt index 2b1f3df353..9712bb099b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteBackupTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteBackupTask.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.crypto.keysbackup.tasks import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus import javax.inject.Inject internal interface DeleteBackupTask : Task { @@ -27,11 +28,13 @@ internal interface DeleteBackupTask : Task { ) } -internal class DefaultDeleteBackupTask @Inject constructor(private val roomKeysApi: RoomKeysApi) - : DeleteBackupTask { +internal class DefaultDeleteBackupTask @Inject constructor( + private val roomKeysApi: RoomKeysApi, + private val eventBus: EventBus +) : DeleteBackupTask { override suspend fun execute(params: DeleteBackupTask.Params) { - return executeRequest { + return executeRequest(eventBus) { apiCall = roomKeysApi.deleteBackup(params.version) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteRoomSessionDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteRoomSessionDataTask.kt index ccb3645ef3..72173ec7f4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteRoomSessionDataTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteRoomSessionDataTask.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.crypto.keysbackup.tasks import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus import javax.inject.Inject internal interface DeleteRoomSessionDataTask : Task { @@ -29,11 +30,13 @@ internal interface DeleteRoomSessionDataTask : Task { @@ -28,11 +29,13 @@ internal interface DeleteRoomSessionsDataTask : Task { @@ -27,11 +28,13 @@ internal interface DeleteSessionsDataTask : Task -internal class DefaultGetKeysBackupLastVersionTask @Inject constructor(private val roomKeysApi: RoomKeysApi) - : GetKeysBackupLastVersionTask { +internal class DefaultGetKeysBackupLastVersionTask @Inject constructor( + private val roomKeysApi: RoomKeysApi, + private val eventBus: EventBus +) : GetKeysBackupLastVersionTask { override suspend fun execute(params: Unit): KeysVersionResult { - return executeRequest { + return executeRequest(eventBus) { apiCall = roomKeysApi.getKeysBackupLastVersion() } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt index ea0b9b9f3a..70cc7472a9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt @@ -20,15 +20,18 @@ import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus import javax.inject.Inject internal interface GetKeysBackupVersionTask : Task -internal class DefaultGetKeysBackupVersionTask @Inject constructor(private val roomKeysApi: RoomKeysApi) - : GetKeysBackupVersionTask { +internal class DefaultGetKeysBackupVersionTask @Inject constructor( + private val roomKeysApi: RoomKeysApi, + private val eventBus: EventBus +) : GetKeysBackupVersionTask { override suspend fun execute(params: String): KeysVersionResult { - return executeRequest { + return executeRequest(eventBus) { apiCall = roomKeysApi.getKeysBackupVersion(params) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetRoomSessionDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetRoomSessionDataTask.kt index a36850ba08..327836ed5f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetRoomSessionDataTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetRoomSessionDataTask.kt @@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeyBackupData import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus import javax.inject.Inject internal interface GetRoomSessionDataTask : Task { @@ -30,11 +31,13 @@ internal interface GetRoomSessionDataTask : Task { @@ -29,11 +30,13 @@ internal interface GetRoomSessionsDataTask : Task { @@ -28,11 +29,13 @@ internal interface GetSessionsDataTask : Task { @@ -32,11 +33,13 @@ internal interface StoreRoomSessionDataTask : Task { @@ -31,11 +32,13 @@ internal interface StoreRoomSessionsDataTask : Task { @@ -30,11 +31,13 @@ internal interface StoreSessionsDataTask : Task { @@ -29,11 +30,13 @@ internal interface UpdateKeysBackupVersionTask : Task { val body = KeysClaimBody(oneTimeKeys = params.usersDevicesKeyTypesMap.map) - val keysClaimResponse = executeRequest { + val keysClaimResponse = executeRequest(eventBus) { apiCall = cryptoApi.claimOneTimeKeysForUsersDevices(body) } val map = MXUsersDevicesMap() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceTask.kt index 0e52c118d9..fbbaa0e0f7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceTask.kt @@ -23,6 +23,7 @@ import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceParams import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus import javax.inject.Inject internal interface DeleteDeviceTask : Task { @@ -31,12 +32,14 @@ internal interface DeleteDeviceTask : Task { ) } -internal class DefaultDeleteDeviceTask @Inject constructor(private val cryptoApi: CryptoApi) - : DeleteDeviceTask { +internal class DefaultDeleteDeviceTask @Inject constructor( + private val cryptoApi: CryptoApi, + private val eventBus: EventBus +) : DeleteDeviceTask { override suspend fun execute(params: DeleteDeviceTask.Params) { try { - executeRequest { + executeRequest(eventBus) { apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams()) } } catch (throwable: Throwable) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt index eb23f02275..19e0f6efb5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt @@ -23,6 +23,7 @@ import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceParams import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus import javax.inject.Inject internal interface DeleteDeviceWithUserPasswordTask : Task { @@ -33,12 +34,14 @@ internal interface DeleteDeviceWithUserPasswordTask : Task { @@ -31,8 +32,10 @@ internal interface DownloadKeysForUsersTask : Task() }.orEmpty() @@ -45,7 +48,7 @@ internal class DefaultDownloadKeysForUsers @Inject constructor(private val crypt body.token = params.token } - return executeRequest { + return executeRequest(eventBus) { apiCall = cryptoApi.downloadKeysForUsers(body) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetDeviceInfoTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetDeviceInfoTask.kt new file mode 100644 index 0000000000..9d9513b773 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetDeviceInfoTask.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.tasks + +import im.vector.matrix.android.internal.crypto.api.CryptoApi +import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetDeviceInfoTask : Task { + data class Params(val deviceId: String) +} + +internal class DefaultGetDeviceInfoTask @Inject constructor( + private val cryptoApi: CryptoApi, + private val eventBus: EventBus +) : GetDeviceInfoTask { + + override suspend fun execute(params: GetDeviceInfoTask.Params): DeviceInfo { + return executeRequest(eventBus) { + apiCall = cryptoApi.getDeviceInfo(params.deviceId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetDevicesTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetDevicesTask.kt index d6e82adb4e..7a805f6a08 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetDevicesTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetDevicesTask.kt @@ -20,15 +20,18 @@ import im.vector.matrix.android.internal.crypto.api.CryptoApi import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus import javax.inject.Inject internal interface GetDevicesTask : Task -internal class DefaultGetDevicesTask @Inject constructor(private val cryptoApi: CryptoApi) - : GetDevicesTask { +internal class DefaultGetDevicesTask @Inject constructor( + private val cryptoApi: CryptoApi, + private val eventBus: EventBus +) : GetDevicesTask { override suspend fun execute(params: Unit): DevicesListResponse { - return executeRequest { + return executeRequest(eventBus) { apiCall = cryptoApi.getDevices() } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetKeyChangesTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetKeyChangesTask.kt index 42c36bd1e7..84e2c293b9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetKeyChangesTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetKeyChangesTask.kt @@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.crypto.api.CryptoApi import im.vector.matrix.android.internal.crypto.model.rest.KeyChangesResponse import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus import javax.inject.Inject internal interface GetKeyChangesTask : Task { @@ -31,11 +32,13 @@ internal interface GetKeyChangesTask : Task { ) } -internal class DefaultSendToDeviceTask @Inject constructor(private val cryptoApi: CryptoApi) - : SendToDeviceTask { +internal class DefaultSendToDeviceTask @Inject constructor( + private val cryptoApi: CryptoApi, + private val eventBus: EventBus +) : SendToDeviceTask { override suspend fun execute(params: SendToDeviceTask.Params) { val sendToDeviceBody = SendToDeviceBody() sendToDeviceBody.messages = params.contentMap.map - return executeRequest { + return executeRequest(eventBus) { apiCall = cryptoApi.sendToDevice( params.eventType, params.transactionId ?: Random.nextInt(Integer.MAX_VALUE).toString(), diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SetDeviceNameTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SetDeviceNameTask.kt index 47f3050b88..74757c5cb3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SetDeviceNameTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SetDeviceNameTask.kt @@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.crypto.api.CryptoApi import im.vector.matrix.android.internal.crypto.model.rest.UpdateDeviceInfoBody import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus import javax.inject.Inject internal interface SetDeviceNameTask : Task { @@ -31,14 +32,16 @@ internal interface SetDeviceNameTask : Task { ) } -internal class DefaultSetDeviceNameTask @Inject constructor(private val cryptoApi: CryptoApi) - : SetDeviceNameTask { +internal class DefaultSetDeviceNameTask @Inject constructor( + private val cryptoApi: CryptoApi, + private val eventBus: EventBus +) : SetDeviceNameTask { override suspend fun execute(params: SetDeviceNameTask.Params) { val body = UpdateDeviceInfoBody( displayName = params.deviceName ) - return executeRequest { + return executeRequest(eventBus) { apiCall = cryptoApi.updateDeviceInfo(params.deviceId, body) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt index db05f473b1..d8bfe73eda 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt @@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.convertToUTF8 +import org.greenrobot.eventbus.EventBus import javax.inject.Inject internal interface UploadKeysTask : Task { @@ -36,8 +37,10 @@ internal interface UploadKeysTask : Task(private val realmConfiguration: RealmConfiguration, - private val realmQueryBuilder: (Realm) -> RealmQuery) { +internal suspend fun awaitNotEmptyResult(realmConfiguration: RealmConfiguration, + timeoutMillis: Long, + builder: (Realm) -> RealmQuery) { + withTimeout(timeoutMillis) { + // Confine Realm interaction to a single thread with Looper. + withContext(Dispatchers.Main) { + val latch = CompletableDeferred() - private companion object { - val QUERY_LATCH_HANDLER = createBackgroundHandler("REALM_QUERY_LATCH") - } + Realm.getInstance(realmConfiguration).use { realm -> + val result = builder(realm).findAllAsync() - @Throws(InterruptedException::class) - fun await(timeout: Long, timeUnit: TimeUnit) { - val realmRef = AtomicReference() - val latch = CountDownLatch(1) - QUERY_LATCH_HANDLER.post { - val realm = Realm.getInstance(realmConfiguration) - realmRef.set(realm) - val result = realmQueryBuilder(realm).findAllAsync() - result.addChangeListener(object : RealmChangeListener> { - override fun onChange(t: RealmResults) { - if (t.isNotEmpty()) { - result.removeChangeListener(this) - latch.countDown() + val listener = object : RealmChangeListener> { + override fun onChange(it: RealmResults) { + if (it.isNotEmpty()) { + result.removeChangeListener(this) + latch.complete(Unit) + } } } - }) - } - try { - latch.await(timeout, timeUnit) - } catch (exception: InterruptedException) { - throw exception - } finally { - QUERY_LATCH_HANDLER.post { - realmRef.getAndSet(null).close() + + result.addChangeListener(listener) + try { + latch.await() + } catch (e: CancellationException) { + result.removeChangeListener(listener) + throw e + } } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt index bc806a56a4..ddc7f5e8e6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.database import android.content.Context import im.vector.matrix.android.internal.database.model.SessionRealmModule +import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.di.UserCacheDirectory import im.vector.matrix.android.internal.di.UserMd5 import im.vector.matrix.android.internal.session.SessionModule @@ -37,13 +38,14 @@ private const val REALM_NAME = "disk_store.realm" */ internal class SessionRealmConfigurationFactory @Inject constructor(private val realmKeysUtils: RealmKeysUtils, @UserCacheDirectory val directory: File, + @SessionId val sessionId: String, @UserMd5 val userMd5: String, context: Context) { private val sharedPreferences = context.getSharedPreferences("im.vector.matrix.android.realm", Context.MODE_PRIVATE) fun create(): RealmConfiguration { - val shouldClearRealm = sharedPreferences.getBoolean("$REALM_SHOULD_CLEAR_FLAG_$userMd5", false) + val shouldClearRealm = sharedPreferences.getBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", false) if (shouldClearRealm) { Timber.v("************************************************************") Timber.v("The realm file session was corrupted and couldn't be loaded.") @@ -53,14 +55,15 @@ internal class SessionRealmConfigurationFactory @Inject constructor(private val } sharedPreferences .edit() - .putBoolean("$REALM_SHOULD_CLEAR_FLAG_$userMd5", true) + .putBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", true) .apply() val realmConfiguration = RealmConfiguration.Builder() + .compactOnLaunch() .directory(directory) .name(REALM_NAME) .apply { - realmKeysUtils.configureEncryption(this, "${SessionModule.DB_ALIAS_PREFIX}$userMd5") + realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5)) } .modules(SessionRealmModule()) .deleteRealmIfMigrationNeeded() @@ -71,7 +74,7 @@ internal class SessionRealmConfigurationFactory @Inject constructor(private val Timber.v("Successfully create realm instance") sharedPreferences .edit() - .putBoolean("$REALM_SHOULD_CLEAR_FLAG_$userMd5", false) + .putBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", false) .apply() } return realmConfiguration diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt index f05fa01444..3fa355fe3c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt @@ -28,15 +28,7 @@ import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.extensions.assertIsManaged import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection import io.realm.Sort - -// By default if a chunk is empty we consider it unlinked -internal fun ChunkEntity.isUnlinked(): Boolean { - assertIsManaged() - return timelineEvents.where() - .equalTo(TimelineEventEntityFields.ROOT.IS_UNLINKED, false) - .findAll() - .isEmpty() -} +import io.realm.kotlin.createObject internal fun ChunkEntity.deleteOnCascade() { assertIsManaged() @@ -46,11 +38,10 @@ internal fun ChunkEntity.deleteOnCascade() { internal fun ChunkEntity.merge(roomId: String, chunkToMerge: ChunkEntity, - direction: PaginationDirection) { + direction: PaginationDirection): List { assertIsManaged() - val isChunkToMergeUnlinked = chunkToMerge.isUnlinked() - val isCurrentChunkUnlinked = this.isUnlinked() - val isUnlinked = isCurrentChunkUnlinked && isChunkToMergeUnlinked + val isChunkToMergeUnlinked = chunkToMerge.isUnlinked + val isCurrentChunkUnlinked = isUnlinked if (isCurrentChunkUnlinked && !isChunkToMergeUnlinked) { this.timelineEvents.forEach { it.root?.isUnlinked = false } @@ -65,49 +56,21 @@ internal fun ChunkEntity.merge(roomId: String, this.isLastBackward = chunkToMerge.isLastBackward eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING) } - val events = eventsToMerge.mapNotNull { it.root?.asDomain() } - val eventIds = ArrayList() - events.forEach { event -> - add(roomId, event, direction, isUnlinked = isUnlinked) - if (event.eventId != null) { - eventIds.add(event.eventId) - } - } - updateSenderDataFor(eventIds) -} - -internal fun ChunkEntity.addAll(roomId: String, - events: List, - direction: PaginationDirection, - stateIndexOffset: Int = 0, - // Set to true for Event retrieved from a Permalink (i.e. not linked to live Chunk) - isUnlinked: Boolean = false) { - assertIsManaged() - val eventIds = ArrayList() - events.forEach { event -> - add(roomId, event, direction, stateIndexOffset, isUnlinked) - if (event.eventId != null) { - eventIds.add(event.eventId) - } - } - updateSenderDataFor(eventIds) -} - -internal fun ChunkEntity.updateSenderDataFor(eventIds: List) { - for (eventId in eventIds) { - val timelineEventEntity = timelineEvents.find(eventId) ?: continue - timelineEventEntity.updateSenderData() - } + return eventsToMerge + .mapNotNull { + val event = it.root?.asDomain() ?: return@mapNotNull null + add(roomId, event, direction) + } } internal fun ChunkEntity.add(roomId: String, event: Event, direction: PaginationDirection, - stateIndexOffset: Int = 0, - isUnlinked: Boolean = false) { + stateIndexOffset: Int = 0 +): TimelineEventEntity? { assertIsManaged() if (event.eventId != null && timelineEvents.find(event.eventId) != null) { - return + return null } var currentDisplayIndex = lastDisplayIndex(direction, 0) if (direction == PaginationDirection.FORWARDS) { @@ -129,12 +92,15 @@ internal fun ChunkEntity.add(roomId: String, } } + val isChunkUnlinked = isUnlinked val localId = TimelineEventEntity.nextId(realm) val eventId = event.eventId ?: "" val senderId = event.senderId ?: "" val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst() - ?: ReadReceiptsSummaryEntity(eventId, roomId) + ?: realm.createObject(eventId).apply { + this.roomId = roomId + } // Update RR for the sender of a new message with a dummy one @@ -151,13 +117,15 @@ internal fun ChunkEntity.add(roomId: String, } } - val eventEntity = TimelineEventEntity(localId).also { - it.root = event.toEntity(roomId).apply { - this.stateIndex = currentStateIndex - this.isUnlinked = isUnlinked - this.displayIndex = currentDisplayIndex - this.sendState = SendState.SYNCED - } + val rootEvent = event.toEntity(roomId).apply { + this.stateIndex = currentStateIndex + this.displayIndex = currentDisplayIndex + this.sendState = SendState.SYNCED + this.isUnlinked = isChunkUnlinked + } + val eventEntity = realm.createObject().also { + it.localId = localId + it.root = realm.copyToRealm(rootEvent) it.eventId = eventId it.roomId = roomId it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() @@ -165,6 +133,7 @@ internal fun ChunkEntity.add(roomId: String, } val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size timelineEvents.add(position, eventEntity) + return eventEntity } internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt index 948af2af96..19c4715faa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt @@ -60,7 +60,7 @@ internal fun RoomEntity.addSendingEvent(event: Event) { this.sendState = SendState.UNSENT } val roomMembers = RoomMembers(realm, roomId) - val myUser = roomMembers.get(senderId) + val myUser = roomMembers.getLastRoomMember(senderId) val localId = TimelineEventEntity.nextId(realm) val timelineEventEntity = TimelineEventEntity(localId).also { it.root = eventEntity @@ -69,7 +69,6 @@ internal fun RoomEntity.addSendingEvent(event: Event) { it.senderName = myUser?.displayName it.senderAvatar = myUser?.avatarUrl it.isUniqueDisplayName = roomMembers.isUniqueDisplayName(myUser?.displayName) - it.senderMembershipEvent = roomMembers.queryRoomMemberEvent(senderId).findFirst() } sendingTimelineEvents.add(0, timelineEventEntity) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt index 36ed2f7edf..0bf02aa92f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt @@ -16,74 +16,9 @@ package im.vector.matrix.android.internal.database.helper -import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.model.RoomMember -import im.vector.matrix.android.internal.database.mapper.ContentMapper -import im.vector.matrix.android.internal.database.model.* -import im.vector.matrix.android.internal.database.query.next -import im.vector.matrix.android.internal.database.query.prev -import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.extensions.assertIsManaged -import im.vector.matrix.android.internal.session.room.membership.RoomMembers +import im.vector.matrix.android.internal.database.model.TimelineEventEntity +import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields import io.realm.Realm -import io.realm.RealmList -import io.realm.RealmQuery - -internal fun TimelineEventEntity.updateSenderData() { - assertIsManaged() - val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return - val stateIndex = root?.stateIndex ?: return - val senderId = root?.sender ?: return - val chunkEntity = chunk?.firstOrNull() ?: return - val isUnlinked = chunkEntity.isUnlinked() - var senderMembershipEvent: EventEntity? - var senderRoomMemberContent: String? - var senderRoomMemberPrevContent: String? - when { - stateIndex <= 0 -> { - senderMembershipEvent = chunkEntity.timelineEvents.buildQuery(senderId, isUnlinked).next(from = stateIndex)?.root - senderRoomMemberContent = senderMembershipEvent?.prevContent - senderRoomMemberPrevContent = senderMembershipEvent?.content - } - else -> { - senderMembershipEvent = chunkEntity.timelineEvents.buildQuery(senderId, isUnlinked).prev(since = stateIndex)?.root - senderRoomMemberContent = senderMembershipEvent?.content - senderRoomMemberPrevContent = senderMembershipEvent?.prevContent - } - } - - // We fallback to untimelinedStateEvents if we can't find membership events in timeline - if (senderMembershipEvent == null) { - senderMembershipEvent = roomEntity.untimelinedStateEvents - .where() - .equalTo(EventEntityFields.STATE_KEY, senderId) - .equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_MEMBER) - .prev(since = stateIndex) - senderRoomMemberContent = senderMembershipEvent?.content - senderRoomMemberPrevContent = senderMembershipEvent?.prevContent - } - - ContentMapper.map(senderRoomMemberContent).toModel()?.also { - this.senderAvatar = it.avatarUrl - this.senderName = it.displayName - this.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName) - } - - // We try to fallback on prev content if we got a room member state events with null fields - if (root?.type == EventType.STATE_ROOM_MEMBER) { - ContentMapper.map(senderRoomMemberPrevContent).toModel()?.also { - if (this.senderAvatar == null && it.avatarUrl != null) { - this.senderAvatar = it.avatarUrl - } - if (this.senderName == null && it.displayName != null) { - this.senderName = it.displayName - this.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName) - } - } - } - this.senderMembershipEvent = senderMembershipEvent -} internal fun TimelineEventEntity.Companion.nextId(realm: Realm): Long { val currentIdNum = realm.where(TimelineEventEntity::class.java).max(TimelineEventEntityFields.LOCAL_ID) @@ -93,10 +28,3 @@ internal fun TimelineEventEntity.Companion.nextId(realm: Realm): Long { currentIdNum.toLong() + 1 } } - -private fun RealmList.buildQuery(sender: String, isUnlinked: Boolean): RealmQuery { - return where() - .equalTo(TimelineEventEntityFields.ROOT.STATE_KEY, sender) - .equalTo(TimelineEventEntityFields.ROOT.TYPE, EventType.STATE_ROOM_MEMBER) - .equalTo(TimelineEventEntityFields.ROOT.IS_UNLINKED, isUnlinked) -} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventSenderVisitor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventSenderVisitor.kt new file mode 100644 index 0000000000..983de3a50f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventSenderVisitor.kt @@ -0,0 +1,148 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.database.helper + +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.RoomMemberContent +import im.vector.matrix.android.internal.database.mapper.ContentMapper +import im.vector.matrix.android.internal.database.model.* +import im.vector.matrix.android.internal.database.query.next +import im.vector.matrix.android.internal.database.query.prev +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.extensions.assertIsManaged +import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.session.room.membership.RoomMembers +import io.realm.RealmList +import io.realm.RealmQuery +import javax.inject.Inject + +/** + * This is an internal cache to avoid querying all the time the room member events + */ +@SessionScope +internal class TimelineEventSenderVisitor @Inject constructor() { + + internal data class Key( + val roomId: String, + val stateIndex: Int, + val senderId: String + ) + + internal class Value( + var senderAvatar: String? = null, + var senderName: String? = null, + var isUniqueDisplayName: Boolean = false, + var senderMembershipEventId: String? = null + ) + + private val values = HashMap() + + fun clear() { + values.clear() + } + + fun clear(roomId: String, senderId: String) { + val keysToRemove = values.keys.filter { it.senderId == senderId && it.roomId == roomId } + keysToRemove.forEach { + values.remove(it) + } + } + + fun visit(timelineEventEntities: List) = timelineEventEntities.forEach { visit(it) } + + fun visit(timelineEventEntity: TimelineEventEntity) { + if (!timelineEventEntity.isValid) { + return + } + val key = Key( + roomId = timelineEventEntity.roomId, + stateIndex = timelineEventEntity.root?.stateIndex ?: 0, + senderId = timelineEventEntity.root?.sender ?: "" + ) + val result = values.getOrPut(key) { + timelineEventEntity.computeValue() + } + timelineEventEntity.apply { + this.isUniqueDisplayName = result.isUniqueDisplayName + this.senderAvatar = result.senderAvatar + this.senderName = result.senderName + this.senderMembershipEventId = result.senderMembershipEventId + } + } + + private fun RealmList.buildQuery(sender: String, isUnlinked: Boolean): RealmQuery { + return where() + .equalTo(TimelineEventEntityFields.ROOT.STATE_KEY, sender) + .equalTo(TimelineEventEntityFields.ROOT.TYPE, EventType.STATE_ROOM_MEMBER) + .equalTo(TimelineEventEntityFields.ROOT.IS_UNLINKED, isUnlinked) + } + + private fun TimelineEventEntity.computeValue(): Value { + assertIsManaged() + val result = Value() + val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return result + val stateIndex = root?.stateIndex ?: return result + val senderId = root?.sender ?: return result + val chunkEntity = chunk?.firstOrNull() ?: return result + val isUnlinked = chunkEntity.isUnlinked + var senderMembershipEvent: EventEntity? + var senderRoomMemberContent: String? + var senderRoomMemberPrevContent: String? + + if (stateIndex <= 0) { + senderMembershipEvent = chunkEntity.timelineEvents.buildQuery(senderId, isUnlinked).next(from = stateIndex)?.root + senderRoomMemberContent = senderMembershipEvent?.prevContent + senderRoomMemberPrevContent = senderMembershipEvent?.content + } else { + senderMembershipEvent = chunkEntity.timelineEvents.buildQuery(senderId, isUnlinked).prev(since = stateIndex)?.root + senderRoomMemberContent = senderMembershipEvent?.content + senderRoomMemberPrevContent = senderMembershipEvent?.prevContent + } + + // We fallback to untimelinedStateEvents if we can't find membership events in timeline + if (senderMembershipEvent == null) { + senderMembershipEvent = roomEntity.untimelinedStateEvents + .where() + .equalTo(EventEntityFields.STATE_KEY, senderId) + .equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_MEMBER) + .prev(since = stateIndex) + senderRoomMemberContent = senderMembershipEvent?.content + senderRoomMemberPrevContent = senderMembershipEvent?.prevContent + } + + ContentMapper.map(senderRoomMemberContent).toModel()?.also { + result.senderAvatar = it.avatarUrl + result.senderName = it.displayName + result.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName) + } + // We try to fallback on prev content if we got a room member state events with null fields + if (root?.type == EventType.STATE_ROOM_MEMBER) { + ContentMapper.map(senderRoomMemberPrevContent).toModel()?.also { + if (result.senderAvatar == null && it.avatarUrl != null) { + result.senderAvatar = it.avatarUrl + } + if (result.senderName == null && it.displayName != null) { + result.senderName = it.displayName + result.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName) + } + } + } + result.senderMembershipEventId = senderMembershipEvent?.eventId + return result + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomMemberMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomMemberMapper.kt new file mode 100644 index 0000000000..a458c5e506 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomMemberMapper.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.database.mapper + +import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.internal.database.model.RoomMemberEntity + +internal object RoomMemberMapper { + + fun map(roomMemberEntity: RoomMemberEntity): RoomMember { + return RoomMember( + userId = roomMemberEntity.userId, + avatarUrl = roomMemberEntity.avatarUrl, + displayName = roomMemberEntity.displayName, + membership = roomMemberEntity.membership + ) + } +} + +internal fun RoomMemberEntity.asDomain(): RoomMember { + return RoomMemberMapper.map(this) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt index eeb340eacb..7d25a846ff 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt @@ -70,7 +70,8 @@ internal class RoomSummaryMapper @Inject constructor( readMarkerId = roomSummaryEntity.readMarkerId, userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList(), canonicalAlias = roomSummaryEntity.canonicalAlias, - aliases = roomSummaryEntity.aliases.toList() + aliases = roomSummaryEntity.aliases.toList(), + isEncrypted = roomSummaryEntity.isEncrypted ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt index 577c391b3a..94d4a9043f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt @@ -30,7 +30,8 @@ internal open class ChunkEntity(@Index var prevToken: String? = null, var backwardsDisplayIndex: Int? = null, var forwardsDisplayIndex: Int? = null, var backwardsStateIndex: Int? = null, - var forwardsStateIndex: Int? = null + var forwardsStateIndex: Int? = null, + var isUnlinked: Boolean = false ) : RealmObject() { fun identifier() = "${prevToken}_$nextToken" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomMemberEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomMemberEntity.kt new file mode 100644 index 0000000000..c532857fe1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomMemberEntity.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.database.model + +import im.vector.matrix.android.api.session.room.model.Membership +import io.realm.RealmObject +import io.realm.annotations.Index +import io.realm.annotations.PrimaryKey + +internal open class RoomMemberEntity(@PrimaryKey var primaryKey: String = "", + @Index var userId: String = "", + @Index var roomId: String = "", + var displayName: String = "", + var avatarUrl: String = "", + var reason: String? = null, + var isDirect: Boolean = false +) : RealmObject() { + + private var membershipStr: String = Membership.NONE.name + var membership: Membership + get() { + return Membership.valueOf(membershipStr) + } + set(value) { + membershipStr = value.name + } + + companion object +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt index 406c8700b6..4c99832b39 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt @@ -42,7 +42,9 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "", var breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS, var canonicalAlias: String? = null, var aliases: RealmList = RealmList(), - var flatAliases: String = "" + // this is required for querying + var flatAliases: String = "", + var isEncrypted: Boolean = false ) : RealmObject() { private var membershipStr: String = Membership.NONE.name diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt index 4a93819027..752f2a6c23 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt @@ -50,6 +50,7 @@ import io.realm.annotations.RealmModule ReadMarkerEntity::class, UserDraftsEntity::class, DraftEntity::class, - HomeServerCapabilitiesEntity::class + HomeServerCapabilitiesEntity::class, + RoomMemberEntity::class ]) internal class SessionRealmModule diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt index 235910b1ea..22f4b9c506 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt @@ -29,7 +29,7 @@ internal open class TimelineEventEntity(var localId: Long = 0, var senderName: String? = null, var isUniqueDisplayName: Boolean = false, var senderAvatar: String? = null, - var senderMembershipEvent: EventEntity? = null, + var senderMembershipEventId: String? = null, var readReceipts: ReadReceiptsSummaryEntity? = null ) : RealmObject() { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt index 69402ac1de..b8c058e667 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt @@ -57,9 +57,15 @@ internal fun ChunkEntity.Companion.findIncludingEvent(realm: Realm, eventId: Str return findAllIncludingEvents(realm, listOf(eventId)).firstOrNull() } -internal fun ChunkEntity.Companion.create(realm: Realm, prevToken: String?, nextToken: String?): ChunkEntity { +internal fun ChunkEntity.Companion.create( + realm: Realm, + prevToken: String?, + nextToken: String?, + isUnlinked: Boolean +): ChunkEntity { return realm.createObject().apply { this.prevToken = prevToken this.nextToken = nextToken + this.isUnlinked = isUnlinked } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomMemberEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomMemberEntityQueries.kt new file mode 100644 index 0000000000..2ddade0048 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomMemberEntityQueries.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.database.query + +import im.vector.matrix.android.internal.database.model.RoomMemberEntity +import im.vector.matrix.android.internal.database.model.RoomMemberEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun RoomMemberEntity.Companion.where(realm: Realm, roomId: String, userId: String? = null): RealmQuery { + val query = realm + .where() + .equalTo(RoomMemberEntityFields.ROOM_ID, roomId) + + if (userId != null) { + query.equalTo(RoomMemberEntityFields.USER_ID, userId) + } + return query +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt index 3bd035c0b1..221e8ccb46 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt @@ -54,7 +54,7 @@ internal fun TimelineEventEntity.Companion.where(realm: Realm, internal fun TimelineEventEntity.Companion.findWithSenderMembershipEvent(realm: Realm, senderMembershipEventId: String): List { return realm.where() - .equalTo(TimelineEventEntityFields.SENDER_MEMBERSHIP_EVENT.EVENT_ID, senderMembershipEventId) + .equalTo(TimelineEventEntityFields.SENDER_MEMBERSHIP_EVENT_ID, senderMembershipEventId) .findAll() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt index 3444a8fa70..b084ba012d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt @@ -38,3 +38,10 @@ internal annotation class DeviceId @Qualifier @Retention(AnnotationRetention.RUNTIME) internal annotation class UserMd5 + +/** + * Used to inject the sessionId, which is defined as md5(userId|deviceId) + */ +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class SessionId diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/AccessTokenInterceptor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/AccessTokenInterceptor.kt index e0257bfc83..c802d4b63a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/AccessTokenInterceptor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/AccessTokenInterceptor.kt @@ -17,13 +17,13 @@ package im.vector.matrix.android.internal.network import im.vector.matrix.android.internal.auth.SessionParamsStore -import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.di.SessionId import okhttp3.Interceptor import okhttp3.Response import javax.inject.Inject internal class AccessTokenInterceptor @Inject constructor( - @UserId private val userId: String, + @SessionId private val sessionId: String, private val sessionParamsStore: SessionParamsStore) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { @@ -40,5 +40,5 @@ internal class AccessTokenInterceptor @Inject constructor( } private val accessToken - get() = sessionParamsStore.get(userId)?.credentials?.accessToken + get() = sessionParamsStore.get(sessionId)?.credentials?.accessToken } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt index 7f8e6643c3..074a97662b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt @@ -18,12 +18,14 @@ package im.vector.matrix.android.internal.network import im.vector.matrix.android.api.failure.Failure import kotlinx.coroutines.CancellationException +import org.greenrobot.eventbus.EventBus import retrofit2.Call import java.io.IOException -internal suspend inline fun executeRequest(block: Request.() -> Unit) = Request().apply(block).execute() +internal suspend inline fun executeRequest(eventBus: EventBus?, + block: Request.() -> Unit) = Request(eventBus).apply(block).execute() -internal class Request { +internal class Request(private val eventBus: EventBus?) { lateinit var apiCall: Call @@ -34,7 +36,7 @@ internal class Request { response.body() ?: throw IllegalStateException("The request returned a null body") } else { - throw response.toFailure() + throw response.toFailure(eventBus) } } catch (exception: Throwable) { throw when (exception) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt index e95c161491..2a2076db6c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt @@ -73,18 +73,18 @@ internal suspend fun okhttp3.Call.awaitResponse(): okhttp3.Response { /** * Convert a retrofit Response to a Failure, and eventually parse errorBody to convert it to a MatrixError */ -internal fun Response.toFailure(): Failure { - return toFailure(errorBody(), code()) +internal fun Response.toFailure(eventBus: EventBus?): Failure { + return toFailure(errorBody(), code(), eventBus) } /** * Convert a okhttp3 Response to a Failure, and eventually parse errorBody to convert it to a MatrixError */ -internal fun okhttp3.Response.toFailure(): Failure { - return toFailure(body, code) +internal fun okhttp3.Response.toFailure(eventBus: EventBus?): Failure { + return toFailure(body, code, eventBus) } -private fun toFailure(errorBody: ResponseBody?, httpCode: Int): Failure { +private fun toFailure(errorBody: ResponseBody?, httpCode: Int, eventBus: EventBus?): Failure { if (errorBody == null) { return Failure.Unknown(RuntimeException("errorBody should not be null")) } @@ -99,11 +99,11 @@ private fun toFailure(errorBody: ResponseBody?, httpCode: Int): Failure { if (matrixError != null) { if (matrixError.code == MatrixError.M_CONSENT_NOT_GIVEN && !matrixError.consentUri.isNullOrBlank()) { // Also send this error to the bus, for a global management - EventBus.getDefault().post(GlobalError.ConsentNotGivenError(matrixError.consentUri)) + eventBus?.post(GlobalError.ConsentNotGivenError(matrixError.consentUri)) } else if (httpCode == HttpURLConnection.HTTP_UNAUTHORIZED /* 401 */ && matrixError.code == MatrixError.M_UNKNOWN_TOKEN) { // Also send this error to the bus, for a global management - EventBus.getDefault().post(GlobalError.InvalidToken(matrixError.isSoftLogout)) + eventBus?.post(GlobalError.InvalidToken(matrixError.isSoftLogout)) } return Failure.ServerError(matrixError, httpCode) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryEnumListProcessor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryEnumListProcessor.kt new file mode 100644 index 0000000000..2bc05eacec --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryEnumListProcessor.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.query + +import io.realm.RealmObject +import io.realm.RealmQuery + +fun > RealmQuery.process(field: String, enums: List>): RealmQuery { + val lastEnumValue = enums.lastOrNull() + beginGroup() + for (enumValue in enums) { + equalTo(field, enumValue.name) + if (enumValue != lastEnumValue) { + or() + } + } + endGroup() + return this +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryStringValueProcessor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryStringValueProcessor.kt new file mode 100644 index 0000000000..ebe10cad9c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryStringValueProcessor.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.query + +import im.vector.matrix.android.api.query.QueryStringValue +import io.realm.Case +import io.realm.RealmObject +import io.realm.RealmQuery +import timber.log.Timber + +fun RealmQuery.process(field: String, queryStringValue: QueryStringValue): RealmQuery { + when (queryStringValue) { + is QueryStringValue.NoCondition -> Timber.v("No condition to process") + is QueryStringValue.IsNotNull -> isNotNull(field) + is QueryStringValue.IsNull -> isNull(field) + is QueryStringValue.IsEmpty -> isEmpty(field) + is QueryStringValue.IsNotEmpty -> isNotEmpty(field) + is QueryStringValue.Equals -> equalTo(field, queryStringValue.string, queryStringValue.case.toRealmCase()) + is QueryStringValue.Contains -> contains(field, queryStringValue.string, queryStringValue.case.toRealmCase()) + } + return this +} + +private fun QueryStringValue.Case.toRealmCase(): Case { + return when (this) { + QueryStringValue.Case.INSENSITIVE -> Case.INSENSITIVE + QueryStringValue.Case.SENSITIVE -> Case.SENSITIVE + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt index c160ac9b31..66b94cf68d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt @@ -25,7 +25,7 @@ import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments -import im.vector.matrix.android.internal.di.UserMd5 +import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.md5 @@ -42,7 +42,7 @@ import java.io.IOException import javax.inject.Inject internal class DefaultFileService @Inject constructor(private val context: Context, - @UserMd5 private val userMd5: String, + @SessionId private val sessionId: String, private val contentUrlResolver: ContentUrlResolver, private val coroutineDispatchers: MatrixCoroutineDispatchers) : FileService { @@ -103,9 +103,9 @@ internal class DefaultFileService @Inject constructor(private val context: Conte return when (downloadMode) { FileService.DownloadMode.FOR_INTERNAL_USE -> { // Create dir tree (MF stands for Matrix File): - // /MF/// + // /MF/// val tmpFolderRoot = File(context.cacheDir, "MF") - val tmpFolderUser = File(tmpFolderRoot, userMd5) + val tmpFolderUser = File(tmpFolderRoot, sessionId) File(tmpFolderUser, id.md5()) } FileService.DownloadMode.TO_EXPORT -> { 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 d52379eb6e..dc1a20802b 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 @@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.session import android.content.Context -import android.os.Looper import androidx.annotation.MainThread import androidx.lifecycle.LiveData import dagger.Lazy @@ -45,6 +44,7 @@ import im.vector.matrix.android.api.session.user.UserService import im.vector.matrix.android.internal.auth.SessionParamsStore import im.vector.matrix.android.internal.crypto.DefaultCryptoService import im.vector.matrix.android.internal.database.LiveEntityObserver +import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.session.sync.SyncTaskSequencer import im.vector.matrix.android.internal.session.sync.SyncTokenStore import im.vector.matrix.android.internal.session.sync.job.SyncThread @@ -60,67 +60,70 @@ import javax.inject.Inject import javax.inject.Provider @SessionScope -internal class DefaultSession @Inject constructor(override val sessionParams: SessionParams, - private val context: Context, - private val liveEntityObservers: Set<@JvmSuppressWildcards LiveEntityObserver>, - private val sessionListeners: SessionListeners, - private val roomService: Lazy, - private val roomDirectoryService: Lazy, - private val groupService: Lazy, - private val userService: Lazy, - private val filterService: Lazy, - private val cacheService: Lazy, - private val signOutService: Lazy, - private val pushRuleService: Lazy, - private val pushersService: Lazy, - private val cryptoService: Lazy, - private val fileService: Lazy, - private val secureStorageService: Lazy, - 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) +internal class DefaultSession @Inject constructor( + override val sessionParams: SessionParams, + private val context: Context, + private val eventBus: EventBus, + @SessionId + override val sessionId: String, + private val liveEntityObservers: Set<@JvmSuppressWildcards LiveEntityObserver>, + private val sessionListeners: SessionListeners, + private val roomService: Lazy, + private val roomDirectoryService: Lazy, + private val groupService: Lazy, + private val userService: Lazy, + private val filterService: Lazy, + private val cacheService: Lazy, + private val signOutService: Lazy, + private val pushRuleService: Lazy, + private val pushersService: Lazy, + private val cryptoService: Lazy, + private val fileService: Lazy, + private val secureStorageService: Lazy, + 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) : 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(), - PushersService by pushersService.get(), - FileService by fileService.get(), - InitialSyncProgressService by initialSyncProgressService.get(), - SecureStorageService by secureStorageService.get(), - HomeServerCapabilitiesService by homeServerCapabilitiesService.get() { + 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(), + PushersService by pushersService.get(), + FileService by fileService.get(), + InitialSyncProgressService by initialSyncProgressService.get(), + SecureStorageService by secureStorageService.get(), + HomeServerCapabilitiesService by homeServerCapabilitiesService.get() { private var isOpen = false private var syncThread: SyncThread? = null override val isOpenable: Boolean - get() = sessionParamsStore.get(myUserId)?.isTokenValid ?: false + get() = sessionParamsStore.get(sessionId)?.isTokenValid ?: false @MainThread override fun open() { - assertMainThread() assert(!isOpen) isOpen = true liveEntityObservers.forEach { it.start() } - EventBus.getDefault().register(this) + eventBus.register(this) } override fun requireBackgroundSync() { - SyncWorker.requireBackgroundSync(context, myUserId) + SyncWorker.requireBackgroundSync(context, sessionId) } override fun startAutomaticBackgroundSync(repeatDelay: Long) { - SyncWorker.automaticallyBackgroundSync(context, myUserId, 0, repeatDelay) + SyncWorker.automaticallyBackgroundSync(context, sessionId, 0, repeatDelay) } override fun stopAnyBackgroundSync() { @@ -152,11 +155,11 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se liveEntityObservers.forEach { it.dispose() } cryptoService.get().close() isOpen = false - EventBus.getDefault().unregister(this) + eventBus.unregister(this) syncTaskSequencer.close() } - override fun syncState(): LiveData { + override fun getSyncStateLive(): LiveData { return getSyncThread().liveState() } @@ -180,10 +183,10 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se @Subscribe(threadMode = ThreadMode.MAIN) fun onGlobalError(globalError: GlobalError) { if (globalError is GlobalError.InvalidToken - && globalError.softLogout) { + && globalError.softLogout) { // Mark the token has invalid GlobalScope.launch(Dispatchers.IO) { - sessionParamsStore.setTokenInvalid(myUserId) + sessionParamsStore.setTokenInvalid(sessionId) } } @@ -201,12 +204,4 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se override fun removeListener(listener: Session.Listener) { sessionListeners.removeListener(listener) } - - // Private methods ***************************************************************************** - - private fun assertMainThread() { - if (Looper.getMainLooper().thread !== Thread.currentThread()) { - throw IllegalStateException("This method can only be called on the main thread!") - } - } } 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 6f4136405e..d220ef236c 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 @@ -26,6 +26,7 @@ import dagger.multibindings.IntoSet import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.SessionParams +import im.vector.matrix.android.api.auth.data.sessionId import im.vector.matrix.android.api.session.InitialSyncProgressService import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService @@ -47,6 +48,7 @@ import im.vector.matrix.android.internal.session.securestorage.DefaultSecureStor import im.vector.matrix.android.internal.util.md5 import io.realm.RealmConfiguration import okhttp3.OkHttpClient +import org.greenrobot.eventbus.EventBus import retrofit2.Retrofit import java.io.File @@ -55,8 +57,7 @@ internal abstract class SessionModule { @Module companion object { - - internal const val DB_ALIAS_PREFIX = "session_db_" + internal fun getKeyAlias(userMd5: String) = "session_db_$userMd5" @JvmStatic @Provides @@ -91,11 +92,26 @@ internal abstract class SessionModule { return userId.md5() } + @JvmStatic + @SessionId + @Provides + fun providesSessionId(credentials: Credentials): String { + return credentials.sessionId() + } + @JvmStatic @Provides @UserCacheDirectory - fun providesFilesDir(@UserMd5 userMd5: String, context: Context): File { - return File(context.filesDir, userMd5) + fun providesFilesDir(@UserMd5 userMd5: String, + @SessionId sessionId: String, + context: Context): File { + // Temporary code for migration + val old = File(context.filesDir, userMd5) + if (old.exists()) { + old.renameTo(File(context.filesDir, sessionId)) + } + + return File(context.filesDir, sessionId) } @JvmStatic @@ -147,6 +163,13 @@ internal abstract class SessionModule { return retrofitFactory .create(okHttpClient, sessionParams.homeServerConnectionConfig.homeServerUri.toString()) } + + @JvmStatic + @Provides + @SessionScope + fun providesEventBus(): EventBus { + return EventBus.builder().build() + } } @Binds diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt index 2f4e991e62..4071c9224f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt @@ -29,12 +29,14 @@ import okhttp3.Request import okhttp3.RequestBody import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody +import org.greenrobot.eventbus.EventBus import java.io.File import java.io.IOException import javax.inject.Inject internal class FileUploader @Inject constructor(@Authenticated private val okHttpClient: OkHttpClient, + private val eventBus: EventBus, sessionParams: SessionParams, moshi: Moshi) { @@ -73,7 +75,7 @@ internal class FileUploader @Inject constructor(@Authenticated return okHttpClient.newCall(request).awaitResponse().use { response -> if (!response.isSuccessful) { - throw response.toFailure() + throw response.toFailure(eventBus) } else { response.body?.source()?.let { responseAdapter.fromJson(it) 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 a05edb7b0f..1725ef99aa 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 @@ -42,7 +42,7 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : @JsonClass(generateAdapter = true) internal data class Params( - override val userId: String, + override val sessionId: String, val roomId: String, val event: Event, val attachment: ContentAttachmentData, @@ -64,7 +64,7 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : return Result.success(inputData) } - val sessionComponent = getSessionComponent(params.userId) ?: return Result.success() + val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() sessionComponent.inject(this) val eventId = params.event.eventId ?: return Result.success() @@ -169,7 +169,7 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : 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.userId, params.roomId, event) + val sendParams = SendEventWorker.Params(params.sessionId, params.roomId, event) return Result.success(WorkerParamsFactory.toData(sendParams)) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultSaveFilterTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultSaveFilterTask.kt index 08985bf17d..47c5e4a08a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultSaveFilterTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultSaveFilterTask.kt @@ -20,6 +20,7 @@ import im.vector.matrix.android.api.session.sync.FilterService import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus import javax.inject.Inject /** @@ -32,9 +33,11 @@ internal interface SaveFilterTask : Task { ) } -internal class DefaultSaveFilterTask @Inject constructor(@UserId private val userId: String, - private val filterAPI: FilterApi, - private val filterRepository: FilterRepository +internal class DefaultSaveFilterTask @Inject constructor( + @UserId private val userId: String, + private val filterAPI: FilterApi, + private val filterRepository: FilterRepository, + private val eventBus: EventBus ) : SaveFilterTask { override suspend fun execute(params: SaveFilterTask.Params) { @@ -56,7 +59,7 @@ internal class DefaultSaveFilterTask @Inject constructor(@UserId private val use } val updated = filterRepository.storeFilter(filterBody, roomFilter) if (updated) { - val filterResponse = executeRequest { + val filterResponse = executeRequest(eventBus) { // TODO auto retry apiCall = filterAPI.uploadFilter(userId, filterBody) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGetGroupDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGetGroupDataTask.kt index 40dc0a44c0..069c9b8d21 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGetGroupDataTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGetGroupDataTask.kt @@ -25,6 +25,7 @@ import im.vector.matrix.android.internal.session.group.model.GroupRooms import im.vector.matrix.android.internal.session.group.model.GroupSummaryResponse import im.vector.matrix.android.internal.session.group.model.GroupUsers import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus import javax.inject.Inject internal interface GetGroupDataTask : Task { @@ -34,18 +35,19 @@ internal interface GetGroupDataTask : Task { internal class DefaultGetGroupDataTask @Inject constructor( private val groupAPI: GroupAPI, - private val monarchy: Monarchy + private val monarchy: Monarchy, + private val eventBus: EventBus ) : GetGroupDataTask { override suspend fun execute(params: GetGroupDataTask.Params) { val groupId = params.groupId - val groupSummary = executeRequest { + val groupSummary = executeRequest(eventBus) { apiCall = groupAPI.getSummary(groupId) } - val groupRooms = executeRequest { + val groupRooms = executeRequest(eventBus) { apiCall = groupAPI.getRooms(groupId) } - val groupUsers = executeRequest { + val groupUsers = executeRequest(eventBus) { apiCall = groupAPI.getUsers(groupId) } insertInDb(groupSummary, groupRooms, groupUsers, groupId) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroupService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroupService.kt index 192c6fe40c..baa8f5218d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroupService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroupService.kt @@ -20,12 +20,16 @@ import androidx.lifecycle.LiveData import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.group.Group import im.vector.matrix.android.api.session.group.GroupService +import im.vector.matrix.android.api.session.group.GroupSummaryQueryParams import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.GroupSummaryEntity import im.vector.matrix.android.internal.database.model.GroupSummaryEntityFields import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.query.process import im.vector.matrix.android.internal.util.fetchCopyMap +import io.realm.Realm +import io.realm.RealmQuery import javax.inject.Inject internal class DefaultGroupService @Inject constructor(private val monarchy: Monarchy) : GroupService { @@ -41,10 +45,23 @@ internal class DefaultGroupService @Inject constructor(private val monarchy: Mon ) } - override fun liveGroupSummaries(): LiveData> { - return monarchy.findAllMappedWithChanges( - { realm -> GroupSummaryEntity.where(realm).isNotEmpty(GroupSummaryEntityFields.DISPLAY_NAME) }, + override fun getGroupSummaries(groupSummaryQueryParams: GroupSummaryQueryParams): List { + return monarchy.fetchAllMappedSync( + { groupSummariesQuery(it, groupSummaryQueryParams) }, { it.asDomain() } ) } + + override fun getGroupSummariesLive(groupSummaryQueryParams: GroupSummaryQueryParams): LiveData> { + return monarchy.findAllMappedWithChanges( + { groupSummariesQuery(it, groupSummaryQueryParams) }, + { it.asDomain() } + ) + } + + private fun groupSummariesQuery(realm: Realm, queryParams: GroupSummaryQueryParams): RealmQuery { + return GroupSummaryEntity.where(realm) + .process(GroupSummaryEntityFields.DISPLAY_NAME, queryParams.displayName) + .process(GroupSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships) + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GetGroupDataWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GetGroupDataWorker.kt index 76a7d5a48d..93705774e6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GetGroupDataWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GetGroupDataWorker.kt @@ -29,7 +29,7 @@ internal class GetGroupDataWorker(context: Context, params: WorkerParameters) : @JsonClass(generateAdapter = true) internal data class Params( - override val userId: String, + override val sessionId: String, val groupIds: List, override val lastFailureMessage: String? = null ) : SessionWorkerParams @@ -40,7 +40,7 @@ internal class GetGroupDataWorker(context: Context, params: WorkerParameters) : val params = WorkerParamsFactory.fromData(inputData) ?: return Result.failure() - val sessionComponent = getSessionComponent(params.userId) ?: return Result.success() + val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() sessionComponent.inject(this) val results = params.groupIds.map { groupId -> runCatching { fetchGroupData(groupId) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt index 553a0387c5..a60bc78b6c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt @@ -26,7 +26,7 @@ import im.vector.matrix.android.internal.database.awaitTransaction import im.vector.matrix.android.internal.database.model.GroupEntity import im.vector.matrix.android.internal.database.model.GroupSummaryEntity import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.worker.WorkManagerUtil import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder import im.vector.matrix.android.internal.worker.WorkerParamsFactory @@ -37,9 +37,10 @@ import javax.inject.Inject private const val GET_GROUP_DATA_WORKER = "GET_GROUP_DATA_WORKER" -internal class GroupSummaryUpdater @Inject constructor(private val context: Context, - @UserId private val userId: String, - private val monarchy: Monarchy) +internal class GroupSummaryUpdater @Inject constructor( + private val context: Context, + @SessionId private val sessionId: String, + private val monarchy: Monarchy) : RealmLiveEntityObserver(monarchy.realmConfiguration) { override val query = Monarchy.Query { GroupEntity.where(it) } @@ -51,9 +52,9 @@ internal class GroupSummaryUpdater @Inject constructor(private val context: Cont .mapNotNull { results[it] } fetchGroupsData(modifiedGroupEntity - .filter { it.membership == Membership.JOIN || it.membership == Membership.INVITE } - .map { it.groupId } - .toList()) + .filter { it.membership == Membership.JOIN || it.membership == Membership.INVITE } + .map { it.groupId } + .toList()) modifiedGroupEntity .filter { it.membership == Membership.LEAVE } @@ -67,7 +68,7 @@ internal class GroupSummaryUpdater @Inject constructor(private val context: Cont } private fun fetchGroupsData(groupIds: List) { - val getGroupDataWorkerParams = GetGroupDataWorker.Params(userId, groupIds) + val getGroupDataWorkerParams = GetGroupDataWorker.Params(sessionId, groupIds) val workData = WorkerParamsFactory.toData(getGroupDataWorkerParams) 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 45571286b9..3837d893f9 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 @@ -23,14 +23,16 @@ import im.vector.matrix.android.internal.database.query.getOrCreate 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 java.util.Date +import org.greenrobot.eventbus.EventBus +import java.util.* import javax.inject.Inject internal interface GetHomeServerCapabilitiesTask : Task internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( private val capabilitiesAPI: CapabilitiesAPI, - private val monarchy: Monarchy + private val monarchy: Monarchy, + private val eventBus: EventBus ) : GetHomeServerCapabilitiesTask { override suspend fun execute(params: Unit) { @@ -45,7 +47,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( return } - val uploadCapabilities = executeRequest { + val uploadCapabilities = executeRequest(eventBus) { apiCall = capabilitiesAPI.getUploadCapabilities() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/AddHttpPusherWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/AddHttpPusherWorker.kt index 9eed515d14..adb4bf32c2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/AddHttpPusherWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/AddHttpPusherWorker.kt @@ -27,8 +27,10 @@ import im.vector.matrix.android.internal.database.model.PusherEntity import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.util.awaitTransaction +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 javax.inject.Inject internal class AddHttpPusherWorker(context: Context, params: WorkerParameters) @@ -36,18 +38,20 @@ internal class AddHttpPusherWorker(context: Context, params: WorkerParameters) @JsonClass(generateAdapter = true) internal data class Params( + override val sessionId: String, val pusher: JsonPusher, - val userId: String - ) + override val lastFailureMessage: String? = null + ) : SessionWorkerParams @Inject lateinit var pushersAPI: PushersAPI @Inject lateinit var monarchy: Monarchy + @Inject lateinit var eventBus: EventBus override suspend fun doWork(): Result { val params = WorkerParamsFactory.fromData(inputData) ?: return Result.failure() - val sessionComponent = getSessionComponent(params.userId) ?: return Result.success() + val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() sessionComponent.inject(this) val pusher = params.pusher @@ -76,7 +80,7 @@ internal class AddHttpPusherWorker(context: Context, params: WorkerParameters) } private suspend fun setPusher(pusher: JsonPusher) { - executeRequest { + executeRequest(eventBus) { apiCall = pushersAPI.setPusher(pusher) } monarchy.awaitTransaction { realm -> diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/AddPushRuleTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/AddPushRuleTask.kt index 99992ef4dc..b310ba7cd1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/AddPushRuleTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/AddPushRuleTask.kt @@ -19,6 +19,7 @@ import im.vector.matrix.android.api.pushrules.RuleKind import im.vector.matrix.android.api.pushrules.rest.PushRule import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus import javax.inject.Inject internal interface AddPushRuleTask : Task { @@ -28,11 +29,13 @@ internal interface AddPushRuleTask : Task { ) } -internal class DefaultAddPushRuleTask @Inject constructor(private val pushRulesApi: PushRulesApi) - : AddPushRuleTask { +internal class DefaultAddPushRuleTask @Inject constructor( + private val pushRulesApi: PushRulesApi, + private val eventBus: EventBus +) : AddPushRuleTask { override suspend fun execute(params: AddPushRuleTask.Params) { - return executeRequest { + return executeRequest(eventBus) { apiCall = pushRulesApi.addRule(params.kind.value, params.pushRule.ruleId, params.pushRule) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt index 8c7e9fb263..cdbf6aeee4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt @@ -26,22 +26,23 @@ import im.vector.matrix.android.api.session.pushers.PushersService import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.PusherEntity import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.worker.WorkManagerUtil import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder import im.vector.matrix.android.internal.worker.WorkerParamsFactory -import java.util.UUID +import java.util.* import java.util.concurrent.TimeUnit import javax.inject.Inject -internal class DefaultPusherService @Inject constructor(private val context: Context, - private val monarchy: Monarchy, - @UserId private val userId: String, - private val getPusherTask: GetPushersTask, - private val removePusherTask: RemovePusherTask, - private val taskExecutor: TaskExecutor +internal class DefaultPusherService @Inject constructor( + private val context: Context, + private val monarchy: Monarchy, + @SessionId private val sessionId: String, + private val getPusherTask: GetPushersTask, + private val removePusherTask: RemovePusherTask, + private val taskExecutor: TaskExecutor ) : PushersService { override fun refreshPushers() { @@ -65,7 +66,7 @@ internal class DefaultPusherService @Inject constructor(private val context: Con data = JsonPusherData(url, if (withEventIdOnly) PushersService.EVENT_ID_ONLY else null), append = append) - val params = AddHttpPusherWorker.Params(pusher, userId) + val params = AddHttpPusherWorker.Params(sessionId, pusher) val request = matrixOneTimeWorkRequestBuilder() .setConstraints(WorkManagerUtil.workConstraints) @@ -86,7 +87,7 @@ internal class DefaultPusherService @Inject constructor(private val context: Con .executeBy(taskExecutor) } - override fun livePushers(): LiveData> { + override fun getPushersLive(): LiveData> { return monarchy.findAllMappedWithChanges( { realm -> PusherEntity.where(realm) }, { it.asDomain() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/GetPushRulesTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/GetPushRulesTask.kt index d135c36543..4f3b39d1a7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/GetPushRulesTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/GetPushRulesTask.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.session.pushers import im.vector.matrix.android.api.pushrules.rest.GetPushRulesResponse import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus import javax.inject.Inject internal interface GetPushRulesTask : Task { @@ -27,11 +28,14 @@ internal interface GetPushRulesTask : Task { /** * We keep this task, but it should not be used anymore, the push rules comes from the sync response */ -internal class DefaultGetPushRulesTask @Inject constructor(private val pushRulesApi: PushRulesApi, - private val savePushRulesTask: SavePushRulesTask) : GetPushRulesTask { +internal class DefaultGetPushRulesTask @Inject constructor( + private val pushRulesApi: PushRulesApi, + private val savePushRulesTask: SavePushRulesTask, + private val eventBus: EventBus +) : GetPushRulesTask { override suspend fun execute(params: GetPushRulesTask.Params) { - val response = executeRequest { + val response = executeRequest(eventBus) { apiCall = pushRulesApi.getAllRules() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/GetPushersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/GetPushersTask.kt index 045db56786..64e982914a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/GetPushersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/GetPushersTask.kt @@ -22,15 +22,19 @@ import im.vector.matrix.android.internal.database.model.PusherEntity 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 javax.inject.Inject internal interface GetPushersTask : Task -internal class DefaultGetPushersTask @Inject constructor(private val pushersAPI: PushersAPI, - private val monarchy: Monarchy) : GetPushersTask { +internal class DefaultGetPushersTask @Inject constructor( + private val pushersAPI: PushersAPI, + private val monarchy: Monarchy, + private val eventBus: EventBus +) : GetPushersTask { override suspend fun execute(params: Unit) { - val response = executeRequest { + val response = executeRequest(eventBus) { apiCall = pushersAPI.getPushers() } monarchy.awaitTransaction { realm -> diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/RemovePushRuleTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/RemovePushRuleTask.kt index c4938fa0cc..d0edda9cfb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/RemovePushRuleTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/RemovePushRuleTask.kt @@ -19,6 +19,7 @@ import im.vector.matrix.android.api.pushrules.RuleKind import im.vector.matrix.android.api.pushrules.rest.PushRule import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus import javax.inject.Inject internal interface RemovePushRuleTask : Task { @@ -28,11 +29,13 @@ internal interface RemovePushRuleTask : Task { ) } -internal class DefaultRemovePushRuleTask @Inject constructor(private val pushRulesApi: PushRulesApi) - : RemovePushRuleTask { +internal class DefaultRemovePushRuleTask @Inject constructor( + private val pushRulesApi: PushRulesApi, + private val eventBus: EventBus +) : RemovePushRuleTask { override suspend fun execute(params: RemovePushRuleTask.Params) { - return executeRequest { + return executeRequest(eventBus) { apiCall = pushRulesApi.deleteRule(params.kind.value, params.pushRule.ruleId) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/RemovePusherTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/RemovePusherTask.kt index 297375454a..48e3f40a21 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/RemovePusherTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/RemovePusherTask.kt @@ -25,6 +25,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 io.realm.Realm +import org.greenrobot.eventbus.EventBus import javax.inject.Inject internal interface RemovePusherTask : Task { @@ -34,7 +35,8 @@ internal interface RemovePusherTask : Task { internal class DefaultRemovePusherTask @Inject constructor( private val pushersAPI: PushersAPI, - private val monarchy: Monarchy + private val monarchy: Monarchy, + private val eventBus: EventBus ) : RemovePusherTask { override suspend fun execute(params: RemovePusherTask.Params) { @@ -59,7 +61,7 @@ internal class DefaultRemovePusherTask @Inject constructor( data = JsonPusherData(existing.data.url, existing.data.format), append = false ) - executeRequest { + executeRequest(eventBus) { apiCall = pushersAPI.setPusher(deleteBody) } monarchy.awaitTransaction { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/UpdatePushRuleEnableStatusTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/UpdatePushRuleEnableStatusTask.kt index 91ed65d833..c5d593c263 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/UpdatePushRuleEnableStatusTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/UpdatePushRuleEnableStatusTask.kt @@ -19,6 +19,7 @@ import im.vector.matrix.android.api.pushrules.RuleKind import im.vector.matrix.android.api.pushrules.rest.PushRule import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus import javax.inject.Inject internal interface UpdatePushRuleEnableStatusTask : Task { @@ -27,11 +28,13 @@ internal interface UpdatePushRuleEnableStatusTask : Task) { + if (isEncrypted()) { + callback.onFailure(IllegalStateException("Encryption is already enabled for this room")) + } else { + stateService.enableEncryption(algorithm, callback) + } + } } 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 dae9ba3bfd..dc93327ca4 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 @@ -21,6 +21,7 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.RoomService +import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.VersioningState import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams @@ -32,6 +33,7 @@ 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.query.findByAlias import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.query.process import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask import im.vector.matrix.android.internal.session.room.create.CreateRoomTask import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask @@ -41,6 +43,7 @@ import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.fetchCopyMap import io.realm.Realm +import io.realm.RealmQuery import javax.inject.Inject internal class DefaultRoomService @Inject constructor(private val monarchy: Monarchy, @@ -100,30 +103,51 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona }) } - override fun liveRoomSummaries(): LiveData> { - return monarchy.findAllMappedWithChanges( - { realm -> - RoomSummaryEntity.where(realm) - .isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) - .notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name) - }, + override fun getRoomSummaries(queryParams: RoomSummaryQueryParams): List { + return monarchy.fetchAllMappedSync( + { roomSummariesQuery(it, queryParams) }, { roomSummaryMapper.map(it) } ) } - override fun liveBreadcrumbs(): LiveData> { + override fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams): LiveData> { return monarchy.findAllMappedWithChanges( - { realm -> - RoomSummaryEntity.where(realm) - .isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) - .notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name) - .greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummaryEntity.NOT_IN_BREADCRUMBS) - .sort(RoomSummaryEntityFields.BREADCRUMBS_INDEX) - }, + { roomSummariesQuery(it, queryParams) }, { roomSummaryMapper.map(it) } ) } + private fun roomSummariesQuery(realm: Realm, queryParams: RoomSummaryQueryParams): RealmQuery { + val query = RoomSummaryEntity.where(realm) + query.process(RoomSummaryEntityFields.DISPLAY_NAME, queryParams.displayName) + query.process(RoomSummaryEntityFields.CANONICAL_ALIAS, queryParams.canonicalAlias) + query.process(RoomSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships) + query.notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name) + return query + } + + override fun getBreadcrumbs(): List { + return monarchy.fetchAllMappedSync( + { breadcrumbsQuery(it) }, + { roomSummaryMapper.map(it) } + ) + } + + override fun getBreadcrumbsLive(): LiveData> { + return monarchy.findAllMappedWithChanges( + { breadcrumbsQuery(it) }, + { roomSummaryMapper.map(it) } + ) + } + + private fun breadcrumbsQuery(realm: Realm): RealmQuery { + return RoomSummaryEntity.where(realm) + .isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) + .notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name) + .greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummaryEntity.NOT_IN_BREADCRUMBS) + .sort(RoomSummaryEntityFields.BREADCRUMBS_INDEX) + } + override fun onRoomDisplayed(roomId: String): Cancelable { return updateBreadcrumbsTask .configureWith(UpdateBreadcrumbsTask.Params(roomId)) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt index 548590ba48..61c21396ed 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt @@ -36,9 +36,10 @@ import javax.inject.Inject * The summaries can then be extracted and added (as a decoration) to a TimelineEvent for final display. */ -internal class EventRelationsAggregationUpdater @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration, - @UserId private val userId: String, - private val task: EventRelationsAggregationTask) : +internal class EventRelationsAggregationUpdater @Inject constructor( + @SessionDatabase realmConfiguration: RealmConfiguration, + @UserId private val userId: String, + private val task: EventRelationsAggregationTask) : RealmLiveEntityObserver(realmConfiguration) { override val query = Monarchy.Query { 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 c5b3f03d35..6896788de9 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 @@ -212,11 +212,12 @@ internal interface RoomAPI { /** * Join the given room. * - * @param roomId the room id + * @param roomIdOrAlias the room id or alias + * @param server_name the servers to attempt to join the room through * @param params the request body */ - @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/join") - fun join(@Path("roomId") roomId: String, + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "join/{roomIdOrAlias}") + fun join(@Path("roomIdOrAlias") roomIdOrAlias: String, @Query("server_name") viaServers: List, @Body params: Map): Call diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAvatarResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAvatarResolver.kt index c9d5aeb6bb..0bb2dc0f27 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAvatarResolver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAvatarResolver.kt @@ -20,10 +20,9 @@ import com.zhuinden.monarchy.Monarchy 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.RoomAvatarContent -import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.internal.database.mapper.ContentMapper import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.model.EventEntityFields +import im.vector.matrix.android.internal.database.model.RoomMemberEntityFields import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.UserId @@ -47,19 +46,15 @@ internal class RoomAvatarResolver @Inject constructor(private val monarchy: Mona return@doWithRealm } val roomMembers = RoomMembers(realm, roomId) - val members = roomMembers.queryRoomMembersEvent().findAll() + val members = roomMembers.queryActiveRoomMembersEvent().findAll() // detect if it is a room with no more than 2 members (i.e. an alone or a 1:1 chat) if (members.size == 1) { - res = members.firstOrNull()?.toRoomMember()?.avatarUrl + res = members.firstOrNull()?.avatarUrl } else if (members.size == 2) { - val firstOtherMember = members.where().notEqualTo(EventEntityFields.STATE_KEY, userId).findFirst() - res = firstOtherMember?.toRoomMember()?.avatarUrl + val firstOtherMember = members.where().notEqualTo(RoomMemberEntityFields.USER_ID, userId).findFirst() + res = firstOtherMember?.avatarUrl } } return res } - - private fun EventEntity?.toRoomMember(): RoomMember? { - return ContentMapper.map(this?.content).toModel() - } } 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 e9f33a547b..9fa922b940 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 @@ -24,15 +24,11 @@ import im.vector.matrix.android.api.session.room.model.RoomAliasesContent import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent import im.vector.matrix.android.api.session.room.model.RoomTopicContent import im.vector.matrix.android.internal.database.mapper.ContentMapper +import im.vector.matrix.android.internal.database.model.* import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.model.EventEntityFields 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.* -import im.vector.matrix.android.internal.database.query.isEventRead -import im.vector.matrix.android.internal.database.query.latestEvent -import im.vector.matrix.android.internal.database.query.prev -import im.vector.matrix.android.internal.database.query.where 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.RoomMembers @@ -41,10 +37,11 @@ import im.vector.matrix.android.internal.session.sync.model.RoomSyncUnreadNotifi import io.realm.Realm import javax.inject.Inject -internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId: String, - private val roomDisplayNameResolver: RoomDisplayNameResolver, - private val roomAvatarResolver: RoomAvatarResolver, - private val monarchy: Monarchy) { +internal class RoomSummaryUpdater @Inject constructor( + @UserId private val userId: String, + private val roomDisplayNameResolver: RoomDisplayNameResolver, + private val roomAvatarResolver: RoomAvatarResolver, + private val monarchy: Monarchy) { // TODO: maybe allow user of SDK to give that list private val PREVIEWABLE_TYPES = listOf( @@ -57,7 +54,7 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId EventType.CALL_HANGUP, EventType.CALL_ANSWER, EventType.ENCRYPTED, - EventType.ENCRYPTION, + EventType.STATE_ROOM_ENCRYPTION, EventType.STATE_ROOM_THIRD_PARTY_INVITE, EventType.STICKER, EventType.STATE_ROOM_CREATE @@ -93,10 +90,11 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev() val lastCanonicalAliasEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_CANONICAL_ALIAS).prev() val lastAliasesEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).prev() + val encryptionEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ENCRYPTION).prev() roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 - // avoid this call if we are sure there are unread events - || !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId) + // avoid this call if we are sure there are unread events + || !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId) roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString() roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId) @@ -105,18 +103,20 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId roomSummaryEntity.canonicalAlias = ContentMapper.map(lastCanonicalAliasEvent?.content).toModel() ?.canonicalAlias - val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel()?.aliases ?: emptyList() + val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel()?.aliases + ?: emptyList() roomSummaryEntity.aliases.clear() roomSummaryEntity.aliases.addAll(roomAliases) roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|") + roomSummaryEntity.isEncrypted = encryptionEvent != null if (updateMembers) { val otherRoomMembers = RoomMembers(realm, roomId) .queryRoomMembersEvent() - .notEqualTo(EventEntityFields.STATE_KEY, userId) + .notEqualTo(RoomMemberEntityFields.USER_ID, userId) .findAll() .asSequence() - .map { it.stateKey } + .map { it.userId } roomSummaryEntity.otherMemberIds.clear() roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/alias/GetRoomIdByAliasTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/alias/GetRoomIdByAliasTask.kt index 1a726c3fe5..6aee8c170b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/alias/GetRoomIdByAliasTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/alias/GetRoomIdByAliasTask.kt @@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.task.Task import io.realm.Realm +import org.greenrobot.eventbus.EventBus import javax.inject.Inject internal interface GetRoomIdByAliasTask : Task> { @@ -33,8 +34,11 @@ internal interface GetRoomIdByAliasTask : Task { var roomId = Realm.getInstance(monarchy.realmConfiguration).use { @@ -45,7 +49,7 @@ internal class DefaultGetRoomIdByAliasTask @Inject constructor(private val monar } else if (!params.searchOnServer) { Optional.from(null) } else { - roomId = executeRequest { + roomId = executeRequest(eventBus) { apiCall = roomAPI.getRoomIdByAlias(params.roomAlias) }.roomId Optional.from(roomId) 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 9af8434b7c..6567b7ad97 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 @@ -20,7 +20,7 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.room.failure.CreateRoomFailure 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.internal.database.RealmQueryLatch +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 import im.vector.matrix.android.internal.database.model.RoomSummaryEntity @@ -34,32 +34,36 @@ import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAcco import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.RealmConfiguration +import kotlinx.coroutines.TimeoutCancellationException +import org.greenrobot.eventbus.EventBus import java.util.concurrent.TimeUnit import javax.inject.Inject internal interface CreateRoomTask : Task -internal class DefaultCreateRoomTask @Inject constructor(private val roomAPI: RoomAPI, - private val monarchy: Monarchy, - private val directChatsHelper: DirectChatsHelper, - private val updateUserAccountDataTask: UpdateUserAccountDataTask, - private val readMarkersTask: SetReadMarkersTask, - @SessionDatabase - private val realmConfiguration: RealmConfiguration) : CreateRoomTask { +internal class DefaultCreateRoomTask @Inject constructor( + private val roomAPI: RoomAPI, + private val monarchy: Monarchy, + private val directChatsHelper: DirectChatsHelper, + private val updateUserAccountDataTask: UpdateUserAccountDataTask, + private val readMarkersTask: SetReadMarkersTask, + @SessionDatabase + private val realmConfiguration: RealmConfiguration, + private val eventBus: EventBus +) : CreateRoomTask { override suspend fun execute(params: CreateRoomParams): String { - val createRoomResponse = executeRequest { + val createRoomResponse = executeRequest(eventBus) { apiCall = roomAPI.createRoom(params) } 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) - val rql = RealmQueryLatch(realmConfiguration) { realm -> - realm.where(RoomEntity::class.java) - .equalTo(RoomEntityFields.ROOM_ID, roomId) - } try { - rql.await(timeout = 1L, timeUnit = TimeUnit.MINUTES) - } catch (exception: Exception) { + awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> + realm.where(RoomEntity::class.java) + .equalTo(RoomEntityFields.ROOM_ID, roomId) + } + } catch (exception: TimeoutCancellationException) { throw CreateRoomFailure.CreatedWithTimeout } if (params.isDirect()) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/directory/GetPublicRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/directory/GetPublicRoomTask.kt index a24765e0bb..2b7d2e7dd9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/directory/GetPublicRoomTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/directory/GetPublicRoomTask.kt @@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRooms import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus import javax.inject.Inject internal interface GetPublicRoomTask : Task { @@ -30,10 +31,13 @@ internal interface GetPublicRoomTask : Task> -internal class DefaultGetThirdPartyProtocolsTask @Inject constructor(private val roomAPI: RoomAPI) : GetThirdPartyProtocolsTask { +internal class DefaultGetThirdPartyProtocolsTask @Inject constructor( + private val roomAPI: RoomAPI, + private val eventBus: EventBus +) : GetThirdPartyProtocolsTask { override suspend fun execute(params: Unit): Map { - return executeRequest { + return executeRequest(eventBus) { apiCall = roomAPI.thirdPartyProtocols() } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt index 00c1c2c4ca..679f4a050b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt @@ -21,18 +21,23 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.members.MembershipService +import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.internal.database.model.RoomMemberEntity +import im.vector.matrix.android.internal.database.model.RoomMemberEntityFields +import im.vector.matrix.android.internal.query.process import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.fetchCopied +import io.realm.Realm +import io.realm.RealmQuery internal class DefaultMembershipService @AssistedInject constructor(@Assisted private val roomId: String, private val monarchy: Monarchy, @@ -58,29 +63,44 @@ internal class DefaultMembershipService @AssistedInject constructor(@Assisted pr } override fun getRoomMember(userId: String): RoomMember? { - val eventEntity = monarchy.fetchCopied { - RoomMembers(it, roomId).queryRoomMemberEvent(userId).findFirst() + val roomMemberEntity = monarchy.fetchCopied { + RoomMembers(it, roomId).getLastRoomMember(userId) } - return eventEntity?.asDomain()?.content.toModel() + return roomMemberEntity?.asDomain() } - override fun getRoomMemberIdsLive(): LiveData> { - return monarchy.findAllMappedWithChanges( + override fun getRoomMembers(queryParams: RoomMemberQueryParams): List { + return monarchy.fetchAllMappedSync( { - RoomMembers(it, roomId).queryRoomMembersEvent() + roomMembersQuery(it, queryParams) }, { - it.stateKey!! + it.asDomain() } ) } + override fun getRoomMembersLive(queryParams: RoomMemberQueryParams): LiveData> { + return monarchy.findAllMappedWithChanges( + { + roomMembersQuery(it, queryParams) + }, + { + it.asDomain() + } + ) + } + + private fun roomMembersQuery(realm: Realm, queryParams: RoomMemberQueryParams): RealmQuery { + return RoomMembers(realm, roomId).queryRoomMembersEvent() + .process(RoomMemberEntityFields.MEMBERSHIP_STR, queryParams.memberships) + .process(RoomMemberEntityFields.DISPLAY_NAME, queryParams.displayName) + } + override fun getNumberOfJoinedMembers(): Int { - var result = 0 - monarchy.runTransactionSync { - result = RoomMembers(it, roomId).getNumberOfJoinedMembers() + return Realm.getInstance(monarchy.realmConfiguration).use { + RoomMembers(it, roomId).getNumberOfJoinedMembers() } - return result } override fun invite(userId: String, reason: String?, callback: MatrixCallback): Cancelable { 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 7d9332ee84..3610511dbf 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 @@ -18,19 +18,19 @@ package im.vector.matrix.android.internal.session.room.membership import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.room.model.Membership +import im.vector.matrix.android.internal.database.helper.TimelineEventSenderVisitor import im.vector.matrix.android.internal.database.helper.addStateEvent -import im.vector.matrix.android.internal.database.helper.updateSenderData import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.session.sync.SyncTokenStore -import im.vector.matrix.android.internal.session.user.UserEntityFactory import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.Realm import io.realm.kotlin.createObject +import org.greenrobot.eventbus.EventBus import javax.inject.Inject internal interface LoadRoomMembersTask : Task { @@ -41,10 +41,14 @@ internal interface LoadRoomMembersTask : Task ) } -internal class DefaultLoadRoomMembersTask @Inject constructor(private val roomAPI: RoomAPI, - private val monarchy: Monarchy, - private val syncTokenStore: SyncTokenStore, - private val roomSummaryUpdater: RoomSummaryUpdater +internal class DefaultLoadRoomMembersTask @Inject constructor( + private val roomAPI: RoomAPI, + private val monarchy: Monarchy, + private val syncTokenStore: SyncTokenStore, + private val roomSummaryUpdater: RoomSummaryUpdater, + private val roomMemberEventHandler: RoomMemberEventHandler, + private val timelineEventSenderVisitor: TimelineEventSenderVisitor, + private val eventBus: EventBus ) : LoadRoomMembersTask { override suspend fun execute(params: LoadRoomMembersTask.Params) { @@ -52,7 +56,7 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(private val roomAP return } val lastToken = syncTokenStore.getLastToken() - val response = executeRequest { + val response = executeRequest(eventBus) { apiCall = roomAPI.getMembers(params.roomId, lastToken, null, params.excludeMembership?.value) } insertInDb(response, params.roomId) @@ -66,12 +70,11 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(private val roomAP for (roomMemberEvent in response.roomMemberEvents) { roomEntity.addStateEvent(roomMemberEvent) - UserEntityFactory.createOrNull(roomMemberEvent)?.also { - realm.insertOrUpdate(it) - } + roomMemberEventHandler.handle(realm, roomId, roomMemberEvent) } + timelineEventSenderVisitor.clear() roomEntity.chunks.flatMap { it.timelineEvents }.forEach { - it.updateSenderData() + timelineEventSenderVisitor.visit(it) } roomEntity.areAllMembersLoaded = true roomSummaryUpdater.update(realm, roomId, updateMembers = true) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt index 21270308ed..9382fbc54a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt @@ -23,9 +23,10 @@ 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.* import im.vector.matrix.android.internal.database.mapper.ContentMapper +import im.vector.matrix.android.internal.database.model.* import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.model.RoomEntity +import im.vector.matrix.android.internal.database.model.RoomMemberEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.where @@ -75,43 +76,46 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: } val roomMembers = RoomMembers(realm, roomId) - val loadedMembers = roomMembers.queryRoomMembersEvent().findAll() + val activeMembers = roomMembers.queryActiveRoomMembersEvent().findAll() if (roomEntity?.membership == Membership.INVITE) { - val inviteMeEvent = roomMembers.queryRoomMemberEvent(userId).findFirst() + val inviteMeEvent = roomMembers.getLastStateEvent(userId) val inviterId = inviteMeEvent?.sender name = if (inviterId != null) { - val inviterMemberEvent = loadedMembers.where() - .equalTo(EventEntityFields.STATE_KEY, inviterId) + activeMembers.where() + .equalTo(RoomMemberEntityFields.USER_ID, inviterId) .findFirst() - inviterMemberEvent?.toRoomMember()?.displayName + ?.displayName } else { context.getString(R.string.room_displayname_room_invite) } } else if (roomEntity?.membership == Membership.JOIN) { val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() - val otherMembersSubset: List = if (roomSummary?.heroes?.isNotEmpty() == true) { - roomSummary.heroes.mapNotNull { - roomMembers.getStateEvent(it) + val otherMembersSubset: List = if (roomSummary?.heroes?.isNotEmpty() == true) { + roomSummary.heroes.mapNotNull { userId -> + roomMembers.getLastRoomMember(userId)?.takeIf { + it.membership == Membership.INVITE || it.membership == Membership.JOIN + } } } else { - loadedMembers.where() - .notEqualTo(EventEntityFields.STATE_KEY, userId) + activeMembers.where() + .notEqualTo(RoomMemberEntityFields.USER_ID, userId) .limit(3) .findAll() + .createSnapshot() } - val otherMembersCount = roomMembers.getNumberOfMembers() - 1 + val otherMembersCount = otherMembersSubset.count() name = when (otherMembersCount) { 0 -> context.getString(R.string.room_displayname_empty_room) 1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers) 2 -> context.getString(R.string.room_displayname_two_members, - resolveRoomMemberName(otherMembersSubset[0], roomMembers), - resolveRoomMemberName(otherMembersSubset[1], roomMembers) + resolveRoomMemberName(otherMembersSubset[0], roomMembers), + resolveRoomMemberName(otherMembersSubset[1], roomMembers) ) else -> context.resources.getQuantityString(R.plurals.room_displayname_three_and_more_members, - roomMembers.getNumberOfJoinedMembers() - 1, - resolveRoomMemberName(otherMembersSubset[0], roomMembers), - roomMembers.getNumberOfJoinedMembers() - 1) + roomMembers.getNumberOfJoinedMembers() - 1, + resolveRoomMemberName(otherMembersSubset[0], roomMembers), + roomMembers.getNumberOfJoinedMembers() - 1) } } return@doWithRealm @@ -119,19 +123,14 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: return name ?: roomId } - private fun resolveRoomMemberName(eventEntity: EventEntity?, + private fun resolveRoomMemberName(roomMember: RoomMemberEntity?, roomMembers: RoomMembers): String? { - if (eventEntity == null) return null - val roomMember = eventEntity.toRoomMember() ?: return null + if (roomMember == null) return null val isUnique = roomMembers.isUniqueDisplayName(roomMember.displayName) return if (isUnique) { roomMember.displayName } else { - "${roomMember.displayName} (${eventEntity.stateKey})" + "${roomMember.displayName} (${roomMember.userId})" } } - - private fun EventEntity?.toRoomMember(): RoomMember? { - return ContentMapper.map(this?.content).toModel() - } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEntityFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEntityFactory.kt new file mode 100644 index 0000000000..51df244401 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEntityFactory.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.room.membership + +import im.vector.matrix.android.api.session.room.model.RoomMemberContent +import im.vector.matrix.android.internal.database.model.RoomMemberEntity + +internal object RoomMemberEntityFactory { + + fun create(roomId: String, userId: String, roomMember: RoomMemberContent): RoomMemberEntity { + val primaryKey = "${roomId}_$userId" + return RoomMemberEntity( + primaryKey = primaryKey, + userId = userId, + roomId = roomId, + displayName = roomMember.displayName ?: "", + avatarUrl = roomMember.avatarUrl ?: "" + ).apply { + membership = roomMember.membership + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt new file mode 100644 index 0000000000..9bd97cec10 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.room.membership + +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.Membership +import im.vector.matrix.android.api.session.room.model.RoomMemberContent +import im.vector.matrix.android.internal.session.user.UserEntityFactory +import io.realm.Realm +import javax.inject.Inject + +internal class RoomMemberEventHandler @Inject constructor() { + + fun handle(realm: Realm, roomId: String, event: Event): Boolean { + if (event.type != EventType.STATE_ROOM_MEMBER) { + return false + } + val roomMember = event.content.toModel() ?: return false + val userId = event.stateKey ?: return false + val roomMemberEntity = RoomMemberEntityFactory.create(roomId, userId, roomMember) + realm.insertOrUpdate(roomMemberEntity) + if (roomMember.membership in Membership.activeMemberships()) { + val userEntity = UserEntityFactory.create(userId, roomMember) + realm.insertOrUpdate(userEntity) + } + return true + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt index 9fba1d8f02..e3775f5ade 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt @@ -17,12 +17,10 @@ package im.vector.matrix.android.internal.session.room.membership 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.Membership -import im.vector.matrix.android.api.session.room.model.RoomMember -import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.internal.database.model.* import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.model.EventEntityFields +import im.vector.matrix.android.internal.database.model.RoomMemberEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.query.where import io.realm.Realm @@ -42,19 +40,18 @@ internal class RoomMembers(private val realm: Realm, RoomSummaryEntity.where(realm, roomId).findFirst() } - fun getStateEvent(userId: String): EventEntity? { + fun getLastStateEvent(userId: String): EventEntity? { return EventEntity .where(realm, roomId, EventType.STATE_ROOM_MEMBER) - .sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING) .equalTo(EventEntityFields.STATE_KEY, userId) + .sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING) .findFirst() } - fun get(userId: String): RoomMember? { - return getStateEvent(userId) - ?.let { - it.asDomain().content?.toModel() - } + fun getLastRoomMember(userId: String): RoomMemberEntity? { + return RoomMemberEntity + .where(realm, roomId, userId) + .findFirst() } fun isUniqueDisplayName(displayName: String?): Boolean { @@ -69,36 +66,37 @@ internal class RoomMembers(private val realm: Realm, .size == 1 } - fun queryRoomMembersEvent(): RealmQuery { - return EventEntity - .where(realm, roomId, EventType.STATE_ROOM_MEMBER) - .sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING) - .isNotNull(EventEntityFields.STATE_KEY) - .distinct(EventEntityFields.STATE_KEY) - .isNotNull(EventEntityFields.CONTENT) + fun queryRoomMembersEvent(): RealmQuery { + return RoomMemberEntity.where(realm, roomId) } - fun queryJoinedRoomMembersEvent(): RealmQuery { - return queryRoomMembersEvent().contains(EventEntityFields.CONTENT, "\"membership\":\"join\"") - } - - fun queryInvitedRoomMembersEvent(): RealmQuery { - return queryRoomMembersEvent().contains(EventEntityFields.CONTENT, "\"membership\":\"invite\"") - } - - fun queryRoomMemberEvent(userId: String): RealmQuery { + fun queryJoinedRoomMembersEvent(): RealmQuery { return queryRoomMembersEvent() - .equalTo(EventEntityFields.STATE_KEY, userId) + .equalTo(RoomMemberEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) + } + + fun queryInvitedRoomMembersEvent(): RealmQuery { + return queryRoomMembersEvent() + .equalTo(RoomMemberEntityFields.MEMBERSHIP_STR, Membership.INVITE.name) + } + + fun queryActiveRoomMembersEvent(): RealmQuery { + return queryRoomMembersEvent() + .beginGroup() + .equalTo(RoomMemberEntityFields.MEMBERSHIP_STR, Membership.INVITE.name) + .or() + .equalTo(RoomMemberEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) + .endGroup() } fun getNumberOfJoinedMembers(): Int { return roomSummary?.joinedMembersCount - ?: queryJoinedRoomMembersEvent().findAll().size + ?: queryJoinedRoomMembersEvent().findAll().size } fun getNumberOfInvitedMembers(): Int { return roomSummary?.invitedMembersCount - ?: queryInvitedRoomMembersEvent().findAll().size + ?: queryInvitedRoomMembersEvent().findAll().size } fun getNumberOfMembers(): Int { @@ -111,7 +109,7 @@ internal class RoomMembers(private val realm: Realm, * @return a roomMember id list of joined or invited members. */ fun getActiveRoomMemberIds(): List { - return getRoomMemberIdsFiltered { it.membership == Membership.JOIN || it.membership == Membership.INVITE } + return queryActiveRoomMembersEvent().findAll().map { it.userId } } /** @@ -120,21 +118,6 @@ internal class RoomMembers(private val realm: Realm, * @return a roomMember id list of joined members. */ fun getJoinedRoomMemberIds(): List { - return getRoomMemberIdsFiltered { it.membership == Membership.JOIN } - } - - /* ========================================================================================== - * Private - * ========================================================================================== */ - - private fun getRoomMemberIdsFiltered(predicate: (RoomMember) -> Boolean): List { - return RoomMembers(realm, roomId) - .queryRoomMembersEvent() - .findAll() - .map { it.asDomain() } - .associateBy { it.stateKey!! } - .filterValues { predicate(it.content.toModel()!!) } - .keys - .toList() + return queryJoinedRoomMembersEvent().findAll().map { it.userId } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteTask.kt index 6bc453a0f3..93b3889455 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteTask.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.session.room.membership.joining import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus import javax.inject.Inject internal interface InviteTask : Task { @@ -29,10 +30,13 @@ internal interface InviteTask : Task { ) } -internal class DefaultInviteTask @Inject constructor(private val roomAPI: RoomAPI) : InviteTask { +internal class DefaultInviteTask @Inject constructor( + private val roomAPI: RoomAPI, + private val eventBus: EventBus +) : InviteTask { override suspend fun execute(params: InviteTask.Params) { - return executeRequest { + return executeRequest(eventBus) { val body = InviteBody(params.userId, params.reason) apiCall = roomAPI.invite(params.roomId, body) } 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 7304c09d57..d4341951eb 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,7 +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.internal.database.RealmQueryLatch +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 import im.vector.matrix.android.internal.di.SessionDatabase @@ -26,6 +26,8 @@ import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask import im.vector.matrix.android.internal.task.Task import io.realm.RealmConfiguration +import kotlinx.coroutines.TimeoutCancellationException +import org.greenrobot.eventbus.EventBus import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -37,27 +39,28 @@ internal interface JoinRoomTask : Task { ) } -internal class DefaultJoinRoomTask @Inject constructor(private val roomAPI: RoomAPI, - private val readMarkersTask: SetReadMarkersTask, - @SessionDatabase - private val realmConfiguration: RealmConfiguration) : JoinRoomTask { +internal class DefaultJoinRoomTask @Inject constructor( + private val roomAPI: RoomAPI, + private val readMarkersTask: SetReadMarkersTask, + @SessionDatabase + private val realmConfiguration: RealmConfiguration, + private val eventBus: EventBus +) : JoinRoomTask { override suspend fun execute(params: JoinRoomTask.Params) { - executeRequest { + executeRequest(eventBus) { apiCall = roomAPI.join(params.roomId, params.viaServers, mapOf("reason" to params.reason)) } - val roomId = params.roomId // 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 rql = RealmQueryLatch(realmConfiguration) { realm -> - realm.where(RoomEntity::class.java) - .equalTo(RoomEntityFields.ROOM_ID, roomId) - } try { - rql.await(timeout = 1L, timeUnit = TimeUnit.MINUTES) - } catch (exception: Exception) { + awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> + realm.where(RoomEntity::class.java) + .equalTo(RoomEntityFields.ROOM_ID, params.roomId) + } + } catch (exception: TimeoutCancellationException) { throw JoinRoomFailure.JoinedWithTimeout } - setReadMarkers(roomId) + setReadMarkers(params.roomId) } private suspend fun setReadMarkers(roomId: String) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/leaving/LeaveRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/leaving/LeaveRoomTask.kt index 01198c47de..08eb71fc89 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/leaving/LeaveRoomTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/leaving/LeaveRoomTask.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.session.room.membership.leaving import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus import javax.inject.Inject internal interface LeaveRoomTask : Task { @@ -28,10 +29,13 @@ internal interface LeaveRoomTask : Task { ) } -internal class DefaultLeaveRoomTask @Inject constructor(private val roomAPI: RoomAPI) : LeaveRoomTask { +internal class DefaultLeaveRoomTask @Inject constructor( + private val roomAPI: RoomAPI, + private val eventBus: EventBus +) : LeaveRoomTask { override suspend fun execute(params: LeaveRoomTask.Params) { - return executeRequest { + return executeRequest(eventBus) { apiCall = roomAPI.leave(params.roomId, mapOf("reason" to params.reason)) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt index de3eb1eab2..8228136f10 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt @@ -20,7 +20,7 @@ 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 import im.vector.matrix.android.api.session.events.model.UnsignedData -import im.vector.matrix.android.internal.database.helper.updateSenderData +import im.vector.matrix.android.internal.database.helper.TimelineEventSenderVisitor import im.vector.matrix.android.internal.database.mapper.ContentMapper import im.vector.matrix.android.internal.database.mapper.EventMapper import im.vector.matrix.android.internal.database.model.EventEntity @@ -41,7 +41,8 @@ internal interface PruneEventTask : Task { ) } -internal class DefaultPruneEventTask @Inject constructor(private val monarchy: Monarchy) : PruneEventTask { +internal class DefaultPruneEventTask @Inject constructor(private val monarchy: Monarchy, + private val timelineEventSenderVisitor: TimelineEventSenderVisitor) : PruneEventTask { override suspend fun execute(params: PruneEventTask.Params) { monarchy.awaitTransaction { realm -> @@ -65,12 +66,14 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst() ?: return - val allowedKeys = computeAllowedKeys(eventToPrune.type) + val typeToPrune = eventToPrune.type + val stateKey = eventToPrune.stateKey + val allowedKeys = computeAllowedKeys(typeToPrune) if (allowedKeys.isNotEmpty()) { val prunedContent = ContentMapper.map(eventToPrune.content)?.filterKeys { key -> allowedKeys.contains(key) } eventToPrune.content = ContentMapper.map(prunedContent) } else { - when (eventToPrune.type) { + when (typeToPrune) { EventType.ENCRYPTED, EventType.MESSAGE -> { Timber.d("REDACTION for message ${eventToPrune.eventId}") @@ -94,11 +97,10 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M // } } } - if (eventToPrune.type == EventType.STATE_ROOM_MEMBER) { + if (typeToPrune == EventType.STATE_ROOM_MEMBER && stateKey != null) { + timelineEventSenderVisitor.clear(roomId = eventToPrune.roomId, senderId = stateKey) val timelineEventsToUpdate = TimelineEventEntity.findWithSenderMembershipEvent(realm, eventToPrune.eventId) - for (timelineEvent in timelineEventsToUpdate) { - timelineEvent.updateSenderData() - } + timelineEventSenderVisitor.visit(timelineEventsToUpdate) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt index b8b9fe82ef..a9a0f60083 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt @@ -36,12 +36,13 @@ import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith -internal class DefaultReadService @AssistedInject constructor(@Assisted private val roomId: String, - private val monarchy: Monarchy, - private val taskExecutor: TaskExecutor, - private val setReadMarkersTask: SetReadMarkersTask, - private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, - @UserId private val userId: String +internal class DefaultReadService @AssistedInject constructor( + @Assisted private val roomId: String, + private val monarchy: Monarchy, + private val taskExecutor: TaskExecutor, + private val setReadMarkersTask: SetReadMarkersTask, + private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, + @UserId private val userId: String ) : ReadService { @AssistedInject.Factory diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt index b9dca748cb..6a0d04193d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt @@ -20,7 +20,8 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.events.model.LocalEcho 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.* +import im.vector.matrix.android.internal.database.query.isEventRead +import im.vector.matrix.android.internal.database.query.isReadMarkerMoreRecent import im.vector.matrix.android.internal.database.query.latestEvent import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.UserId @@ -31,8 +32,11 @@ import im.vector.matrix.android.internal.session.sync.RoomFullyReadHandler import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.Realm +import org.greenrobot.eventbus.EventBus import timber.log.Timber import javax.inject.Inject +import kotlin.collections.HashMap +import kotlin.collections.set internal interface SetReadMarkersTask : Task { @@ -47,12 +51,14 @@ internal interface SetReadMarkersTask : Task { private const val READ_MARKER = "m.fully_read" private const val READ_RECEIPT = "m.read" -internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI: RoomAPI, - private val monarchy: Monarchy, - private val roomFullyReadHandler: RoomFullyReadHandler, - private val readReceiptHandler: ReadReceiptHandler, - @UserId private val userId: String) - : SetReadMarkersTask { +internal class DefaultSetReadMarkersTask @Inject constructor( + private val roomAPI: RoomAPI, + private val monarchy: Monarchy, + private val roomFullyReadHandler: RoomFullyReadHandler, + private val readReceiptHandler: ReadReceiptHandler, + @UserId private val userId: String, + private val eventBus: EventBus +) : SetReadMarkersTask { override suspend fun execute(params: SetReadMarkersTask.Params) { val markers = HashMap() @@ -76,7 +82,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI } if (readReceiptEventId != null - && !isEventRead(monarchy, userId, params.roomId, readReceiptEventId)) { + && !isEventRead(monarchy, userId, params.roomId, readReceiptEventId)) { if (LocalEcho.isLocalEchoId(readReceiptEventId)) { Timber.w("Can't set read receipt for local event $readReceiptEventId") } else { @@ -87,7 +93,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI return } updateDatabase(params.roomId, markers) - executeRequest { + executeRequest(eventBus) { apiCall = roomAPI.sendReadMarker(params.roomId, markers) } } @@ -105,7 +111,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI val isLatestReceived = TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId == readReceiptId if (isLatestReceived) { val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() - ?: return@awaitTransaction + ?: return@awaitTransaction roomSummary.notificationCount = 0 roomSummary.highlightCount = 0 roomSummary.hasUnreadMessages = false 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 8731045e14..1b2b27a3eb 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 @@ -38,7 +38,7 @@ import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.session.room.send.EncryptEventWorker import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory import im.vector.matrix.android.internal.session.room.send.RedactEventWorker @@ -51,16 +51,17 @@ import im.vector.matrix.android.internal.util.fetchCopyMap import im.vector.matrix.android.internal.worker.WorkerParamsFactory import timber.log.Timber -internal class DefaultRelationService @AssistedInject constructor(@Assisted private val roomId: String, - private val context: Context, - @UserId private val userId: String, - private val eventFactory: LocalEchoEventFactory, - private val cryptoService: CryptoService, - private val findReactionEventForUndoTask: FindReactionEventForUndoTask, - private val fetchEditHistoryTask: FetchEditHistoryTask, - private val timelineEventMapper: TimelineEventMapper, - private val monarchy: Monarchy, - private val taskExecutor: TaskExecutor) +internal class DefaultRelationService @AssistedInject constructor( + @Assisted private val roomId: String, + private val context: Context, + @SessionId private val sessionId: String, + private val eventFactory: LocalEchoEventFactory, + private val cryptoService: CryptoService, + private val findReactionEventForUndoTask: FindReactionEventForUndoTask, + private val fetchEditHistoryTask: FetchEditHistoryTask, + private val timelineEventMapper: TimelineEventMapper, + private val monarchy: Monarchy, + private val taskExecutor: TaskExecutor) : RelationService { @AssistedInject.Factory @@ -125,7 +126,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv // TODO duplicate with send service? private fun createRedactEventWork(localEvent: Event, eventId: String, reason: String?): OneTimeWorkRequest { val sendContentWorkerParams = RedactEventWorker.Params( - userId, + sessionId, localEvent.eventId!!, roomId, eventId, @@ -204,18 +205,27 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv private fun createEncryptEventWork(event: Event, keepKeys: List?): OneTimeWorkRequest { // Same parameter - val params = EncryptEventWorker.Params(userId, roomId, event, keepKeys) + val params = EncryptEventWorker.Params(sessionId, roomId, event, keepKeys) val sendWorkData = WorkerParamsFactory.toData(params) return TimelineSendEventWorkCommon.createWork(sendWorkData, true) } private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { - val sendContentWorkerParams = SendEventWorker.Params(userId, roomId, event) + val sendContentWorkerParams = SendEventWorker.Params(sessionId, roomId, event) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) return TimelineSendEventWorkCommon.createWork(sendWorkData, startChain) } - override fun getEventSummaryLive(eventId: String): LiveData> { + override fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary? { + return monarchy.fetchCopyMap( + { EventAnnotationsSummaryEntity.where(it, eventId).findFirst() }, + { entity, _ -> + entity.asDomain() + } + ) + } + + override fun getEventAnnotationsSummaryLive(eventId: String): LiveData> { val liveData = monarchy.findAllMappedWithChanges( { EventAnnotationsSummaryEntity.where(it, eventId) }, { it.asDomain() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FetchEditHistoryTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FetchEditHistoryTask.kt index 5e5db58bdb..a4ab06f767 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FetchEditHistoryTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FetchEditHistoryTask.kt @@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.events.model.RelationType import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus import javax.inject.Inject internal interface FetchEditHistoryTask : Task> { @@ -33,11 +34,12 @@ internal interface FetchEditHistoryTask : Task { - val response = executeRequest { + val response = executeRequest(eventBus) { apiCall = roomAPI.getRelations(params.roomId, params.eventId, RelationType.REPLACE, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FindReactionEventForUndoTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FindReactionEventForUndoTask.kt index baa01e4042..bdf4fab35e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FindReactionEventForUndoTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FindReactionEventForUndoTask.kt @@ -38,8 +38,9 @@ internal interface FindReactionEventForUndoTask : Task @@ -55,7 +56,7 @@ internal class DefaultFindReactionEventForUndoTask @Inject constructor(private v .equalTo(ReactionAggregatedSummaryEntityFields.KEY, reaction) .findFirst() ?: return null - // want to find the event orignated by me! + // want to find the event originated by me! return rase.sourceEvents .asSequence() .mapNotNull { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt index eafe1e7419..5857eaa89b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt @@ -30,20 +30,23 @@ import im.vector.matrix.android.internal.session.room.send.SendResponse 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 javax.inject.Inject +// TODO This is not used. Delete? internal class SendRelationWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { @JsonClass(generateAdapter = true) internal data class Params( - override val userId: String, + override val sessionId: String, val roomId: String, val event: Event, val relationType: String? = null, - override val lastFailureMessage: String? + override val lastFailureMessage: String? = null ) : SessionWorkerParams @Inject lateinit var roomAPI: RoomAPI + @Inject lateinit var eventBus: EventBus override suspend fun doWork(): Result { val params = WorkerParamsFactory.fromData(inputData) @@ -54,7 +57,7 @@ internal class SendRelationWorker(context: Context, params: WorkerParameters) : return Result.success(inputData) } - val sessionComponent = getSessionComponent(params.userId) ?: return Result.success() + val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() sessionComponent.inject(this) val localEvent = params.event @@ -82,7 +85,7 @@ internal class SendRelationWorker(context: Context, params: WorkerParameters) : } private suspend fun sendRelation(roomId: String, relationType: String, relatedEventId: String, localEvent: Event) { - executeRequest { + executeRequest(eventBus) { apiCall = roomAPI.sendRelation( roomId = roomId, parent_id = relatedEventId, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/ReportContentTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/ReportContentTask.kt index 60c031158a..74012348df 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/ReportContentTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/ReportContentTask.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.session.room.reporting import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus import javax.inject.Inject internal interface ReportContentTask : Task { @@ -30,9 +31,13 @@ internal interface ReportContentTask : Task { ) } -internal class DefaultReportContentTask @Inject constructor(private val roomAPI: RoomAPI) : ReportContentTask { +internal class DefaultReportContentTask @Inject constructor( + private val roomAPI: RoomAPI, + private val eventBus: EventBus +) : ReportContentTask { + override suspend fun execute(params: ReportContentTask.Params) { - return executeRequest { + return executeRequest(eventBus) { apiCall = roomAPI.reportContent(params.roomId, params.eventId, ReportContentBody(params.score, params.reason)) } } 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 0e6c93590b..9942e36593 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 @@ -37,7 +37,7 @@ import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.session.content.UploadContentWorker import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon import im.vector.matrix.android.internal.util.CancelableWork @@ -53,12 +53,13 @@ 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, - private val context: Context, - @UserId private val userId: String, - private val localEchoEventFactory: LocalEchoEventFactory, - private val cryptoService: CryptoService, - private val monarchy: Monarchy +internal class DefaultSendService @AssistedInject constructor( + @Assisted private val roomId: String, + private val context: Context, + @SessionId private val sessionId: String, + private val localEchoEventFactory: LocalEchoEventFactory, + private val cryptoService: CryptoService, + private val monarchy: Monarchy ) : SendService { @AssistedInject.Factory @@ -285,7 +286,7 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { // Same parameter - val params = EncryptEventWorker.Params(userId, roomId, event) + val params = EncryptEventWorker.Params(sessionId, roomId, event) val sendWorkData = WorkerParamsFactory.toData(params) return matrixOneTimeWorkRequestBuilder() @@ -297,7 +298,7 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private } private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { - val sendContentWorkerParams = SendEventWorker.Params(userId, roomId, event) + val sendContentWorkerParams = SendEventWorker.Params(sessionId, roomId, event) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) return TimelineSendEventWorkCommon.createWork(sendWorkData, startChain) @@ -307,7 +308,7 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private val redactEvent = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason).also { saveLocalEcho(it) } - val sendContentWorkerParams = RedactEventWorker.Params(userId, redactEvent.eventId!!, roomId, event.eventId, reason) + val sendContentWorkerParams = RedactEventWorker.Params(sessionId, redactEvent.eventId!!, roomId, event.eventId, reason) val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) return TimelineSendEventWorkCommon.createWork(redactWorkData, true) } @@ -316,7 +317,7 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private attachment: ContentAttachmentData, isRoomEncrypted: Boolean, startChain: Boolean): OneTimeWorkRequest { - val uploadMediaWorkerParams = UploadContentWorker.Params(userId, roomId, event, attachment, isRoomEncrypted) + val uploadMediaWorkerParams = UploadContentWorker.Params(sessionId, roomId, event, attachment, isRoomEncrypted) val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams) return matrixOneTimeWorkRequestBuilder() 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 a269529dd2..6f1593bc08 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 @@ -37,7 +37,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) @JsonClass(generateAdapter = true) internal data class Params( - override val userId: String, + 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)*/ @@ -61,7 +61,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) return Result.success(inputData) } - val sessionComponent = getSessionComponent(params.userId) ?: return Result.success() + val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() sessionComponent.inject(this) val localEvent = params.event @@ -97,7 +97,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) type = safeResult.eventType, content = safeResult.eventContent ) - val nextWorkerParams = SendEventWorker.Params(params.userId, params.roomId, encryptedEvent) + val nextWorkerParams = SendEventWorker.Params(params.sessionId, params.roomId, encryptedEvent) return Result.success(WorkerParamsFactory.toData(nextWorkerParams)) } else { val sendState = when (error) { @@ -106,7 +106,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.userId, params.roomId, localEvent, error?.localizedMessage + val nextWorkerParams = SendEventWorker.Params(params.sessionId, params.roomId, 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/RedactEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt index ec311458cd..3ff318aa8a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt @@ -25,13 +25,14 @@ import im.vector.matrix.android.internal.session.room.RoomAPI 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 javax.inject.Inject internal class RedactEventWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { @JsonClass(generateAdapter = true) internal data class Params( - override val userId: String, + override val sessionId: String, val txID: String, val roomId: String, val eventId: String, @@ -40,6 +41,7 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters) : C ) : SessionWorkerParams @Inject lateinit var roomAPI: RoomAPI + @Inject lateinit var eventBus: EventBus override suspend fun doWork(): Result { val params = WorkerParamsFactory.fromData(inputData) @@ -50,12 +52,12 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters) : C return Result.success(inputData) } - val sessionComponent = getSessionComponent(params.userId) ?: return Result.success() + val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() sessionComponent.inject(this) val eventId = params.eventId return runCatching { - executeRequest { + executeRequest(eventBus) { apiCall = roomAPI.redactEvent( params.txID, params.roomId, 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 026c5e9df5..627a444c6d 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 @@ -29,6 +29,7 @@ import im.vector.matrix.android.internal.session.room.RoomAPI 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 javax.inject.Inject internal class SendEventWorker constructor(context: Context, params: WorkerParameters) @@ -36,7 +37,7 @@ internal class SendEventWorker constructor(context: Context, params: WorkerParam @JsonClass(generateAdapter = true) internal data class Params( - override val userId: String, + override val sessionId: String, val roomId: String, val event: Event, override val lastFailureMessage: String? = null @@ -44,12 +45,13 @@ internal class SendEventWorker constructor(context: Context, params: WorkerParam @Inject lateinit var localEchoUpdater: LocalEchoUpdater @Inject lateinit var roomAPI: RoomAPI + @Inject lateinit var eventBus: EventBus override suspend fun doWork(): Result { val params = WorkerParamsFactory.fromData(inputData) ?: return Result.success() - val sessionComponent = getSessionComponent(params.userId) ?: return Result.success() + val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() sessionComponent.inject(this) val event = params.event @@ -78,7 +80,7 @@ internal class SendEventWorker constructor(context: Context, params: WorkerParam private suspend fun sendEvent(eventId: String, eventType: String, content: Content?, roomId: String) { localEchoUpdater.updateSendState(eventId, SendState.SENDING) - executeRequest { + executeRequest(eventBus) { apiCall = roomAPI.send( eventId, roomId, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt index 785fd9ae71..3cd73a97e0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt @@ -22,6 +22,7 @@ import im.vector.matrix.android.api.MatrixCallback 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.room.state.StateService +import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM 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.prev @@ -31,6 +32,7 @@ import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import io.realm.Realm import io.realm.RealmConfiguration +import java.security.InvalidParameterException internal class DefaultStateService @AssistedInject constructor(@Assisted private val roomId: String, @SessionDatabase @@ -52,10 +54,10 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private override fun updateTopic(topic: String, callback: MatrixCallback) { val params = SendStateTask.Params(roomId, - EventType.STATE_ROOM_TOPIC, - mapOf( - "topic" to topic - )) + EventType.STATE_ROOM_TOPIC, + mapOf( + "topic" to topic + )) sendStateTask .configureWith(params) { @@ -63,4 +65,22 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private } .executeBy(taskExecutor) } + + override fun enableEncryption(algorithm: String, callback: MatrixCallback) { + if (algorithm != MXCRYPTO_ALGORITHM_MEGOLM) { + callback.onFailure(InvalidParameterException("Only MXCRYPTO_ALGORITHM_MEGOLM algorithm is supported")) + } else { + val params = SendStateTask.Params(roomId, + EventType.STATE_ROOM_ENCRYPTION, + mapOf( + "algorithm" to algorithm + )) + + sendStateTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/SendStateTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/SendStateTask.kt index 39d606f5df..b0d583c6d1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/SendStateTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/SendStateTask.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.session.room.state import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus import javax.inject.Inject internal interface SendStateTask : Task { @@ -29,9 +30,13 @@ internal interface SendStateTask : Task { ) } -internal class DefaultSendStateTask @Inject constructor(private val roomAPI: RoomAPI) : SendStateTask { +internal class DefaultSendStateTask @Inject constructor( + private val roomAPI: RoomAPI, + private val eventBus: EventBus +) : SendStateTask { + override suspend fun execute(params: SendStateTask.Params) { - return executeRequest { + return executeRequest(eventBus) { apiCall = roomAPI.sendStateEvent(params.roomId, params.eventType, params.body) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/ClearUnlinkedEventsTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/ClearUnlinkedEventsTask.kt index 04cf810fe4..b532d61914 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/ClearUnlinkedEventsTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/ClearUnlinkedEventsTask.kt @@ -21,7 +21,6 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.internal.database.helper.deleteOnCascade import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ChunkEntityFields -import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.awaitTransaction @@ -38,7 +37,7 @@ internal class DefaultClearUnlinkedEventsTask @Inject constructor(private val mo monarchy.awaitTransaction { localRealm -> val unlinkedChunks = ChunkEntity .where(localRealm, roomId = params.roomId) - .equalTo("${ChunkEntityFields.TIMELINE_EVENTS.ROOT}.${EventEntityFields.IS_UNLINKED}", true) + .equalTo(ChunkEntityFields.IS_UNLINKED, true) .findAll() unlinkedChunks.forEach { it.deleteOnCascade() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt index 08d34d3056..966bdcc1fd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt @@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.filter.FilterRepository import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus import javax.inject.Inject internal interface GetContextOfEventTask : Task { @@ -31,14 +32,16 @@ internal interface GetContextOfEventTask : Task { + val response = executeRequest(eventBus) { apiCall = roomAPI.getContextOfEvent(params.roomId, params.eventId, params.limit, filter) } return tokenChunkEventPersistor.insertInDb(response, params.roomId, PaginationDirection.BACKWARDS) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultPaginationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultPaginationTask.kt index d817b22996..aa7b4321dc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultPaginationTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultPaginationTask.kt @@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.filter.FilterRepository import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus import javax.inject.Inject internal interface PaginationTask : Task { @@ -32,14 +33,16 @@ internal interface PaginationTask : Task { + val chunk = executeRequest(eventBus) { apiCall = roomAPI.getRoomMessagesFrom(params.roomId, params.from, params.direction.value, params.limit, filter) } return tokenChunkEventPersistor.insertInDb(chunk, params.roomId, params.direction) 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 85bab5d706..057295ec44 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 @@ -27,13 +27,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.api.util.CancelableBag import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper import im.vector.matrix.android.internal.database.mapper.asDomain -import im.vector.matrix.android.internal.database.model.ChunkEntity -import im.vector.matrix.android.internal.database.model.ChunkEntityFields -import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity -import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.model.RoomEntity -import im.vector.matrix.android.internal.database.model.TimelineEventEntity -import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields +import im.vector.matrix.android.internal.database.model.* import im.vector.matrix.android.internal.database.query.FilterContent import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates import im.vector.matrix.android.internal.database.query.where @@ -44,16 +38,10 @@ import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.Debouncer import im.vector.matrix.android.internal.util.createBackgroundHandler import im.vector.matrix.android.internal.util.createUIHandler -import io.realm.OrderedCollectionChangeSet -import io.realm.OrderedRealmCollectionChangeListener -import io.realm.Realm -import io.realm.RealmConfiguration -import io.realm.RealmQuery -import io.realm.RealmResults -import io.realm.Sort +import io.realm.* import timber.log.Timber -import java.util.Collections -import java.util.UUID +import java.util.* +import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference import kotlin.collections.ArrayList @@ -77,11 +65,11 @@ internal class DefaultTimeline( private val hiddenReadReceipts: TimelineHiddenReadReceipts ) : Timeline, TimelineHiddenReadReceipts.Delegate { - private companion object { + companion object { val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD") } - private val listeners = ArrayList() + private val listeners = CopyOnWriteArrayList() private val isStarted = AtomicBoolean(false) private val isReady = AtomicBoolean(false) private val mainHandler = createUIHandler() @@ -113,11 +101,7 @@ internal class DefaultTimeline( if (!results.isLoaded || !results.isValid) { return@OrderedRealmCollectionChangeListener } - if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL) { - handleInitialLoad() - } else { - handleUpdates(changeSet) - } + handleUpdates(changeSet) } private val relationsListener = OrderedRealmCollectionChangeListener> { collection, changeSet -> @@ -179,8 +163,9 @@ internal class DefaultTimeline( nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING).findAll() filteredEvents = nonFilteredEvents.where() .filterEventsWithSettings() - .findAllAsync() - .also { it.addChangeListener(eventsChangeListener) } + .findAll() + handleInitialLoad() + filteredEvents.addChangeListener(eventsChangeListener) eventRelations = EventAnnotationsSummaryEntity.whereInRoom(realm, roomId) .findAllAsync() @@ -288,20 +273,20 @@ internal class DefaultTimeline( return hasMoreInCache(direction) || !hasReachedEnd(direction) } - override fun addListener(listener: Timeline.Listener) = synchronized(listeners) { + override fun addListener(listener: Timeline.Listener): Boolean { if (listeners.contains(listener)) { return false } - listeners.add(listener).also { + return listeners.add(listener).also { postSnapshot() } } - override fun removeListener(listener: Timeline.Listener) = synchronized(listeners) { - listeners.remove(listener) + override fun removeListener(listener: Timeline.Listener): Boolean { + return listeners.remove(listener) } - override fun removeAllListeners() = synchronized(listeners) { + override fun removeAllListeners() { listeners.clear() } @@ -402,14 +387,14 @@ internal class DefaultTimeline( private fun getState(direction: Timeline.Direction): State { return when (direction) { - Timeline.Direction.FORWARDS -> forwardsState.get() + Timeline.Direction.FORWARDS -> forwardsState.get() Timeline.Direction.BACKWARDS -> backwardsState.get() } } private fun updateState(direction: Timeline.Direction, update: (State) -> State) { val stateReference = when (direction) { - Timeline.Direction.FORWARDS -> forwardsState + Timeline.Direction.FORWARDS -> forwardsState Timeline.Direction.BACKWARDS -> backwardsState } val currentValue = stateReference.get() @@ -508,10 +493,10 @@ internal class DefaultTimeline( this.callback = object : MatrixCallback { override fun onSuccess(data: TokenChunkEventPersistor.Result) { when (data) { - TokenChunkEventPersistor.Result.SUCCESS -> { + TokenChunkEventPersistor.Result.SUCCESS -> { Timber.v("Success fetching $limit items $direction from pagination request") } - TokenChunkEventPersistor.Result.REACHED_END -> { + TokenChunkEventPersistor.Result.REACHED_END -> { postSnapshot() } TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE -> @@ -656,10 +641,8 @@ internal class DefaultTimeline( updateLoadingStates(filteredEvents) val snapshot = createSnapshot() val runnable = Runnable { - synchronized(listeners) { - listeners.forEach { - it.onTimelineUpdated(snapshot) - } + listeners.forEach { + it.onTimelineUpdated(snapshot) } } debouncer.debounce("post_snapshot", runnable, 50) @@ -671,10 +654,8 @@ internal class DefaultTimeline( return } val runnable = Runnable { - synchronized(listeners) { - listeners.forEach { - it.onTimelineFailure(throwable) - } + listeners.forEach { + it.onTimelineFailure(throwable) } } mainHandler.post(runnable) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetEventTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetEventTask.kt index 8e097c50d9..10c0f5003b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetEventTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetEventTask.kt @@ -20,9 +20,14 @@ import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus import javax.inject.Inject -internal class GetEventTask @Inject constructor(private val roomAPI: RoomAPI +// TODO Add parent task + +internal class GetEventTask @Inject constructor( + private val roomAPI: RoomAPI, + private val eventBus: EventBus ) : Task { internal data class Params( @@ -31,7 +36,7 @@ internal class GetEventTask @Inject constructor(private val roomAPI: RoomAPI ) override suspend fun execute(params: Params): Event { - return executeRequest { + return executeRequest(eventBus) { apiCall = roomAPI.getEvent(params.roomId, params.eventId) } } 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 7030509bfc..87c59e832b 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 @@ -18,17 +18,12 @@ package im.vector.matrix.android.internal.session.room.timeline import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.internal.database.helper.* -import im.vector.matrix.android.internal.database.helper.add -import im.vector.matrix.android.internal.database.helper.addOrUpdate -import im.vector.matrix.android.internal.database.helper.addStateEvent -import im.vector.matrix.android.internal.database.helper.deleteOnCascade 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.query.create import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.database.query.findAllIncludingEvents import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.session.user.UserEntityFactory import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.kotlin.createObject import timber.log.Timber @@ -37,7 +32,8 @@ import javax.inject.Inject /** * Insert Chunk in DB, and eventually merge with existing chunk event */ -internal class TokenChunkEventPersistor @Inject constructor(private val monarchy: Monarchy) { +internal class TokenChunkEventPersistor @Inject constructor(private val monarchy: Monarchy, + private val timelineEventSenderVisitor: TimelineEventSenderVisitor) { /** *
@@ -136,27 +132,22 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
 
                     // The current chunk is the one we will keep all along the merge processChanges.
                     // We try to look for a chunk next to the token,
-                    // otherwise we create a whole new one
+                    // otherwise we create a whole new one which is unlinked (not live)
 
                     var currentChunk = if (direction == PaginationDirection.FORWARDS) {
                         prevChunk?.apply { this.nextToken = nextToken }
                     } else {
                         nextChunk?.apply { this.prevToken = prevToken }
                     }
-                            ?: ChunkEntity.create(realm, prevToken, nextToken)
+                            ?: ChunkEntity.create(realm, prevToken, nextToken, isUnlinked = true)
 
                     if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) {
                         Timber.v("Reach end of $roomId")
                         currentChunk.isLastBackward = true
                     } else if (!shouldSkip) {
                         Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}")
-                        val eventIds = ArrayList(receivedChunk.events.size)
-                        for (event in receivedChunk.events) {
-                            event.eventId?.also { eventIds.add(it) }
-                            currentChunk.add(roomId, event, direction, isUnlinked = currentChunk.isUnlinked())
-                            UserEntityFactory.createOrNull(event)?.also {
-                                realm.insertOrUpdate(it)
-                            }
+                        val timelineEvents = receivedChunk.events.mapNotNull {
+                            currentChunk.add(roomId, it, direction)
                         }
                         // Then we merge chunks if needed
                         if (currentChunk != prevChunk && prevChunk != null) {
@@ -174,12 +165,9 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
                         }
                         roomEntity.addOrUpdate(currentChunk)
                         for (stateEvent in receivedChunk.stateEvents) {
-                            roomEntity.addStateEvent(stateEvent, isUnlinked = currentChunk.isUnlinked())
-                            UserEntityFactory.createOrNull(stateEvent)?.also {
-                                realm.insertOrUpdate(it)
-                            }
+                            roomEntity.addStateEvent(stateEvent, isUnlinked = currentChunk.isUnlinked)
                         }
-                        currentChunk.updateSenderDataFor(eventIds)
+                        timelineEventSenderVisitor.visit(timelineEvents)
                     }
                 }
         return if (receivedChunk.events.isEmpty()) {
@@ -200,11 +188,13 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
         // We always merge the bottom chunk into top chunk, so we are always merging backwards
         Timber.v("Merge ${currentChunk.prevToken} | ${currentChunk.nextToken} with ${otherChunk.prevToken} | ${otherChunk.nextToken}")
         return if (direction == PaginationDirection.BACKWARDS && !otherChunk.isLastForward) {
-            currentChunk.merge(roomEntity.roomId, otherChunk, PaginationDirection.BACKWARDS)
+            val events = currentChunk.merge(roomEntity.roomId, otherChunk, PaginationDirection.BACKWARDS)
+            timelineEventSenderVisitor.visit(events)
             roomEntity.deleteOnCascade(otherChunk)
             currentChunk
         } else {
-            otherChunk.merge(roomEntity.roomId, currentChunk, PaginationDirection.BACKWARDS)
+            val events = otherChunk.merge(roomEntity.roomId, currentChunk, PaginationDirection.BACKWARDS)
+            timelineEventSenderVisitor.visit(events)
             roomEntity.deleteOnCascade(currentChunk)
             otherChunk
         }
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/DefaultSignOutService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/DefaultSignOutService.kt
index 17c91011e7..68df456831 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/DefaultSignOutService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/DefaultSignOutService.kt
@@ -50,10 +50,10 @@ internal class DefaultSignOutService @Inject constructor(private val signOutTask
         }
     }
 
-    override fun signOut(sigOutFromHomeserver: Boolean,
+    override fun signOut(signOutFromHomeserver: Boolean,
                          callback: MatrixCallback): Cancelable {
         return signOutTask
-                .configureWith(SignOutTask.Params(sigOutFromHomeserver)) {
+                .configureWith(SignOutTask.Params(signOutFromHomeserver)) {
                     this.callback = callback
                 }
                 .executeBy(taskExecutor)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignInAgainTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignInAgainTask.kt
index 666852c988..0b8902e71b 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignInAgainTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignInAgainTask.kt
@@ -22,6 +22,7 @@ import im.vector.matrix.android.internal.auth.SessionParamsStore
 import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
 import im.vector.matrix.android.internal.network.executeRequest
 import im.vector.matrix.android.internal.task.Task
+import org.greenrobot.eventbus.EventBus
 import javax.inject.Inject
 
 internal interface SignInAgainTask : Task {
@@ -33,10 +34,12 @@ internal interface SignInAgainTask : Task {
 internal class DefaultSignInAgainTask @Inject constructor(
         private val signOutAPI: SignOutAPI,
         private val sessionParams: SessionParams,
-        private val sessionParamsStore: SessionParamsStore) : SignInAgainTask {
+        private val sessionParamsStore: SessionParamsStore,
+        private val eventBus: EventBus
+) : SignInAgainTask {
 
     override suspend fun execute(params: SignInAgainTask.Params) {
-        val newCredentials = executeRequest {
+        val newCredentials = executeRequest(eventBus) {
             apiCall = signOutAPI.loginAgain(
                     PasswordLoginParams.userIdentifier(
                             // Reuse the same userId
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 51cb22c988..9c31ce567b 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
@@ -32,6 +32,7 @@ import im.vector.matrix.android.internal.task.Task
 import im.vector.matrix.android.internal.worker.WorkManagerUtil
 import io.realm.Realm
 import io.realm.RealmConfiguration
+import org.greenrobot.eventbus.EventBus
 import timber.log.Timber
 import java.io.File
 import java.net.HttpURLConnection
@@ -43,25 +44,28 @@ internal interface SignOutTask : Task {
     )
 }
 
-internal class DefaultSignOutTask @Inject constructor(private val context: Context,
-                                                      @UserId private val userId: String,
-                                                      private val signOutAPI: SignOutAPI,
-                                                      private val sessionManager: SessionManager,
-                                                      private val sessionParamsStore: SessionParamsStore,
-                                                      @SessionDatabase private val clearSessionDataTask: ClearCacheTask,
-                                                      @CryptoDatabase private val clearCryptoDataTask: ClearCacheTask,
-                                                      @UserCacheDirectory private val userFile: File,
-                                                      private val realmKeysUtils: RealmKeysUtils,
-                                                      @SessionDatabase private val realmSessionConfiguration: RealmConfiguration,
-                                                      @CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration,
-                                                      @UserMd5 private val userMd5: String) : SignOutTask {
+internal class DefaultSignOutTask @Inject constructor(
+        private val context: Context,
+        @SessionId private val sessionId: String,
+        private val signOutAPI: SignOutAPI,
+        private val sessionManager: SessionManager,
+        private val sessionParamsStore: SessionParamsStore,
+        @SessionDatabase private val clearSessionDataTask: ClearCacheTask,
+        @CryptoDatabase private val clearCryptoDataTask: ClearCacheTask,
+        @UserCacheDirectory private val userFile: File,
+        private val realmKeysUtils: RealmKeysUtils,
+        @SessionDatabase private val realmSessionConfiguration: RealmConfiguration,
+        @CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration,
+        @UserMd5 private val userMd5: String,
+        private val eventBus: EventBus
+) : SignOutTask {
 
     override suspend fun execute(params: SignOutTask.Params) {
         // It should be done even after a soft logout, to be sure the deviceId is deleted on the
         if (params.sigOutFromHomeserver) {
             Timber.d("SignOut: send request...")
             try {
-                executeRequest {
+                executeRequest(eventBus) {
                     apiCall = signOutAPI.signOut()
                 }
             } catch (throwable: Throwable) {
@@ -79,13 +83,13 @@ internal class DefaultSignOutTask @Inject constructor(private val context: Conte
         }
 
         Timber.d("SignOut: release session...")
-        sessionManager.releaseSession(userId)
+        sessionManager.releaseSession(sessionId)
 
         Timber.d("SignOut: cancel pending works...")
         WorkManagerUtil.cancelAllWorks(context)
 
         Timber.d("SignOut: delete session params...")
-        sessionParamsStore.delete(userId)
+        sessionParamsStore.delete(sessionId)
 
         Timber.d("SignOut: clear session data...")
         clearSessionDataTask.execute(Unit)
@@ -97,8 +101,8 @@ internal class DefaultSignOutTask @Inject constructor(private val context: Conte
         userFile.deleteRecursively()
 
         Timber.d("SignOut: clear the database keys")
-        realmKeysUtils.clear(SessionModule.DB_ALIAS_PREFIX + userMd5)
-        realmKeysUtils.clear(CryptoModule.DB_ALIAS_PREFIX + userMd5)
+        realmKeysUtils.clear(SessionModule.getKeyAlias(userMd5))
+        realmKeysUtils.clear(CryptoModule.getKeyAlias(userMd5))
 
         // Sanity check
         if (BuildConfig.DEBUG) {
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 d5d8275127..a69dfda63f 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
@@ -27,16 +27,17 @@ import im.vector.matrix.android.internal.database.helper.*
 import im.vector.matrix.android.internal.database.model.ChunkEntity
 import im.vector.matrix.android.internal.database.model.EventEntityFields
 import im.vector.matrix.android.internal.database.model.RoomEntity
+import im.vector.matrix.android.internal.database.model.TimelineEventEntity
 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.where
 import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService
 import im.vector.matrix.android.internal.session.mapWithProgress
 import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
+import im.vector.matrix.android.internal.session.room.membership.RoomMemberEventHandler
 import im.vector.matrix.android.internal.session.room.read.FullyReadContent
 import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
 import im.vector.matrix.android.internal.session.sync.model.*
-import im.vector.matrix.android.internal.session.user.UserEntityFactory
 import io.realm.Realm
 import io.realm.kotlin.createObject
 import timber.log.Timber
@@ -46,7 +47,9 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                                                    private val roomSummaryUpdater: RoomSummaryUpdater,
                                                    private val roomTagHandler: RoomTagHandler,
                                                    private val roomFullyReadHandler: RoomFullyReadHandler,
-                                                   private val cryptoService: DefaultCryptoService) {
+                                                   private val cryptoService: DefaultCryptoService,
+                                                   private val roomMemberEventHandler: RoomMemberEventHandler,
+                                                   private val timelineEventSenderVisitor: TimelineEventSenderVisitor) {
 
     sealed class HandlingStrategy {
         data class JOINED(val data: Map) : HandlingStrategy()
@@ -121,9 +124,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                 roomEntity.addStateEvent(event, filterDuplicates = true, stateIndex = untimelinedStateIndex)
                 // Give info to crypto module
                 cryptoService.onStateEvent(roomId, event)
-                UserEntityFactory.createOrNull(event)?.also {
-                    realm.insertOrUpdate(it)
-                }
+                roomMemberEventHandler.handle(realm, roomId, event)
             }
         }
         if (roomSync.timeline != null && roomSync.timeline.events.isNotEmpty()) {
@@ -194,12 +195,13 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
         }
         lastChunk?.isLastForward = false
         chunkEntity.isLastForward = true
+        chunkEntity.isUnlinked = false
 
-        val eventIds = ArrayList(eventList.size)
+        val timelineEvents = ArrayList(eventList.size)
         for (event in eventList) {
-            event.ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
-            event.eventId?.also { eventIds.add(it) }
-            chunkEntity.add(roomEntity.roomId, event, PaginationDirection.FORWARDS, stateIndexOffset)
+            chunkEntity.add(roomEntity.roomId, event, PaginationDirection.FORWARDS, stateIndexOffset)?.also {
+                timelineEvents.add(it)
+            }
             // Give info to crypto module
             cryptoService.onLiveEvent(roomEntity.roomId, event)
             // Try to remove local echo
@@ -212,11 +214,9 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                     Timber.v("Can't find corresponding local echo for tx:$it")
                 }
             }
-            UserEntityFactory.createOrNull(event)?.also {
-                realm.insertOrUpdate(it)
-            }
+            roomMemberEventHandler.handle(realm, roomEntity.roomId, event)
         }
-        chunkEntity.updateSenderDataFor(eventIds)
+        timelineEventSenderVisitor.visit(timelineEvents)
         return chunkEntity
     }
 
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt
index d99b9df4df..b56594bd16 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt
@@ -25,6 +25,7 @@ import im.vector.matrix.android.internal.session.homeserver.GetHomeServerCapabil
 import im.vector.matrix.android.internal.session.sync.model.SyncResponse
 import im.vector.matrix.android.internal.session.user.UserStore
 import im.vector.matrix.android.internal.task.Task
+import org.greenrobot.eventbus.EventBus
 import timber.log.Timber
 import javax.inject.Inject
 
@@ -33,15 +34,17 @@ internal interface SyncTask : Task {
     data class Params(var timeout: Long = 30_000L)
 }
 
-internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI,
-                                                   @UserId private val userId: String,
-                                                   private val filterRepository: FilterRepository,
-                                                   private val syncResponseHandler: SyncResponseHandler,
-                                                   private val initialSyncProgressService: DefaultInitialSyncProgressService,
-                                                   private val syncTokenStore: SyncTokenStore,
-                                                   private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask,
-                                                   private val userStore: UserStore,
-                                                   private val syncTaskSequencer: SyncTaskSequencer
+internal class DefaultSyncTask @Inject constructor(
+        private val syncAPI: SyncAPI,
+        @UserId private val userId: String,
+        private val filterRepository: FilterRepository,
+        private val syncResponseHandler: SyncResponseHandler,
+        private val initialSyncProgressService: DefaultInitialSyncProgressService,
+        private val syncTokenStore: SyncTokenStore,
+        private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask,
+        private val userStore: UserStore,
+        private val syncTaskSequencer: SyncTaskSequencer,
+        private val eventBus: EventBus
 ) : SyncTask {
 
     override suspend fun execute(params: SyncTask.Params) = syncTaskSequencer.post {
@@ -70,7 +73,7 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI,
             initialSyncProgressService.endAll()
             initialSyncProgressService.startTask(R.string.initial_sync_start_importing_account, 100)
         }
-        val syncResponse = executeRequest {
+        val syncResponse = executeRequest(eventBus) {
             apiCall = syncAPI.sync(requestParams)
         }
         syncResponseHandler.handleResponse(syncResponse, token)
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 35988e6c6f..139a3297cd 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
@@ -20,7 +20,7 @@ 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.toModel
-import im.vector.matrix.android.api.session.room.model.RoomMember
+import im.vector.matrix.android.api.session.room.model.RoomMemberContent
 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.*
@@ -38,10 +38,11 @@ import io.realm.RealmList
 import timber.log.Timber
 import javax.inject.Inject
 
-internal class UserAccountDataSyncHandler @Inject constructor(private val monarchy: Monarchy,
-                                                              @UserId private val userId: String,
-                                                              private val directChatsHelper: DirectChatsHelper,
-                                                              private val updateUserAccountDataTask: UpdateUserAccountDataTask) {
+internal class UserAccountDataSyncHandler @Inject constructor(
+        private val monarchy: Monarchy,
+        @UserId private val userId: String,
+        private val directChatsHelper: DirectChatsHelper,
+        private val updateUserAccountDataTask: UpdateUserAccountDataTask) {
 
     fun handle(realm: Realm, accountData: UserAccountDataSync?) {
         accountData?.list?.forEach {
@@ -69,9 +70,9 @@ internal class UserAccountDataSyncHandler @Inject constructor(private val monarc
         var hasUpdate = false
         monarchy.doWithRealm { realm ->
             invites.forEach { (roomId, _) ->
-                val myUserStateEvent = RoomMembers(realm, roomId).getStateEvent(userId)
+                val myUserStateEvent = RoomMembers(realm, roomId).getLastStateEvent(userId)
                 val inviterId = myUserStateEvent?.sender
-                val myUserRoomMember: RoomMember? = myUserStateEvent?.let { it.asDomain().content?.toModel() }
+                val myUserRoomMember: RoomMemberContent? = myUserStateEvent?.let { it.asDomain().content?.toModel() }
                 val isDirect = myUserRoomMember?.isDirect
                 if (inviterId != null && inviterId != userId && isDirect == true) {
                     directChats
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt
index 37bcc225c1..724b0e1360 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt
@@ -39,7 +39,7 @@ import java.util.concurrent.atomic.AtomicBoolean
  */
 abstract class SyncService : Service() {
 
-    private var userId: String? = null
+    private var sessionId: String? = null
     private var mIsSelfDestroyed: Boolean = false
 
     private var isInitialSync: Boolean = false
@@ -58,18 +58,17 @@ abstract class SyncService : Service() {
         Timber.i("onStartCommand $intent")
         intent?.let {
             val matrix = Matrix.getInstance(applicationContext)
-            val safeUserId = it.getStringExtra(EXTRA_USER_ID) ?: return@let
-            val sessionComponent = matrix.sessionManager.getSessionComponent(safeUserId)
+            val safeSessionId = it.getStringExtra(EXTRA_SESSION_ID) ?: return@let
+            val sessionComponent = matrix.sessionManager.getSessionComponent(safeSessionId)
                     ?: return@let
             session = sessionComponent.session()
-            userId = safeUserId
+            sessionId = safeSessionId
             syncTask = sessionComponent.syncTask()
             isInitialSync = !session.hasAlreadySynced()
             networkConnectivityChecker = sessionComponent.networkConnectivityChecker()
             taskExecutor = sessionComponent.taskExecutor()
             coroutineDispatchers = sessionComponent.coroutineDispatchers()
             backgroundDetectionObserver = matrix.backgroundDetectionObserver
-            onStart(isInitialSync)
             if (isRunning.get()) {
                 Timber.i("Received a start while was already syncing... ignore")
             } else {
@@ -79,6 +78,7 @@ abstract class SyncService : Service() {
                 }
             }
         }
+        onStart(isInitialSync)
         // No intent just start the service, an alarm will should call with intent
         return START_STICKY
     }
@@ -101,7 +101,7 @@ abstract class SyncService : Service() {
     private suspend fun doSync() {
         if (!networkConnectivityChecker.hasInternetAccess()) {
             Timber.v("No network reschedule to avoid wasting resources")
-            userId?.also {
+            sessionId?.also {
                 onRescheduleAsked(it, isInitialSync, delay = 10_000L)
             }
             stopMe()
@@ -112,7 +112,7 @@ abstract class SyncService : Service() {
         try {
             syncTask.execute(params)
             // Start sync if we were doing an initial sync and the syncThread is not launched yet
-            if (isInitialSync && session.syncState().value == SyncState.Idle) {
+            if (isInitialSync && session.getSyncStateLive().value == SyncState.Idle) {
                 val isForeground = !backgroundDetectionObserver.isInBackground
                 session.startSync(isForeground)
             }
@@ -131,14 +131,14 @@ abstract class SyncService : Service() {
 
     abstract fun onStart(isInitialSync: Boolean)
 
-    abstract fun onRescheduleAsked(userId: String, isInitialSync: Boolean, delay: Long)
+    abstract fun onRescheduleAsked(sessionId: String, isInitialSync: Boolean, delay: Long)
 
     override fun onBind(intent: Intent?): IBinder? {
         return null
     }
 
     companion object {
-        const val EXTRA_USER_ID = "EXTRA_USER_ID"
+        const val EXTRA_SESSION_ID = "EXTRA_SESSION_ID"
         private const val TIME_OUT = 0L
         private const val DELAY_FAILURE = 5_000L
     }
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt
index eb4f2ff7c2..3637cc624f 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt
@@ -22,6 +22,7 @@ import im.vector.matrix.android.api.failure.isTokenError
 import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
 import im.vector.matrix.android.internal.session.sync.SyncTask
 import im.vector.matrix.android.internal.task.TaskExecutor
+import im.vector.matrix.android.internal.worker.SessionWorkerParams
 import im.vector.matrix.android.internal.worker.WorkManagerUtil
 import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder
 import im.vector.matrix.android.internal.worker.WorkerParamsFactory
@@ -38,10 +39,11 @@ internal class SyncWorker(context: Context,
 
     @JsonClass(generateAdapter = true)
     internal data class Params(
-            val userId: String,
+            override val sessionId: String,
             val timeout: Long = DEFAULT_LONG_POOL_TIMEOUT,
-            val automaticallyRetry: Boolean = false
-    )
+            val automaticallyRetry: Boolean = false,
+            override val lastFailureMessage: String? = null
+    ) : SessionWorkerParams
 
     @Inject lateinit var syncTask: SyncTask
     @Inject lateinit var taskExecutor: TaskExecutor
@@ -50,7 +52,7 @@ internal class SyncWorker(context: Context,
     override suspend fun doWork(): Result {
         Timber.i("Sync work starting")
         val params = WorkerParamsFactory.fromData(inputData) ?: return Result.success()
-        val sessionComponent = getSessionComponent(params.userId) ?: return Result.success()
+        val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
         sessionComponent.inject(this)
         return runCatching {
             doSync(params.timeout)
@@ -75,8 +77,8 @@ internal class SyncWorker(context: Context,
 
         const val BG_SYNC_WORK_NAME = "BG_SYNCP"
 
-        fun requireBackgroundSync(context: Context, userId: String, serverTimeout: Long = 0) {
-            val data = WorkerParamsFactory.toData(Params(userId, serverTimeout, false))
+        fun requireBackgroundSync(context: Context, sessionId: String, serverTimeout: Long = 0) {
+            val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, false))
             val workRequest = matrixOneTimeWorkRequestBuilder()
                     .setConstraints(WorkManagerUtil.workConstraints)
                     .setBackoffCriteria(BackoffPolicy.LINEAR, 1_000, TimeUnit.MILLISECONDS)
@@ -85,8 +87,8 @@ internal class SyncWorker(context: Context,
             WorkManager.getInstance(context).enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.REPLACE, workRequest)
         }
 
-        fun automaticallyBackgroundSync(context: Context, userId: String, serverTimeout: Long = 0, delay: Long = 30_000) {
-            val data = WorkerParamsFactory.toData(Params(userId, serverTimeout, true))
+        fun automaticallyBackgroundSync(context: Context, sessionId: String, serverTimeout: Long = 0, delay: Long = 30_000) {
+            val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, true))
             val workRequest = matrixOneTimeWorkRequestBuilder()
                     .setConstraints(WorkManagerUtil.workConstraints)
                     .setInputData(data)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt
index d314c8d108..761c810b41 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt
@@ -70,7 +70,7 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona
         return userEntity.asDomain()
     }
 
-    override fun liveUser(userId: String): LiveData> {
+    override fun getUserLive(userId: String): LiveData> {
         val liveData = monarchy.findAllMappedWithChanges(
                 { UserEntity.where(it, userId) },
                 { it.asDomain() }
@@ -80,7 +80,7 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona
         }
     }
 
-    override fun liveUsers(): LiveData> {
+    override fun getUsersLive(): LiveData> {
         return monarchy.findAllMappedWithChanges(
                 { realm ->
                     realm.where(UserEntity::class.java)
@@ -91,7 +91,7 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona
         )
     }
 
-    override fun livePagedUsers(filter: String?): LiveData> {
+    override fun getPagedUsersLive(filter: String?): LiveData> {
         realmDataSourceFactory.updateQuery { realm ->
             val query = realm.where(UserEntity::class.java)
             if (filter.isNullOrEmpty()) {
@@ -121,7 +121,7 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona
                 .executeBy(taskExecutor)
     }
 
-    override fun liveIgnoredUsers(): LiveData> {
+    override fun getIgnoredUsersLive(): LiveData> {
         return monarchy.findAllMappedWithChanges(
                 { realm ->
                     realm.where(IgnoredUserEntity::class.java)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserEntityFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserEntityFactory.kt
index 2ded32b7db..f931db1cff 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserEntityFactory.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserEntityFactory.kt
@@ -16,27 +16,16 @@
 
 package im.vector.matrix.android.internal.session.user
 
-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.Membership
-import im.vector.matrix.android.api.session.room.model.RoomMember
+import im.vector.matrix.android.api.session.room.model.RoomMemberContent
 import im.vector.matrix.android.internal.database.model.UserEntity
 
 internal object UserEntityFactory {
 
-    fun createOrNull(event: Event): UserEntity? {
-        if (event.type != EventType.STATE_ROOM_MEMBER) {
-            return null
-        }
-        val roomMember = event.content.toModel() ?: return null
-        // We only use JOIN and INVITED memberships to create User data
-        if (roomMember.membership != Membership.JOIN && roomMember.membership != Membership.INVITE) {
-            return null
-        }
-        return UserEntity(event.stateKey ?: "",
-                roomMember.displayName ?: "",
-                roomMember.avatarUrl ?: ""
+    fun create(userId: String, roomMember: RoomMemberContent): UserEntity {
+        return UserEntity(
+                userId = userId,
+                displayName = roomMember.displayName ?: "",
+                avatarUrl = roomMember.avatarUrl ?: ""
         )
     }
 }
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateIgnoredUserIdsTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateIgnoredUserIdsTask.kt
index 075eeb23d1..55b26f203f 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateIgnoredUserIdsTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateIgnoredUserIdsTask.kt
@@ -23,6 +23,7 @@ import im.vector.matrix.android.internal.network.executeRequest
 import im.vector.matrix.android.internal.session.sync.model.accountdata.IgnoredUsersContent
 import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
 import im.vector.matrix.android.internal.task.Task
+import org.greenrobot.eventbus.EventBus
 import javax.inject.Inject
 
 internal interface UpdateIgnoredUserIdsTask : Task {
@@ -33,10 +34,13 @@ internal interface UpdateIgnoredUserIdsTask : Task {
+        executeRequest(eventBus) {
             apiCall = accountDataApi.setAccountData(userId, UserAccountData.TYPE_IGNORED_USER_LIST, body)
         }
 
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 4c4f40add5..068ce4777a 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
@@ -21,6 +21,7 @@ import im.vector.matrix.android.internal.network.executeRequest
 import im.vector.matrix.android.internal.session.sync.model.accountdata.BreadcrumbsContent
 import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
 import im.vector.matrix.android.internal.task.Task
+import org.greenrobot.eventbus.EventBus
 import javax.inject.Inject
 
 internal interface UpdateUserAccountDataTask : Task {
@@ -50,11 +51,14 @@ internal interface UpdateUserAccountDataTask : Task> {
@@ -31,10 +32,13 @@ internal interface SearchUserTask : Task> {
     )
 }
 
-internal class DefaultSearchUserTask @Inject constructor(private val searchUserAPI: SearchUserAPI) : SearchUserTask {
+internal class DefaultSearchUserTask @Inject constructor(
+        private val searchUserAPI: SearchUserAPI,
+        private val eventBus: EventBus
+) : SearchUserTask {
 
     override suspend fun execute(params: SearchUserTask.Params): List {
-        val response = executeRequest {
+        val response = executeRequest(eventBus) {
             apiCall = searchUserAPI.searchUsers(SearchUsersParams(params.search, params.limit))
         }
         return response.users.map {
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/SessionWorkerParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/SessionWorkerParams.kt
index 0c53a3ef0b..c05367cf10 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/SessionWorkerParams.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/SessionWorkerParams.kt
@@ -17,7 +17,7 @@
 package im.vector.matrix.android.internal.worker
 
 interface SessionWorkerParams {
-    val userId: String
+    val sessionId: String
 
     // Null is no error occurs. When chaining Workers, first step is to check that there is no lastFailureMessage from the previous workers
     val lastFailureMessage: String?
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/Worker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/Worker.kt
index 58abd10e81..88680786ad 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/Worker.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/Worker.kt
@@ -20,6 +20,6 @@ import androidx.work.ListenableWorker
 import im.vector.matrix.android.api.Matrix
 import im.vector.matrix.android.internal.session.SessionComponent
 
-internal fun ListenableWorker.getSessionComponent(userId: String): SessionComponent? {
-    return Matrix.getInstance(applicationContext).sessionManager.getSessionComponent(userId)
+internal fun ListenableWorker.getSessionComponent(sessionId: String): SessionComponent? {
+    return Matrix.getInstance(applicationContext).sessionManager.getSessionComponent(sessionId)
 }
diff --git a/matrix-sdk-android/src/main/res/values-az/strings.xml b/matrix-sdk-android/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000000..c4347619d0
--- /dev/null
+++ b/matrix-sdk-android/src/main/res/values-az/strings.xml
@@ -0,0 +1,189 @@
+
+
+    %1$s: %2$s
+    %1$s şəkil göndərdi.
+    %1$s stiker göndərdi.
+
+    %s-nin dəvəti
+    %1$s dəvət etdi %2$s
+    %1$s sizi dəvət etdi
+    %1$s qoşuldu
+    %1$s qalıb
+    %1$s dəvəti rədd etdi
+    %1$s %2$s-i xaric etdi
+    %1$s %2$s-i blokdan açdı
+    %1$s %2$s-i blokladı
+    %1$s %2$s-in dəvətini geri götürdü
+    %1$s avatarı dəyişdi
+    %1$s ekran adını %2$s olaraq təyin etdi
+    %1$s ekran adını %2$s-dan %3$s-ya dəyişdi
+    %1$s onların göstərilən adlarını sildi (%2$s)
+    %1$s mövzunu dəyişdi: %2$s
+    %1$s otaq adını dəyişdirdi: %2$s
+    %s video zəng etdi.
+    %s səsli zəng etdi.
+    %s zəngə cavab verdi.
+    %s zəng başa çatdı.
+    "%1$s gələcək otaq tarixçəsini %2$s-ə  görünən etdi"
+    bütün otaq üzvləri, dəvət olunduğu andan.
+    bütün otaq üzvləri, qoşulduğu andan.
+    bütün otaq üzvləri.
+    hər kəs.
+    naməlum (%s).
+    %1$s sondan-sona şifrələmə açdı (%2$s)
+    %s bu otağı təkmilləşdirdi.
+
+    %1$s VoIP konfrans istədi
+    VoIP konfransı başladı
+    VoIP konfransı başa çatdı
+
+    (avatar da dəyişdirilib)
+    %1$s otaq adını sildi
+    %1$s otaq mövzusunu sildi
+    Mesaj silindi
+    Mesaj %1$s tərəfindən silindi
+    Mesaj silindi [səbəb: %1$s]
+    Mesaj %1$s tərəfindən qaldırıldı [səbəb: %2$s]
+    %1$s profilini %2$s yenilədi
+    %1$s otağa qoşulmaq üçün %2$s dəvətnamə göndərdi
+    %1$s otağa qoşulmaq üçün %2$s dəvətini ləğv etdi
+    %1$s %2$s üçün dəvəti qəbul etdi
+
+    ** Şifrəni aça bilmir: %s **
+    Göndərənin cihazı bu mesaj üçün açarları bizə göndərməyib.
+
+    Cavab olaraq
+
+    Redaktə etmək olmur
+    Mesaj göndərmək olmur
+
+    Şəkil yükləmək olmur
+
+    Şəbəkə xətası
+    Matris xətası
+
+    Boş bir otağa yenidən qoşulmaq hazırda mümkün deyil.
+
+    Şifrəli mesaj
+
+    Elektron poçt ünvanı
+    Telefon nömrəsi
+
+    şəkil göndərdi.
+    video göndərdi.
+    səs faylı göndərdi.
+    fayl göndərdi.
+
+    %s-dən dəvət
+    Otağa dəvət
+
+    %1$s və %2$s
+
+    
+        %1$s və 1 digər
+        %1$s və %2$d digərləri
+    
+
+    Boş otaq
+
+
+    It
+    Pişik
+    Aslan
+    At
+    Kərgədan
+    Donuz
+    Fil
+    Dovşan
+    Panda
+    Xoruz
+    Pinqvin
+    Tısbağa
+    Balıq
+    Ahtapot
+    Kəpənək
+    Çiçək
+    Ağac
+    Kaktus
+    Göbələk
+    Qlobus
+    Ay
+    Bulud
+    Atəş
+    Banan
+    Alma
+    Çiyələk
+    Qarğıdalı
+    Pizza
+    Tort
+    Ürək
+    Təbəssüm
+    Robot
+    Papaq
+    Eynəklər
+    Açar
+    Santa
+    Baş barmaqlar yuxarı
+    Çətir
+    Qum saatı
+    Saat
+    Hədiyyə
+    Lampa
+    Kitab
+    Qələm
+    Kağız sancağı
+    Qayçı
+    Qıfıl
+    Açar
+    Çəkic
+    Telefon
+    Bayraq
+    Qatar
+    Velosiped
+    Təyyarə
+    Raket
+    Kubok
+    Top
+    Gitara
+    Saz
+    Zəng
+    Anker
+    Qulaqlıqlar
+    Qovluq
+    Sancaq
+
+    İlkin sinxronizasiya:
+\nHesab idxal olunur…
+    İlkin sinxronizasiya:
+\nKriptografiyanın idxalı
+    İlkin sinxronizasiya:
+\nOtaqlar idxalı
+    İlkin sinxronizasiya:
+\nOtaqlara daxil olmaq
+    İlkin sinxronizasiya:
+\nDəvət olunmuş otaqların idxalı
+    İlkin sinxronizasiya:
+\nTərk olunmuş otaqların idxalı
+    İlkin sinxronizasiya:
+\nİcmaların idxalı
+    İlkin sinxronizasiya:
+\nHesab məlumatlarının idxalı
+
+    Mesaj göndərilir…
+    Göndərmə növbəsini təmizləyin
+
+    %1$s-nin dəvəti. Səbəb: %2$s
+    %1$s dəvət olunmuş %2$s. Səbəb: %3$s
+    %1$s sizi dəvət etdi. Səbəb: %2$s
+    %1$s qoşuldu. Səbəb: %2$s
+    %1$s qalıb. Səbəb: %2$s
+    %1$s dəvəti rədd etdi. Səbəb: %2$s
+    %1$s %2$s-i xaric etdi. Səbəb: %3$s
+    %1$s blokdan açdı %2$s. Səbəb: %3$s
+    %1$s blokladı %2$s. Səbəb: %3$s
+    %1$s otağa qoşulmaq üçün %2$s dəvətnamə göndərdi. Səbəb: %3$s
+    %1$s otağa qoşulmaq üçün %2$s dəvətini ləğv etdi. Səbəb: %3$s
+    %1$s %2$s üçün dəvəti qəbul etdi. Səbəb: %3$s
+    %1$s %2$s dəvətini geri götürdü. Səbəb: %3$s
+
+
diff --git a/matrix-sdk-android/src/main/res/values-bg/strings.xml b/matrix-sdk-android/src/main/res/values-bg/strings.xml
index 2566ee780c..1b296074c6 100644
--- a/matrix-sdk-android/src/main/res/values-bg/strings.xml
+++ b/matrix-sdk-android/src/main/res/values-bg/strings.xml
@@ -173,4 +173,18 @@
     Изчисти опашката за изпращане
 
     %1$s оттегли поканата за присъединяване на %2$s към стаята
+    поканата на %1$s. Причина: %2$s
+    %1$s покани %2$s. Причина: %3$s
+    %1$s ви покани. Причина: %2$s
+    %1$s се присъедини. Причина: %2$s
+    %1$s напусна. Причина: %2$s
+    %1$s отхвърли поканата. Причина: %2$s
+    %1$s изгони %2$s. Причина: %3$s
+    %1$s блокира %2$s. Причина: %3$s
+    %1$s блокира %2$s. Причина: %3$s
+    %1$s изпрати покана до %2$s да се присъедини в стаята. Причина: %3$s
+    %1$s премахна поканата за присъединяване на %2$s в стаята. Причина: %3$s
+    %1$s прие поканата за %2$s. Причина: %3$s
+    %1$s оттегли поканата на %2$s. Причина: %3$s
+
 
diff --git a/matrix-sdk-android/src/main/res/values-de/strings.xml b/matrix-sdk-android/src/main/res/values-de/strings.xml
index 59b5ee4212..fa66f96aba 100644
--- a/matrix-sdk-android/src/main/res/values-de/strings.xml
+++ b/matrix-sdk-android/src/main/res/values-de/strings.xml
@@ -176,4 +176,18 @@
     Erste Synchronisation: Importiere Benutzerdaten
 
     %1$s hat die Einladung an %2$s, den Raum zu betreten, zurückgezogen
+    %1$s\'s Einladung. Grund: %2$s
+    %1$s hat %2$s eingeladen. Grund: %3$s
+    %1$s hat dich eingeladen. Grund: %2$s
+    %1$s beigetreten. Grund: %2$s
+    %1$s ging. Grund: %2$s
+    %1$s hat die Einladung abgelehnt. Grund: %2$s
+    %1$s hat %2$s gekickt. Grund: %3$s
+    %1$s hat Verbannung für %2$s aufgehoben. Grund: %3$s
+    %1$s hat %2$s verbannt. Grund: %3$s
+    %1$s hat eine Einladung an %2$s gesandt um diesem Raum beizutreten. Grund: %3$s
+    %1$s hat Einladung an %2$s zu Betreten dieses Raumes zurückgezogen. Grund: %3$s
+    %1$s hat die Einladung für %2$s angenommen. Grund: %3$s
+    %1$s hat Einladung für %2$s verworfen. Grund: %3$s
+
 
diff --git a/matrix-sdk-android/src/main/res/values-eu/strings.xml b/matrix-sdk-android/src/main/res/values-eu/strings.xml
index 5b36858253..acbfcd3a97 100644
--- a/matrix-sdk-android/src/main/res/values-eu/strings.xml
+++ b/matrix-sdk-android/src/main/res/values-eu/strings.xml
@@ -173,4 +173,18 @@
     Garbitu bidalketa-ilara
 
     %1$s erabiltzaileak %2$s gelara elkartzeko gonbidapena indargabetu du
+    %1$s erabiltzailearen gonbidapena. Arrazoia: %2$s
+    %1$s erabiltzaileak %2$s gonbidatu du. Arrazoia: %3$s
+    %1$s erabiltzaileak gonbidatu zaitu. Arrazoia: %2$s
+    %1$s elkartu da. Arrazoia: %2$s
+    %1$s atera da. Arrazoia: %2$s
+    %1$s erabiltzaileak gonbidapena baztertu du. Arrazoia: %2$s
+    %1$s erabiltzaileak %2$s kanporatu du. Arrazoia: %3$s
+    %1$s erabiltzaileak debekua kendu dio %2$s erabiltzaileari. Arrazoia: %3$s
+    %1$s erabiltzaileak %2$s debekatu du. Arrazoia: %3$s
+    "%1$s erabiltzaileak gelara elkartzeko gonbidapen bat bidali dio %2$s erabiltzaileari. Arrazoia: %3$s"
+    "%1$s erabiltzaileak %2$s gelara elkartzeko gonbidapena indargabetu du. Arrazoia: %3$s"
+    "%1$s erabiltzaileak %2$s gelarako gonbidapena onartu du. Arrazoia: %3$s"
+    "%1$s erabiltzaileak %2$s erabiltzailearen gonbidapena indargabetu du. Arrazoia: %3$s"
+
 
diff --git a/matrix-sdk-android/src/main/res/values-fi/strings.xml b/matrix-sdk-android/src/main/res/values-fi/strings.xml
index 3c02e5c2ce..b101b91736 100644
--- a/matrix-sdk-android/src/main/res/values-fi/strings.xml
+++ b/matrix-sdk-android/src/main/res/values-fi/strings.xml
@@ -174,4 +174,18 @@
     Tyhjennä lähetysjono
 
     %1$s veti takaisin käyttäjän %2$s liittymiskutsun huoneeseen
+    Henkilön %1$s kutsu. Syy: %2$s
+    %1$s kutsui henkilön %2$s. Syy: %3$s
+    %1$s kutsui sinut. Syy: %2$s
+    %1$s liittyi. Syy: %2$s
+    %1$s poistui. Syy: %2$s
+    %1$s hylkäsi kutsun. Syy: %2$s
+    %1$s potkaisi käyttäjän %2$s pois. Syy: %3$s
+    %1$s poisti eston käyttäjältä %2$s. Syy: %3$s
+    %1$s esti käyttäjän %2$s. Syy: %3$s
+    %1$s lähetti kutsun käyttäjälle %2$s huoneeseen liittymiseksi. Syy: %3$s
+    %1$s kumosi kutsun käyttäjälle %2$s huoneeseen liittymiseksi. Syy: %3$s
+    %1$s hyväksyi kutsun liityäkseen huoneeseen %2$s. Syy: %3$s
+    %1$s poisti käyttäjän %2$s kutsun. Syy: %3$s
+
 
diff --git a/matrix-sdk-android/src/main/res/values-fr/strings.xml b/matrix-sdk-android/src/main/res/values-fr/strings.xml
index 98a98a3e7a..01f0cfae0f 100644
--- a/matrix-sdk-android/src/main/res/values-fr/strings.xml
+++ b/matrix-sdk-android/src/main/res/values-fr/strings.xml
@@ -173,4 +173,18 @@
     Vider la file d’envoi
 
     %1$s a révoqué l’invitation pour %2$s à rejoindre le salon
+    Invitation de %1$s. Raison : %2$s
+    %1$s a invité %2$s. Raison : %3$s
+    %1$s vous a invité. Raison : %2$s
+    %1$s a rejoint le salon. Raison : %2$s
+    %1$s est parti. Raison : %2$s
+    %1$s a refusé l’invitation. Raison : %2$s
+    %1$s a expulsé %2$s. Raison : %3$s
+    %1$s a révoqué le bannissement de %2$s. Raison : %3$s
+    %1$s a banni %2$s. Raison : %3$s
+    %1$s a envoyé une invitation à %2$s pour rejoindre le salon. Raison : %3$s
+    %1$s a révoqué l’invitation de %2$s à rejoindre le salon. Raison : %3$s
+    %1$s a accepté l’invitation pour %2$s. Raison : %3$s
+    %1$s a annulé l’invitation de %2$s. Raison : %3$s
+
 
diff --git a/matrix-sdk-android/src/main/res/values-hu/strings.xml b/matrix-sdk-android/src/main/res/values-hu/strings.xml
index da6b8f5687..2294661621 100644
--- a/matrix-sdk-android/src/main/res/values-hu/strings.xml
+++ b/matrix-sdk-android/src/main/res/values-hu/strings.xml
@@ -172,4 +172,18 @@
     Küldő sor ürítése
 
     %1$s visszavonta a meghívót a belépéshez ebbe a szobába: %2$s
+    %1$s meghívója. Ok: %2$s
+    %1$s meghívta őt: %2$s. Ok: %3$s
+    %1$s meghívott. Ok: %2$s
+    %1$s csatlakozott. Ok: %2$s
+    %1$s kilépett. Ok: %2$s
+    %1$s visszautasította a meghívót. Ok: %2$s
+    %1$s kirúgta őt: %2$s. Ok: %3$s
+    %1$s visszaengedte őt: %2$s. Ok: %3$s
+    %1$s kitiltotta őt: %2$s. Ok: %3$s
+    %1$s meghívót küldött neki: %2$s, hogy lépjen be a szobába. Ok: %3$s
+    %1$s visszavonta %2$s meghívóját a szobába való belépéshez. Ok: %3$s
+    %1$s elfogadta a meghívót ide: %2$s. Ok: %3$s
+    %1$s visszavonta %2$s meghívóját. Ok: %3$s
+
 
diff --git a/matrix-sdk-android/src/main/res/values-it/strings.xml b/matrix-sdk-android/src/main/res/values-it/strings.xml
index a8d844ddde..585ec47703 100644
--- a/matrix-sdk-android/src/main/res/values-it/strings.xml
+++ b/matrix-sdk-android/src/main/res/values-it/strings.xml
@@ -173,4 +173,18 @@
     Cancella la coda di invio
 
     %1$s ha revocato l\'invito a %2$s di unirsi alla stanza
+    Invito di %1$s. Motivo: %2$s
+    %1$s ha invitato %2$s. Motivo: %3$s
+    %1$s ti ha invitato. Motivo: %2$s
+    %1$s è entrato. Motivo: %2$s
+    %1$s è uscito. Motivo: %2$s
+    %1$s ha rifiutato l\'invito. Motivo: %2$s
+    %1$s ha buttato fuori %2$s. Motivo: %3$s
+    %1$s ha riammesso %2$s. Motivo: %3$s
+    %1$s ha bandito %2$s. Motivo: %3$s
+    %1$s ha inviato un invito a %2$s di unirsi alla stanza. Motivo: %3$s
+    %1$s ha revocato l\'invito a %2$s di unirsi alla stanza. Motivo: %3$s
+    %1$s ha accettato l\'invito per %2$s. Motivo: %3$s
+    %1$s ha rifiutato l\'invito di %2$s. Motivo: %3$s
+
 
diff --git a/matrix-sdk-android/src/main/res/values-ja/strings.xml b/matrix-sdk-android/src/main/res/values-ja/strings.xml
index d2a051caeb..b72d1a13ca 100644
--- a/matrix-sdk-android/src/main/res/values-ja/strings.xml
+++ b/matrix-sdk-android/src/main/res/values-ja/strings.xml
@@ -40,7 +40,7 @@
     部屋のメンバー全員。
     誰でも。
     不明 (%s)。
-    %1$sはエンドツーエンドの暗号化を有効にしました (%2$s)
+    %1$s がエンドツーエンド暗号化を有効にしました (%2$s)
 
     %1$s がVoIP会議をリクエストしました
     VoIP会議が開始されました
diff --git a/matrix-sdk-android/src/main/res/values-ru/strings.xml b/matrix-sdk-android/src/main/res/values-ru/strings.xml
index 07411c097f..b0f2a60ac0 100644
--- a/matrix-sdk-android/src/main/res/values-ru/strings.xml
+++ b/matrix-sdk-android/src/main/res/values-ru/strings.xml
@@ -132,7 +132,7 @@
     Робот
     Шляпа
     Очки
-    гаечный ключ
+    Гаечный ключ
     Санта
     Большой палец вверх
     Зонтик
@@ -186,4 +186,18 @@
     Очистить очередь отправки
 
     %1$s отозвал приглашение %2$s присоединиться к комнате
+    Приглашение %1$s. Причина: %2$s
+    %1$s приглашен %2$s. Причина: %3$s
+    %1$s пригласил вас. Причина: %2$s
+    %1$s присоединился. Причина: %2$s
+    Осталось %1$s. Причина: %2$s
+    %1$s отклонил приглашение. Причина: %2$s
+    %1$s выгнали %2$s. Причина: %3$s
+    %1$s разблокировано %2$s. Причина: %3$s
+    %1$s забанен %2$s. Причина: %3$s
+    %1$s отправил приглашение %2$s в комнату. Причина: %3$s
+    %1$s отозвал приглашение %2$s присоединиться к комнате. Причина: %3$s
+    %1$s принял приглашение для %2$s. Причина: %3$s
+    %1$s отозвал приглашение %2$s. Причина: %3$s
+
 
diff --git a/matrix-sdk-android/src/main/res/values-sq/strings.xml b/matrix-sdk-android/src/main/res/values-sq/strings.xml
index cffd55a7f7..bd6993fab1 100644
--- a/matrix-sdk-android/src/main/res/values-sq/strings.xml
+++ b/matrix-sdk-android/src/main/res/values-sq/strings.xml
@@ -169,4 +169,18 @@
     Spastro radhë pritjeje
 
     %1$s shfuqizoi ftesën për %2$s për pjesëmarrje te dhoma
+    Ftesë e %1$s. Arsye: %2$s
+    %1$s ftoi %2$s. Arsye: %3$s
+    %1$s ju ftoi. Arsye: %2$s
+    %1$s erdhi. Arsye: %2$s
+    %1$s iku. Arsye: %2$s
+    %1$s hodhi poshtë ftesën. Arsye: %2$s
+    %1$s përzuri %2$s. Arsye: %3$s
+    %1$s hoqi dëbimin për %2$s. Arsye: %3$s
+    %1$s dëboi %2$s. Arsye: %3$s
+    %1$s dërgoi një ftesë për %2$s për të ardhur në dhomë. Arsye: %3$s
+    %1$s shfuqizoi ftesën për %2$s për të ardhur në dhomë. Arsye: %3$s
+    %1$s pranoi ftesën për %2$s. Arsye: %3$s
+    %1$s tërhoqi mbrapsht ftesën për %2$s. Arsye: %3$s
+
 
diff --git a/matrix-sdk-android/src/main/res/values-uk/strings.xml b/matrix-sdk-android/src/main/res/values-uk/strings.xml
index 9f8e9078a4..bf83e39d72 100644
--- a/matrix-sdk-android/src/main/res/values-uk/strings.xml
+++ b/matrix-sdk-android/src/main/res/values-uk/strings.xml
@@ -32,7 +32,7 @@
     %1$s змінив(ла) назву кімнати на: %2$s
     %s розпочав(ла) відеодзвінок.
     %s розпочав(ла) голосовий дзвінок.
-    %s віпдовів(ла) на дзвінок.
+    %s відповів(ла) на дзвінок.
     %s завершив(ла) дзвінок.
     %1$s зробив(ла) майбутню історію кімнати видимою для %2$s
     усіх співрозмовників, з моменту їх запрошення.
@@ -80,7 +80,12 @@
         %1$s та 1 інший
         %1$s та %2$d інші
         %1$s та %2$d інших
-        
+        
     
 
+    %s вдосконалили цю кімнату.
+
+    Повідомлення видалено
+    %1$s видалили повідомлення
+    Повідомлення видалено [причина: %1$s]
 
diff --git a/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml b/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml
index 5b5ae3beb0..e60bfd564d 100644
--- a/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml
+++ b/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml
@@ -171,4 +171,18 @@
     清除傳送佇列
 
     %1$s 撤銷了 %2$s 加入聊天室的邀請
+    %1$s 的邀請。理由:%2$s
+    %1$s 邀請了 %2$s。理由:%3$s
+    %1$s 邀請了您。理由:%2$s
+    %1$s 已加入。理由:%2$s
+    %1$s 已離開。理由:%2$s
+    %1$s 已回絕邀請。理由:%2$s
+    %1$s 踢走了 %2$s。理由:%3$s
+    %1$s 取消封鎖了 %2$s。理由:%3$s
+    %1$s 封鎖了 %2$s。理由:%3$s
+    %1$s 已傳送邀請給 %2$s 來加入聊天室。理由:%3$s
+    %1$s 撤銷了 %2$s 加入聊天室的邀請。理由:%3$s
+    %1$s 接受 %2$s 的邀請。理由:%3$s
+    %1$s 撤回了對 %2$s 的邀請。理由:%3$s
+
 
diff --git a/matrix-sdk-android/src/main/res/values/strings.xml b/matrix-sdk-android/src/main/res/values/strings.xml
index e611ae25b0..c0506dd549 100644
--- a/matrix-sdk-android/src/main/res/values/strings.xml
+++ b/matrix-sdk-android/src/main/res/values/strings.xml
@@ -257,4 +257,19 @@
     %1$s accepted the invitation for %2$s. Reason: %3$s
     %1$s withdrew %2$s\'s invitation. Reason: %3$s
 
+    
+        %1$s added %2$s as an address for this room.
+        %1$s added %2$s as addresses for this room.
+    
+
+    
+        %1$s removed %2$s as an address for this room.
+        %1$s removed %3$s as addresses for this room.
+    
+
+    %1$s added %2$s and removed %3$s as addresses for this room.
+
+    "%1$s set the main address for this room to %2$s."
+    "%1$s removed the main address for this room."
+
 
diff --git a/matrix-sdk-android/src/main/res/values/strings_RiotX.xml b/matrix-sdk-android/src/main/res/values/strings_RiotX.xml
index 2edfd0ba03..d781ec5f1e 100644
--- a/matrix-sdk-android/src/main/res/values/strings_RiotX.xml
+++ b/matrix-sdk-android/src/main/res/values/strings_RiotX.xml
@@ -1,24 +1,4 @@
 
 
 
-
-    
-        %1$s added %2$s as an address for this room.
-        %1$s added %2$s as addresses for this room.
-    
-
-    
-        %1$s removed %2$s as an address for this room.
-        %1$s removed %3$s as addresses for this room.
-    
-
-    %1$s added %2$s and removed %3$s as addresses for this room.
-
-    "%1$s set the main address for this room to %2$s."
-    "%1$s removed the main address for this room."
-
-    There is no network connection right now
-
-    %s is requesting to verify your key, but your client does not support in-chat key verification. You will need to use legacy key verification to verify keys.
-
 
diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt
index a29f5d5542..d67f14842b 100644
--- a/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt
+++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt
@@ -21,11 +21,12 @@ import im.vector.matrix.android.api.session.events.model.toContent
 import im.vector.matrix.android.api.session.room.Room
 import im.vector.matrix.android.api.session.room.RoomService
 import im.vector.matrix.android.api.session.room.model.Membership
-import im.vector.matrix.android.api.session.room.model.RoomMember
+import im.vector.matrix.android.api.session.room.model.RoomMemberContent
 import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
 import io.mockk.every
 import io.mockk.mockk
-import org.junit.Assert
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
 import org.junit.Test
 
 class PushrulesConditionTest {
@@ -40,7 +41,7 @@ class PushrulesConditionTest {
                 content = MessageTextContent("m.text", "Yo wtf?").toContent(),
                 originServerTs = 0)
 
-        val rm = RoomMember(
+        val rm = RoomMemberContent(
                 Membership.INVITE,
                 displayName = "Foo",
                 avatarUrl = "mxc://matrix.org/EqMZYbREvHXvYFyfxOlkf"
@@ -72,7 +73,7 @@ class PushrulesConditionTest {
                 type = "m.room.member",
                 eventId = "mx0",
                 stateKey = "@foo:matrix.org",
-                content = RoomMember(
+                content = RoomMemberContent(
                         Membership.INVITE,
                         displayName = "Foo",
                         avatarUrl = "mxc://matrix.org/EqMZYbREvHXvYFyfxOlkf"
@@ -147,9 +148,9 @@ class PushrulesConditionTest {
                 content = MessageTextContent("m.text", "A").toContent(),
                 originServerTs = 0,
                 roomId = room2JoinedId).also {
-            Assert.assertFalse("This room does not have 3 members", conditionEqual3.isSatisfied(it, sessionStub))
-            Assert.assertFalse("This room does not have 3 members", conditionEqual3Bis.isSatisfied(it, sessionStub))
-            Assert.assertTrue("This room has less than 3 members", conditionLessThan3.isSatisfied(it, sessionStub))
+            assertFalse("This room does not have 3 members", conditionEqual3.isSatisfied(it, sessionStub))
+            assertFalse("This room does not have 3 members", conditionEqual3Bis.isSatisfied(it, sessionStub))
+            assertTrue("This room has less than 3 members", conditionLessThan3.isSatisfied(it, sessionStub))
         }
 
         Event(
@@ -158,9 +159,9 @@ class PushrulesConditionTest {
                 content = MessageTextContent("m.text", "A").toContent(),
                 originServerTs = 0,
                 roomId = room3JoinedId).also {
-            Assert.assertTrue("This room has 3 members", conditionEqual3.isSatisfied(it, sessionStub))
-            Assert.assertTrue("This room has 3 members", conditionEqual3Bis.isSatisfied(it, sessionStub))
-            Assert.assertFalse("This room has more than 3 members", conditionLessThan3.isSatisfied(it, sessionStub))
+            assertTrue("This room has 3 members", conditionEqual3.isSatisfied(it, sessionStub))
+            assertTrue("This room has 3 members", conditionEqual3Bis.isSatisfied(it, sessionStub))
+            assertFalse("This room has more than 3 members", conditionLessThan3.isSatisfied(it, sessionStub))
         }
     }
 
@@ -174,7 +175,7 @@ class PushrulesConditionTest {
                 content = MessageTextContent("m.notice", "A").toContent(),
                 originServerTs = 0,
                 roomId = "2joined").also {
-            Assert.assertTrue("Notice", conditionEqual.isSatisfied(it))
+            assertTrue("Notice", conditionEqual.isSatisfied(it))
         }
     }
 }
diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/crypto/keysbackup/util/Base58Test.kt b/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/crypto/keysbackup/util/Base58Test.kt
new file mode 100644
index 0000000000..42295dada0
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/crypto/keysbackup/util/Base58Test.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2018 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.matrix.android.internal.crypto.keysbackup.util
+
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+@FixMethodOrder(MethodSorters.JVM)
+class Base58Test {
+
+    @Test
+    fun encode() {
+        //  Example comes from https://github.com/keis/base58
+        assertEquals("StV1DL6CwTryKyV", base58encode("hello world".toByteArray()))
+    }
+
+    @Test
+    fun decode() {
+        //  Example comes from https://github.com/keis/base58
+        assertArrayEquals("hello world".toByteArray(), base58decode("StV1DL6CwTryKyV"))
+    }
+
+    @Test
+    fun encode_curve25519() {
+        // Encode a 32 bytes key
+        assertEquals("4F85ZySpwyY6FuH7mQYyyr5b8nV9zFRBLj92AJa37sMr",
+                base58encode(("0123456789" + "0123456789" + "0123456789" + "01").toByteArray()))
+    }
+
+    @Test
+    fun decode_curve25519() {
+        assertArrayEquals(("0123456789" + "0123456789" + "0123456789" + "01").toByteArray(),
+                base58decode("4F85ZySpwyY6FuH7mQYyyr5b8nV9zFRBLj92AJa37sMr"))
+    }
+}
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
new file mode 100644
index 0000000000..47a2aa08df
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/crypto/keysbackup/util/RecoveryKeyTest.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2018 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.matrix.android.internal.crypto.keysbackup.util
+
+import org.junit.Assert.*
+import org.junit.Test
+
+class RecoveryKeyTest {
+    private val curve25519Key = byteArrayOf(
+            0x77.toByte(), 0x07.toByte(), 0x6D.toByte(), 0x0A.toByte(), 0x73.toByte(), 0x18.toByte(), 0xA5.toByte(), 0x7D.toByte(),
+            0x3C.toByte(), 0x16.toByte(), 0xC1.toByte(), 0x72.toByte(), 0x51.toByte(), 0xB2.toByte(), 0x66.toByte(), 0x45.toByte(),
+            0xDF.toByte(), 0x4C.toByte(), 0x2F.toByte(), 0x87.toByte(), 0xEB.toByte(), 0xC0.toByte(), 0x99.toByte(), 0x2A.toByte(),
+            0xB1.toByte(), 0x77.toByte(), 0xFB.toByte(), 0xA5.toByte(), 0x1D.toByte(), 0xB9.toByte(), 0x2C.toByte(), 0x2A.toByte())
+
+    @Test
+    fun isValidRecoveryKey_valid_true() {
+        assertTrue(isValidRecoveryKey("EsTcLW2KPGiFwKEA3As5g5c4BXwkqeeJZJV8Q9fugUMNUE4d"))
+
+        // Space should be ignored
+        assertTrue(isValidRecoveryKey("EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d"))
+
+        // All whitespace should be ignored
+        assertTrue(isValidRecoveryKey("EsTc LW2K PGiF wKEA 3As5 g5c4\r\nBXwk qeeJ ZJV8 Q9fu gUMN UE4d"))
+    }
+
+    @Test
+    fun isValidRecoveryKey_null_false() {
+        assertFalse(isValidRecoveryKey(null))
+    }
+
+    @Test
+    fun isValidRecoveryKey_empty_false() {
+        assertFalse(isValidRecoveryKey(""))
+    }
+
+    @Test
+    fun isValidRecoveryKey_wrong_size_false() {
+        assertFalse(isValidRecoveryKey("abc"))
+    }
+
+    @Test
+    fun isValidRecoveryKey_bad_first_byte_false() {
+        assertFalse(isValidRecoveryKey("FsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d"))
+    }
+
+    @Test
+    fun isValidRecoveryKey_bad_second_byte_false() {
+        assertFalse(isValidRecoveryKey("EqTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d"))
+    }
+
+    @Test
+    fun isValidRecoveryKey_bad_parity_false() {
+        assertFalse(isValidRecoveryKey("EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4e"))
+    }
+
+    @Test
+    fun computeRecoveryKey_ok() {
+        assertEquals("EsTcLW2KPGiFwKEA3As5g5c4BXwkqeeJZJV8Q9fugUMNUE4d", computeRecoveryKey(curve25519Key))
+    }
+
+    @Test
+    fun extractCurveKeyFromRecoveryKey_ok() {
+        assertArrayEquals(curve25519Key, extractCurveKeyFromRecoveryKey("EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d"))
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/crypto/store/db/HelperTest.kt b/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/crypto/store/db/HelperTest.kt
new file mode 100644
index 0000000000..d4740bdc4f
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/crypto/store/db/HelperTest.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2018 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.matrix.android.internal.crypto.store.db
+
+import im.vector.matrix.android.internal.util.md5
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class HelperTest {
+
+    @Test
+    fun testHash_ok() {
+        assertEquals("e9ee13b1ba2afc0825f4e556114785dd", "alice_15428931567802abf5ba7-d685-4333-af47-d38417ab3724:localhost:8480".md5())
+    }
+
+    @Test
+    fun testHash_size_ok() {
+        // Any String will have a 32 char hash
+        for (i in 1..100) {
+            assertEquals(32, "a".repeat(i).md5().length)
+        }
+    }
+}
diff --git a/tools/import_from_riot.sh b/tools/import_from_riot.sh
index 271a872a48..23f9167a5d 100755
--- a/tools/import_from_riot.sh
+++ b/tools/import_from_riot.sh
@@ -24,6 +24,7 @@ echo "Copy strings to SDK"
 
 cp ../matrix-android-sdk/matrix-sdk/src/main/res/values/strings.xml        ./matrix-sdk-android/src/main/res/values/strings.xml
 cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-ar/strings.xml     ./matrix-sdk-android/src/main/res/values-ar/strings.xml
+cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-az/strings.xml     ./matrix-sdk-android/src/main/res/values-az/strings.xml
 cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-bg/strings.xml     ./matrix-sdk-android/src/main/res/values-bg/strings.xml
 cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-bs/strings.xml     ./matrix-sdk-android/src/main/res/values-bs/strings.xml
 cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-ca/strings.xml     ./matrix-sdk-android/src/main/res/values-ca/strings.xml
@@ -67,6 +68,7 @@ echo "Copy strings to RiotX"
 
 cp ../riot-android/vector/src/main/res/values/strings.xml           ./vector/src/main/res/values/strings.xml
 cp ../riot-android/vector/src/main/res/values-ar/strings.xml        ./vector/src/main/res/values-ar/strings.xml
+cp ../riot-android/vector/src/main/res/values-az/strings.xml        ./vector/src/main/res/values-az/strings.xml
 cp ../riot-android/vector/src/main/res/values-b+sr+Latn/strings.xml ./vector/src/main/res/values-b+sr+Latn/strings.xml
 cp ../riot-android/vector/src/main/res/values-bg/strings.xml        ./vector/src/main/res/values-bg/strings.xml
 cp ../riot-android/vector/src/main/res/values-bn-rIN/strings.xml    ./vector/src/main/res/values-bn-rIN/strings.xml
diff --git a/vector/build.gradle b/vector/build.gradle
index c8d474088f..19bdd1dd8c 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -15,7 +15,7 @@ androidExtensions {
 }
 
 ext.versionMajor = 0
-ext.versionMinor = 12
+ext.versionMinor = 13
 ext.versionPatch = 0
 
 static def getGitTimestamp() {
@@ -186,6 +186,7 @@ android {
         gplay {
             dimension "store"
 
+            resValue "bool", "isGplay", "true"
             buildConfigField "boolean", "ALLOW_FCM_USE", "true"
             buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"G\""
             buildConfigField "String", "FLAVOR_DESCRIPTION", "\"GooglePlay\""
@@ -194,6 +195,7 @@ android {
         fdroid {
             dimension "store"
 
+            resValue "bool", "isGplay", "false"
             buildConfigField "boolean", "ALLOW_FCM_USE", "false"
             buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"F\""
             buildConfigField "String", "FLAVOR_DESCRIPTION", "\"FDroid\""
@@ -216,8 +218,8 @@ android {
 
 dependencies {
 
-    def epoxy_version = '3.8.0'
-    def fragment_version = '1.2.0-rc01'
+    def epoxy_version = '3.9.0'
+    def fragment_version = '1.2.0-rc04'
     def arrow_version = "0.8.2"
     def coroutines_version = "1.3.2"
     def markwon_version = '4.1.2'
@@ -225,7 +227,7 @@ dependencies {
     def glide_version = '4.10.0'
     def moshi_version = '1.8.0'
     def daggerVersion = '2.24'
-    def autofill_version = "1.0.0-rc01"
+    def autofill_version = "1.0.0"
 
     implementation project(":matrix-sdk-android")
     implementation project(":matrix-sdk-android-rx")
@@ -236,11 +238,11 @@ dependencies {
     implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
     implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
 
+    implementation "androidx.recyclerview:recyclerview:1.2.0-alpha01"
     implementation 'androidx.appcompat:appcompat:1.1.0'
     implementation "androidx.fragment:fragment:$fragment_version"
     implementation "androidx.fragment:fragment-ktx:$fragment_version"
-    //Do not use beta2 at the moment, as it breaks things
-    implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta1'
+    implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'
     implementation 'androidx.core:core-ktx:1.1.0'
 
     implementation "org.threeten:threetenbp:1.4.0:no-tzdb"
@@ -249,9 +251,6 @@ dependencies {
     implementation "com.squareup.moshi:moshi-adapters:$moshi_version"
     kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
 
-    // OSS License
-    implementation 'com.google.android.gms:play-services-oss-licenses:17.0.0'
-
     // Log
     implementation 'com.jakewharton.timber:timber:4.7.1'
 
@@ -276,10 +275,10 @@ dependencies {
     implementation 'com.airbnb.android:mvrx:1.3.0'
 
     // Work
-    implementation "androidx.work:work-runtime-ktx:2.3.0-alpha01"
+    implementation "androidx.work:work-runtime-ktx:2.3.0-beta02"
 
     // Paging
-    implementation "androidx.paging:paging-runtime-ktx:2.1.0"
+    implementation "androidx.paging:paging-runtime-ktx:2.1.1"
 
     // Functional Programming
     implementation "io.arrow-kt:arrow-core:$arrow_version"
@@ -289,7 +288,7 @@ dependencies {
 
     // UI
     implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
-    implementation 'com.google.android.material:material:1.1.0-beta01'
+    implementation 'com.google.android.material:material:1.2.0-alpha03'
     implementation 'me.gujun.android:span:1.7'
     implementation "io.noties.markwon:core:$markwon_version"
     implementation "io.noties.markwon:html:$markwon_version"
@@ -343,6 +342,9 @@ dependencies {
         exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
     }
 
+    // OSS License, gplay flavor only
+    gplayImplementation 'com.google.android.gms:play-services-oss-licenses:17.0.0'
+
     implementation "androidx.emoji:emoji-appcompat:1.0.0"
 
     // TESTS
diff --git a/vector/src/fdroid/java/im/vector/riotx/FlavorCode.kt b/vector/src/fdroid/java/im/vector/riotx/FlavorCode.kt
new file mode 100644
index 0000000000..de1ee2290b
--- /dev/null
+++ b/vector/src/fdroid/java/im/vector/riotx/FlavorCode.kt
@@ -0,0 +1,22 @@
+/*
+ * 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
+
+import android.content.Context
+
+// No op
+fun openOssLicensesMenuActivity(@Suppress("UNUSED_PARAMETER") context: Context) = Unit
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 951fcaa14d..84895f9f43 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
@@ -31,17 +31,17 @@ import timber.log.Timber
 class AlarmSyncBroadcastReceiver : BroadcastReceiver() {
 
     override fun onReceive(context: Context, intent: Intent) {
-        // Aquire a lock to give enough time for the sync :/
+        // Acquire a lock to give enough time for the sync :/
         (context.getSystemService(Context.POWER_SERVICE) as PowerManager).run {
             newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "riotx:fdroidSynclock").apply {
                 acquire((10_000).toLong())
             }
         }
 
-        val userId = intent.getStringExtra(SyncService.EXTRA_USER_ID)
+        val sessionId = intent.getStringExtra(SyncService.EXTRA_SESSION_ID)
         // This method is called when the BroadcastReceiver is receiving an Intent broadcast.
         Timber.d("RestartBroadcastReceiver received intent")
-        VectorSyncService.newIntent(context, userId).also {
+        VectorSyncService.newIntent(context, sessionId).let {
             try {
                 ContextCompat.startForegroundService(context, it)
             } catch (ex: Throwable) {
@@ -50,7 +50,7 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() {
             }
         }
 
-        scheduleAlarm(context, userId, 30_000L)
+        scheduleAlarm(context, sessionId, 30_000L)
 
         Timber.i("Alarm scheduled to restart service")
     }
@@ -58,10 +58,10 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() {
     companion object {
         private const val REQUEST_CODE = 0
 
-        fun scheduleAlarm(context: Context, userId: String, delay: Long) {
+        fun scheduleAlarm(context: Context, sessionId: String, delay: Long) {
             // Reschedule
             val intent = Intent(context, AlarmSyncBroadcastReceiver::class.java).apply {
-                putExtra(SyncService.EXTRA_USER_ID, userId)
+                putExtra(SyncService.EXTRA_SESSION_ID, sessionId)
             }
             val pIntent = PendingIntent.getBroadcast(context, REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT)
             val firstMillis = System.currentTimeMillis() + delay
diff --git a/vector/src/gplay/java/im/vector/riotx/FlavorCode.kt b/vector/src/gplay/java/im/vector/riotx/FlavorCode.kt
new file mode 100644
index 0000000000..109e9bc978
--- /dev/null
+++ b/vector/src/gplay/java/im/vector/riotx/FlavorCode.kt
@@ -0,0 +1,23 @@
+/*
+ * 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
+
+import android.content.Context
+import android.content.Intent
+import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
+
+fun openOssLicensesMenuActivity(context: Context) = context.startActivity(Intent(context, OssLicensesMenuActivity::class.java))
diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml
index 124763916b..308ca60094 100644
--- a/vector/src/main/AndroidManifest.xml
+++ b/vector/src/main/AndroidManifest.xml
@@ -6,6 +6,7 @@
     
     
     
+    
 
      : EpoxyModelWithHolder() {
 
+    protected val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
+
     private var onModelVisibilityStateChangedListener: OnVisibilityStateChangedListener? = null
 
+    override fun unbind(holder: H) {
+        coroutineScope.coroutineContext.cancelChildren()
+        super.unbind(holder)
+    }
+
     override fun onVisibilityStateChanged(visibilityState: Int, view: H) {
         onModelVisibilityStateChangedListener?.onVisibilityStateChanged(visibilityState)
         super.onVisibilityStateChanged(visibilityState, view)
diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt
index 7b79ce8549..e5ffd5f350 100644
--- a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt
+++ b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt
@@ -51,7 +51,7 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel {
                         stringProvider.getString(R.string.login_error_not_json)
                     }
+                    throwable.error.code == MatrixError.M_THREEPID_DENIED           -> {
+                        stringProvider.getString(R.string.login_error_threepid_denied)
+                    }
                     throwable.error.code == MatrixError.M_LIMIT_EXCEEDED     -> {
                         limitExceededError(throwable.error)
                     }
diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/RecyclerView.kt b/vector/src/main/java/im/vector/riotx/core/extensions/RecyclerView.kt
index 003045af51..3d247e149c 100644
--- a/vector/src/main/java/im/vector/riotx/core/extensions/RecyclerView.kt
+++ b/vector/src/main/java/im/vector/riotx/core/extensions/RecyclerView.kt
@@ -26,9 +26,13 @@ import com.airbnb.epoxy.EpoxyController
  */
 fun RecyclerView.configureWith(epoxyController: EpoxyController,
                                itemAnimator: RecyclerView.ItemAnimator? = null,
+                               viewPool: RecyclerView.RecycledViewPool? = null,
                                showDivider: Boolean = false,
                                hasFixedSize: Boolean = true) {
-    layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
+    layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false).apply {
+        recycleChildrenOnDetach = viewPool != null
+    }
+    setRecycledViewPool(viewPool)
     itemAnimator?.let { this.itemAnimator = it }
     if (showDivider) {
         addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
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 620e32f51f..0a8345c650 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
@@ -47,7 +47,7 @@ fun Session.configureAndStart(context: Context,
 fun Session.startSyncing(context: Context) {
     val applicationContext = context.applicationContext
     if (!hasAlreadySynced()) {
-        VectorSyncService.newIntent(applicationContext, myUserId).also {
+        VectorSyncService.newIntent(applicationContext, sessionId).also {
             try {
                 ContextCompat.startForegroundService(applicationContext, it)
             } catch (ex: Throwable) {
diff --git a/vector/src/main/java/im/vector/riotx/core/hardware/vibrator.kt b/vector/src/main/java/im/vector/riotx/core/hardware/vibrator.kt
new file mode 100644
index 0000000000..8fc24d7fff
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/core/hardware/vibrator.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.core.hardware
+
+import android.content.Context
+import android.os.Build
+import android.os.VibrationEffect
+import android.os.Vibrator
+
+fun vibrate(context: Context, durationMillis: Long = 100) {
+    val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator? ?: return
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+        vibrator.vibrate(VibrationEffect.createOneShot(durationMillis, VibrationEffect.DEFAULT_AMPLITUDE))
+    } else {
+        @Suppress("DEPRECATION")
+        vibrator.vibrate(durationMillis)
+    }
+}
diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt
index f70aed9393..f4e3631b8a 100644
--- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt
@@ -54,6 +54,7 @@ import im.vector.riotx.features.rageshake.BugReportActivity
 import im.vector.riotx.features.rageshake.BugReporter
 import im.vector.riotx.features.rageshake.RageShake
 import im.vector.riotx.features.session.SessionListener
+import im.vector.riotx.features.settings.VectorPreferences
 import im.vector.riotx.features.themes.ActivityOtherThemes
 import im.vector.riotx.features.themes.ThemeUtils
 import im.vector.riotx.receivers.DebugReceiver
@@ -88,9 +89,11 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
     private lateinit var configurationViewModel: ConfigurationViewModel
     private lateinit var sessionListener: SessionListener
     protected lateinit var bugReporter: BugReporter
-    private lateinit var rageShake: RageShake
+    lateinit var rageShake: RageShake
+        private set
     protected lateinit var navigator: Navigator
     private lateinit var activeSessionHolder: ActiveSessionHolder
+    private lateinit var vectorPreferences: VectorPreferences
 
     // Filter for multiple invalid token error
     private var mainActivityStarted = false
@@ -135,7 +138,8 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
     }
 
     override fun onCreate(savedInstanceState: Bundle?) {
-        screenComponent = DaggerScreenComponent.factory().create(getVectorComponent(), this)
+        val vectorComponent = getVectorComponent()
+        screenComponent = DaggerScreenComponent.factory().create(vectorComponent, this)
         val timeForInjection = measureTimeMillis {
             injectWith(screenComponent)
         }
@@ -150,6 +154,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
         rageShake = screenComponent.rageShake()
         navigator = screenComponent.navigator()
         activeSessionHolder = screenComponent.activeSessionHolder()
+        vectorPreferences = vectorComponent.vectorPreferences()
         configurationViewModel.activityRestarter.observe(this, Observer {
             if (!it.hasBeenHandled) {
                 // Recreate the Activity because configuration has changed
@@ -226,7 +231,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
 
         configurationViewModel.onActivityResumed()
 
-        if (this !is BugReportActivity) {
+        if (this !is BugReportActivity && vectorPreferences.useRageshake()) {
             rageShake.start()
         }
 
diff --git a/vector/src/main/java/im/vector/riotx/core/preference/VectorPreferenceDivider.kt b/vector/src/main/java/im/vector/riotx/core/preference/VectorPreferenceDivider.kt
deleted file mode 100644
index f9f9da644b..0000000000
--- a/vector/src/main/java/im/vector/riotx/core/preference/VectorPreferenceDivider.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright 2018 New Vector Ltd
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package im.vector.riotx.core.preference
-
-import android.content.Context
-import android.util.AttributeSet
-import androidx.preference.Preference
-import im.vector.riotx.R
-
-/**
- * Divider for Preference screen
- */
-class VectorPreferenceDivider @JvmOverloads constructor(context: Context,
-                                                        attrs: AttributeSet? = null,
-                                                        defStyleAttr: Int = 0,
-                                                        defStyleRes: Int = 0
-) : Preference(context, attrs, defStyleAttr, defStyleRes) {
-
-    init {
-        layoutResource = R.layout.vector_preference_divider
-    }
-}
diff --git a/vector/src/main/java/im/vector/riotx/core/rx/Rx.kt b/vector/src/main/java/im/vector/riotx/core/rx/RxConfig.kt
similarity index 57%
rename from vector/src/main/java/im/vector/riotx/core/rx/Rx.kt
rename to vector/src/main/java/im/vector/riotx/core/rx/RxConfig.kt
index 89de9030dc..d8828eb1b8 100644
--- a/vector/src/main/java/im/vector/riotx/core/rx/Rx.kt
+++ b/vector/src/main/java/im/vector/riotx/core/rx/RxConfig.kt
@@ -17,19 +17,26 @@
 package im.vector.riotx.core.rx
 
 import im.vector.riotx.BuildConfig
+import im.vector.riotx.features.settings.VectorPreferences
 import io.reactivex.plugins.RxJavaPlugins
 import timber.log.Timber
+import javax.inject.Inject
 
-/**
- * Make sure unhandled Rx error does not crash the app in production
- */
-fun setupRxPlugin() {
-    RxJavaPlugins.setErrorHandler { throwable ->
-        Timber.e(throwable, "RxError")
+class RxConfig @Inject constructor(
+        private val vectorPreferences: VectorPreferences
+) {
 
-        // Avoid crash in production
-        if (BuildConfig.DEBUG) {
-            throw throwable
+    /**
+     * Make sure unhandled Rx error does not crash the app in production
+     */
+    fun setupRxPlugin() {
+        RxJavaPlugins.setErrorHandler { throwable ->
+            Timber.e(throwable, "RxError")
+
+            // Avoid crash in production
+            if (BuildConfig.DEBUG || vectorPreferences.failFast()) {
+                throw throwable
+            }
         }
     }
 }
diff --git a/vector/src/main/java/im/vector/riotx/core/services/VectorSyncService.kt b/vector/src/main/java/im/vector/riotx/core/services/VectorSyncService.kt
index 31b56c4e47..314e12db05 100644
--- a/vector/src/main/java/im/vector/riotx/core/services/VectorSyncService.kt
+++ b/vector/src/main/java/im/vector/riotx/core/services/VectorSyncService.kt
@@ -30,9 +30,9 @@ class VectorSyncService : SyncService() {
 
     companion object {
 
-        fun newIntent(context: Context, userId: String): Intent {
+        fun newIntent(context: Context, sessionId: String): Intent {
             return Intent(context, VectorSyncService::class.java).also {
-                it.putExtra(EXTRA_USER_ID, userId)
+                it.putExtra(EXTRA_SESSION_ID, sessionId)
             }
         }
     }
@@ -45,36 +45,34 @@ class VectorSyncService : SyncService() {
     }
 
     override fun onStart(isInitialSync: Boolean) {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-            val notificationSubtitleRes = if (isInitialSync) {
-                R.string.notification_initial_sync
-            } else {
-                R.string.notification_listening_for_events
-            }
-            val notification = notificationUtils.buildForegroundServiceNotification(notificationSubtitleRes, false)
-            startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification)
+        val notificationSubtitleRes = if (isInitialSync) {
+            R.string.notification_initial_sync
+        } else {
+            R.string.notification_listening_for_events
         }
+        val notification = notificationUtils.buildForegroundServiceNotification(notificationSubtitleRes, false)
+        startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification)
     }
 
-    override fun onRescheduleAsked(userId: String, isInitialSync: Boolean, delay: Long) {
-        reschedule(userId, delay)
+    override fun onRescheduleAsked(sessionId: String, isInitialSync: Boolean, delay: Long) {
+        reschedule(sessionId, delay)
     }
 
     override fun onDestroy() {
-        removeForegroundNotif()
+        removeForegroundNotification()
         super.onDestroy()
     }
 
-    private fun removeForegroundNotif() {
+    private fun removeForegroundNotification() {
         val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
         notificationManager.cancel(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE)
     }
 
-    private fun reschedule(userId: String, delay: Long) {
+    private fun reschedule(sessionId: String, delay: Long) {
         val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-            PendingIntent.getForegroundService(this, 0, newIntent(this, userId), 0)
+            PendingIntent.getForegroundService(this, 0, newIntent(this, sessionId), 0)
         } else {
-            PendingIntent.getService(this, 0, newIntent(this, userId), 0)
+            PendingIntent.getService(this, 0, newIntent(this, sessionId), 0)
         }
         val firstMillis = System.currentTimeMillis() + delay
         val alarmMgr = getSystemService(Context.ALARM_SERVICE) as AlarmManager
diff --git a/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt b/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt
index b02e3c9366..627d757574 100644
--- a/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt
+++ b/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt
@@ -19,16 +19,14 @@ package im.vector.riotx.core.utils
 
 import android.os.Handler
 
-internal class Debouncer(private val handler: Handler) {
+class Debouncer(private val handler: Handler) {
 
     private val runnables = HashMap()
 
     fun debounce(identifier: String, millis: Long, r: Runnable): Boolean {
-        if (runnables.containsKey(identifier)) {
-            // debounce
-            val old = runnables[identifier]
-            handler.removeCallbacks(old)
-        }
+        // debounce
+        cancel(identifier)
+
         insertRunnable(identifier, r, millis)
         return true
     }
@@ -37,6 +35,14 @@ internal class Debouncer(private val handler: Handler) {
         handler.removeCallbacksAndMessages(null)
     }
 
+    fun cancel(identifier: String) {
+        if (runnables.containsKey(identifier)) {
+            val old = runnables[identifier]
+            handler.removeCallbacks(old)
+            runnables.remove(identifier)
+        }
+    }
+
     private fun insertRunnable(identifier: String, r: Runnable, millis: Long) {
         val chained = Runnable {
             handler.post(r)
diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt
new file mode 100644
index 0000000000..6d498de2d2
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2019 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.features.autocomplete.emoji
+
+import android.graphics.Typeface
+import androidx.recyclerview.widget.RecyclerView
+import com.airbnb.epoxy.TypedEpoxyController
+import im.vector.riotx.EmojiCompatFontProvider
+import im.vector.riotx.features.autocomplete.AutocompleteClickListener
+import im.vector.riotx.features.reactions.ReactionClickListener
+import im.vector.riotx.features.reactions.data.EmojiItem
+import javax.inject.Inject
+
+class AutocompleteEmojiController @Inject constructor(
+        private val fontProvider: EmojiCompatFontProvider
+) : TypedEpoxyController>() {
+
+    var emojiTypeface: Typeface? = fontProvider.typeface
+
+    private val fontProviderListener = object : EmojiCompatFontProvider.FontProviderListener {
+        override fun compatibilityFontUpdate(typeface: Typeface?) {
+            emojiTypeface = typeface
+        }
+    }
+
+    var listener: AutocompleteClickListener? = null
+
+    override fun buildModels(data: List?) {
+        if (data.isNullOrEmpty()) {
+            return
+        }
+        data
+                .take(MAX)
+                .forEach { emojiItem ->
+                    autocompleteEmojiItem {
+                        id(emojiItem.name)
+                        emojiItem(emojiItem)
+                        emojiTypeFace(emojiTypeface)
+                        onClickListener(
+                                object : ReactionClickListener {
+                                    override fun onReactionSelected(reaction: String) {
+                                        listener?.onItemClick(reaction)
+                                    }
+                                }
+                        )
+                    }
+                }
+
+        if (data.size > MAX) {
+            autocompleteMoreResultItem {
+                id("more_result")
+            }
+        }
+    }
+
+    override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
+        fontProvider.addListener(fontProviderListener)
+    }
+
+    override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
+        super.onDetachedFromRecyclerView(recyclerView)
+        fontProvider.removeListener(fontProviderListener)
+    }
+
+    companion object {
+        const val MAX = 50
+    }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiItem.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiItem.kt
new file mode 100644
index 0000000000..36759f9271
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiItem.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2019 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.features.autocomplete.emoji
+
+import android.graphics.Typeface
+import android.widget.TextView
+import com.airbnb.epoxy.EpoxyAttribute
+import com.airbnb.epoxy.EpoxyModelClass
+import im.vector.riotx.R
+import im.vector.riotx.core.epoxy.VectorEpoxyHolder
+import im.vector.riotx.core.epoxy.VectorEpoxyModel
+import im.vector.riotx.core.extensions.setTextOrHide
+import im.vector.riotx.features.reactions.ReactionClickListener
+import im.vector.riotx.features.reactions.data.EmojiItem
+
+@EpoxyModelClass(layout = R.layout.item_autocomplete_emoji)
+abstract class AutocompleteEmojiItem : VectorEpoxyModel() {
+
+    @EpoxyAttribute
+    lateinit var emojiItem: EmojiItem
+
+    @EpoxyAttribute
+    var emojiTypeFace: Typeface? = null
+
+    @EpoxyAttribute
+    var onClickListener: ReactionClickListener? = null
+
+    override fun bind(holder: Holder) {
+        holder.emojiText.text = emojiItem.emoji
+        holder.emojiText.typeface = emojiTypeFace ?: Typeface.DEFAULT
+        holder.emojiNameText.text = emojiItem.name
+        holder.emojiKeywordText.setTextOrHide(emojiItem.keywords.joinToString())
+
+        holder.view.setOnClickListener {
+            onClickListener?.onReactionSelected(emojiItem.emoji)
+        }
+    }
+
+    class Holder : VectorEpoxyHolder() {
+        val emojiText by bind(R.id.itemAutocompleteEmoji)
+        val emojiNameText by bind(R.id.itemAutocompleteEmojiName)
+        val emojiKeywordText by bind(R.id.itemAutocompleteEmojiSubname)
+    }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt
similarity index 60%
rename from vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserPresenter.kt
rename to vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt
index 01dceb5399..731b48af86 100644
--- a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserPresenter.kt
+++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt
@@ -14,22 +14,19 @@
  * limitations under the License.
  */
 
-package im.vector.riotx.features.autocomplete.user
+package im.vector.riotx.features.autocomplete.emoji
 
 import android.content.Context
 import androidx.recyclerview.widget.RecyclerView
-import com.airbnb.mvrx.Async
-import com.airbnb.mvrx.Success
 import com.otaliastudios.autocomplete.RecyclerViewPresenter
-import im.vector.matrix.android.api.session.user.model.User
 import im.vector.riotx.features.autocomplete.AutocompleteClickListener
+import im.vector.riotx.features.reactions.data.EmojiDataSource
 import javax.inject.Inject
 
-class AutocompleteUserPresenter @Inject constructor(context: Context,
-                                                    private val controller: AutocompleteUserController
-) : RecyclerViewPresenter(context), AutocompleteClickListener {
-
-    var callback: Callback? = null
+class AutocompleteEmojiPresenter @Inject constructor(context: Context,
+                                                     private val emojiDataSource: EmojiDataSource,
+                                                     private val controller: AutocompleteEmojiController) :
+        RecyclerViewPresenter(context), AutocompleteClickListener {
 
     init {
         controller.listener = this
@@ -41,21 +38,17 @@ class AutocompleteUserPresenter @Inject constructor(context: Context,
         return controller.adapter
     }
 
-    override fun onItemClick(t: User) {
+    override fun onItemClick(t: String) {
         dispatchClick(t)
     }
 
     override fun onQuery(query: CharSequence?) {
-        callback?.onQueryUsers(query)
-    }
-
-    fun render(users: Async>) {
-        if (users is Success) {
-            controller.setData(users())
+        val data = if (query.isNullOrBlank()) {
+            // Return common emojis
+            emojiDataSource.getQuickReactions()
+        } else {
+            emojiDataSource.filterWith(query.toString())
         }
-    }
-
-    interface Callback {
-        fun onQueryUsers(query: CharSequence?)
+        controller.setData(data)
     }
 }
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerAction.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteMoreResultItem.kt
similarity index 58%
rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerAction.kt
rename to vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteMoreResultItem.kt
index 0f5bf2a8c5..844cc96035 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerAction.kt
+++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteMoreResultItem.kt
@@ -14,12 +14,15 @@
  * limitations under the License.
  */
 
-package im.vector.riotx.features.home.room.detail.composer
+package im.vector.riotx.features.autocomplete.emoji
 
-import im.vector.riotx.core.platform.VectorViewModelAction
+import com.airbnb.epoxy.EpoxyModelClass
+import im.vector.riotx.R
+import im.vector.riotx.core.epoxy.VectorEpoxyHolder
+import im.vector.riotx.core.epoxy.VectorEpoxyModel
 
-sealed class TextComposerAction : VectorViewModelAction {
-    data class QueryUsers(val query: CharSequence?) : TextComposerAction()
-    data class QueryRooms(val query: CharSequence?) : TextComposerAction()
-    data class QueryGroups(val query: CharSequence?) : TextComposerAction()
+@EpoxyModelClass(layout = R.layout.item_autocomplete_more_result)
+abstract class AutocompleteMoreResultItem : VectorEpoxyModel() {
+
+    class Holder : VectorEpoxyHolder()
 }
diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt
index 822ce451e7..b6f45b477c 100644
--- a/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt
+++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt
@@ -18,19 +18,19 @@ package im.vector.riotx.features.autocomplete.group
 
 import android.content.Context
 import androidx.recyclerview.widget.RecyclerView
-import com.airbnb.mvrx.Async
-import com.airbnb.mvrx.Success
 import com.otaliastudios.autocomplete.RecyclerViewPresenter
+import im.vector.matrix.android.api.query.QueryStringValue
+import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.api.session.group.groupSummaryQueryParams
 import im.vector.matrix.android.api.session.group.model.GroupSummary
 import im.vector.riotx.features.autocomplete.AutocompleteClickListener
 import javax.inject.Inject
 
 class AutocompleteGroupPresenter @Inject constructor(context: Context,
-                                                     private val controller: AutocompleteGroupController
+                                                     private val controller: AutocompleteGroupController,
+                                                     private val session: Session
 ) : RecyclerViewPresenter(context), AutocompleteClickListener {
 
-    var callback: Callback? = null
-
     init {
         controller.listener = this
     }
@@ -46,16 +46,16 @@ class AutocompleteGroupPresenter @Inject constructor(context: Context,
     }
 
     override fun onQuery(query: CharSequence?) {
-        callback?.onQueryGroups(query)
-    }
-
-    fun render(groups: Async>) {
-        if (groups is Success) {
-            controller.setData(groups())
+        val queryParams = groupSummaryQueryParams {
+            displayName = if (query.isNullOrBlank()) {
+                QueryStringValue.IsNotEmpty
+            } else {
+                QueryStringValue.Contains(query.toString(), QueryStringValue.Case.INSENSITIVE)
+            }
         }
-    }
-
-    interface Callback {
-        fun onQueryGroups(query: CharSequence?)
+        val groups = session.getGroupSummaries(queryParams)
+                .asSequence()
+                .sortedBy { it.displayName }
+        controller.setData(groups.toList())
     }
 }
diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserController.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberController.kt
similarity index 80%
rename from vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserController.kt
rename to vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberController.kt
index 53a87fe27a..1c8dc99196 100644
--- a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserController.kt
+++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberController.kt
@@ -14,23 +14,23 @@
  * limitations under the License.
  */
 
-package im.vector.riotx.features.autocomplete.user
+package im.vector.riotx.features.autocomplete.member
 
 import com.airbnb.epoxy.TypedEpoxyController
-import im.vector.matrix.android.api.session.user.model.User
+import im.vector.matrix.android.api.session.room.model.RoomMember
 import im.vector.matrix.android.api.util.toMatrixItem
 import im.vector.riotx.features.autocomplete.AutocompleteClickListener
 import im.vector.riotx.features.autocomplete.autocompleteMatrixItem
 import im.vector.riotx.features.home.AvatarRenderer
 import javax.inject.Inject
 
-class AutocompleteUserController @Inject constructor() : TypedEpoxyController>() {
+class AutocompleteMemberController @Inject constructor() : TypedEpoxyController>() {
 
-    var listener: AutocompleteClickListener? = null
+    var listener: AutocompleteClickListener? = null
 
     @Inject lateinit var avatarRenderer: AvatarRenderer
 
-    override fun buildModels(data: List?) {
+    override fun buildModels(data: List?) {
         if (data.isNullOrEmpty()) {
             return
         }
diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt
new file mode 100644
index 0000000000..84a33173b8
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2019 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.features.autocomplete.member
+
+import android.content.Context
+import androidx.recyclerview.widget.RecyclerView
+import com.otaliastudios.autocomplete.RecyclerViewPresenter
+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.room.members.roomMemberQueryParams
+import im.vector.matrix.android.api.session.room.model.Membership
+import im.vector.matrix.android.api.session.room.model.RoomMember
+import im.vector.riotx.features.autocomplete.AutocompleteClickListener
+
+class AutocompleteMemberPresenter @AssistedInject constructor(context: Context,
+                                                              @Assisted val roomId: String,
+                                                              private val session: Session,
+                                                              private val controller: AutocompleteMemberController
+) : RecyclerViewPresenter(context), AutocompleteClickListener {
+
+    private val room = session.getRoom(roomId)!!
+
+    init {
+        controller.listener = this
+    }
+
+    @AssistedInject.Factory
+    interface Factory {
+        fun create(roomId: String): AutocompleteMemberPresenter
+    }
+
+    override fun instantiateAdapter(): RecyclerView.Adapter<*> {
+        // Also remove animation
+        recyclerView?.itemAnimator = null
+        return controller.adapter
+    }
+
+    override fun onItemClick(t: RoomMember) {
+        dispatchClick(t)
+    }
+
+    override fun onQuery(query: CharSequence?) {
+        val queryParams = roomMemberQueryParams {
+            displayName = if (query.isNullOrBlank()) {
+                QueryStringValue.IsNotEmpty
+            } else {
+                QueryStringValue.Contains(query.toString(), QueryStringValue.Case.INSENSITIVE)
+            }
+            memberships = listOf(Membership.JOIN)
+        }
+        val members = room.getRoomMembers(queryParams)
+                .asSequence()
+                .sortedBy { it.displayName }
+        controller.setData(members.toList())
+    }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomController.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomController.kt
index 51285b02b7..aae95502d9 100644
--- a/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomController.kt
+++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomController.kt
@@ -24,12 +24,10 @@ import im.vector.riotx.features.autocomplete.autocompleteMatrixItem
 import im.vector.riotx.features.home.AvatarRenderer
 import javax.inject.Inject
 
-class AutocompleteRoomController @Inject constructor() : TypedEpoxyController>() {
+class AutocompleteRoomController @Inject constructor(private val avatarRenderer: AvatarRenderer) : TypedEpoxyController>() {
 
     var listener: AutocompleteClickListener? = null
 
-    @Inject lateinit var avatarRenderer: AvatarRenderer
-
     override fun buildModels(data: List?) {
         if (data.isNullOrEmpty()) {
             return
diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt
index 53fed7f859..17787a22ef 100644
--- a/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt
+++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt
@@ -18,19 +18,19 @@ package im.vector.riotx.features.autocomplete.room
 
 import android.content.Context
 import androidx.recyclerview.widget.RecyclerView
-import com.airbnb.mvrx.Async
-import com.airbnb.mvrx.Success
 import com.otaliastudios.autocomplete.RecyclerViewPresenter
+import im.vector.matrix.android.api.query.QueryStringValue
+import im.vector.matrix.android.api.session.Session
 import im.vector.matrix.android.api.session.room.model.RoomSummary
+import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
 import im.vector.riotx.features.autocomplete.AutocompleteClickListener
 import javax.inject.Inject
 
 class AutocompleteRoomPresenter @Inject constructor(context: Context,
-                                                    private val controller: AutocompleteRoomController
+                                                    private val controller: AutocompleteRoomController,
+                                                    private val session: Session
 ) : RecyclerViewPresenter(context), AutocompleteClickListener {
 
-    var callback: Callback? = null
-
     init {
         controller.listener = this
     }
@@ -46,16 +46,16 @@ class AutocompleteRoomPresenter @Inject constructor(context: Context,
     }
 
     override fun onQuery(query: CharSequence?) {
-        callback?.onQueryRooms(query)
-    }
-
-    fun render(rooms: Async>) {
-        if (rooms is Success) {
-            controller.setData(rooms())
+        val queryParams = roomSummaryQueryParams {
+            canonicalAlias = if (query.isNullOrBlank()) {
+                QueryStringValue.IsNotNull
+            } else {
+                QueryStringValue.Contains(query.toString(), QueryStringValue.Case.INSENSITIVE)
+            }
         }
-    }
-
-    interface Callback {
-        fun onQueryRooms(query: CharSequence?)
+        val rooms = session.getRoomSummaries(queryParams)
+                .asSequence()
+                .sortedBy { it.displayName }
+        controller.setData(rooms.toList())
     }
 }
diff --git a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt
index b7c3e61ee4..c3d16f3299 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt
@@ -71,6 +71,14 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
                 .into(target)
     }
 
+    @AnyThread
+    fun getCachedDrawable(glideRequest: GlideRequests, matrixItem: MatrixItem): Drawable {
+        return buildGlideRequest(glideRequest, matrixItem.avatarUrl)
+                .onlyRetrieveFromCache(true)
+                .submit()
+                .get()
+    }
+
     @AnyThread
     fun getPlaceholderDrawable(context: Context, matrixItem: MatrixItem): Drawable {
         val avatarColor = when (matrixItem) {
diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt
index b9d3e3c95e..85f14e99a8 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt
@@ -66,6 +66,11 @@ class HomeDetailFragment @Inject constructor(
         setupToolbar()
         setupKeysBackupBanner()
 
+        withState(viewModel) {
+            // Update the navigation view if needed (for when we restore the tabs)
+            bottomNavigationView.selectedItemId = it.displayMode.toMenuId()
+        }
+
         viewModel.selectSubscribe(this, HomeDetailViewState::groupSummary) { groupSummary ->
             onGroupChange(groupSummary.orNull())
         }
@@ -127,7 +132,6 @@ class HomeDetailFragment @Inject constructor(
     private fun setupBottomNavigationView() {
         bottomNavigationView.setOnNavigationItemSelectedListener {
             val displayMode = when (it.itemId) {
-                R.id.bottom_action_home   -> RoomListDisplayMode.HOME
                 R.id.bottom_action_people -> RoomListDisplayMode.PEOPLE
                 R.id.bottom_action_rooms  -> RoomListDisplayMode.ROOMS
                 else                      -> RoomListDisplayMode.HOME
@@ -149,12 +153,6 @@ class HomeDetailFragment @Inject constructor(
     private fun switchDisplayMode(displayMode: RoomListDisplayMode) {
         groupToolbarTitleView.setText(displayMode.titleRes)
         updateSelectedFragment(displayMode)
-        // Update the navigation view (for when we restore the tabs)
-        bottomNavigationView.selectedItemId = when (displayMode) {
-            RoomListDisplayMode.PEOPLE -> R.id.bottom_action_people
-            RoomListDisplayMode.ROOMS  -> R.id.bottom_action_rooms
-            else                       -> R.id.bottom_action_home
-        }
     }
 
     private fun updateSelectedFragment(displayMode: RoomListDisplayMode) {
@@ -194,4 +192,10 @@ class HomeDetailFragment @Inject constructor(
         unreadCounterBadgeViews[INDEX_ROOMS].render(UnreadCounterBadgeView.State(it.notificationCountRooms, it.notificationHighlightRooms))
         syncStateView.render(it.syncState)
     }
+
+    private fun RoomListDisplayMode.toMenuId() = when (this) {
+        RoomListDisplayMode.PEOPLE -> R.id.bottom_action_people
+        RoomListDisplayMode.ROOMS  -> R.id.bottom_action_rooms
+        else                       -> R.id.bottom_action_home
+    }
 }
diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt
index 6ff836e8c8..bc3bc2f9d5 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt
@@ -40,7 +40,7 @@ class HomeDrawerFragment @Inject constructor(
         if (savedInstanceState == null) {
             replaceChildFragment(R.id.homeDrawerGroupListContainer, GroupListFragment::class.java)
         }
-        session.liveUser(session.myUserId).observeK(this) { optionalUser ->
+        session.getUserLive(session.myUserId).observeK(viewLifecycleOwner) { optionalUser ->
             val user = optionalUser?.getOrNull()
             if (user != null) {
                 avatarRenderer.render(user.toMatrixItem(), homeDrawerHeaderAvatarView)
diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt
index 24318bc508..a00ee24b49 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt
@@ -24,7 +24,9 @@ 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.query.QueryStringValue
 import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.api.session.group.groupSummaryQueryParams
 import im.vector.matrix.android.api.session.group.model.GroupSummary
 import im.vector.matrix.android.api.session.room.model.Membership
 import im.vector.matrix.rx.rx
@@ -96,6 +98,10 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro
     }
 
     private fun observeGroupSummaries() {
+        val groupSummariesQueryParams = groupSummaryQueryParams {
+            memberships = listOf(Membership.JOIN)
+            displayName = QueryStringValue.IsNotEmpty
+        }
         Observable.combineLatest, List>(
                 session
                         .rx()
@@ -109,9 +115,7 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro
                         },
                 session
                         .rx()
-                        .liveGroupSummaries()
-                        // Keep only joined groups. Group invitations will be managed later
-                        .map { it.filter { groupSummary -> groupSummary.membership == Membership.JOIN } },
+                        .liveGroupSummaries(groupSummariesQueryParams),
                 BiFunction { allCommunityGroup, communityGroups ->
                     listOf(allCommunityGroup) + communityGroups
                 }
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt
new file mode 100644
index 0000000000..7ca647ea3e
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt
@@ -0,0 +1,229 @@
+/*
+ * Copyright 2019 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.features.home.room.detail
+
+import android.graphics.drawable.ColorDrawable
+import android.graphics.drawable.Drawable
+import android.text.Editable
+import android.text.Spannable
+import android.widget.EditText
+import com.otaliastudios.autocomplete.Autocomplete
+import com.otaliastudios.autocomplete.AutocompleteCallback
+import com.otaliastudios.autocomplete.CharPolicy
+import com.squareup.inject.assisted.Assisted
+import com.squareup.inject.assisted.AssistedInject
+import im.vector.matrix.android.api.session.group.model.GroupSummary
+import im.vector.matrix.android.api.session.room.model.RoomMember
+import im.vector.matrix.android.api.session.room.model.RoomSummary
+import im.vector.matrix.android.api.util.MatrixItem
+import im.vector.matrix.android.api.util.toMatrixItem
+import im.vector.matrix.android.api.util.toRoomAliasMatrixItem
+import im.vector.riotx.R
+import im.vector.riotx.core.glide.GlideApp
+import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
+import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy
+import im.vector.riotx.features.autocomplete.emoji.AutocompleteEmojiPresenter
+import im.vector.riotx.features.autocomplete.group.AutocompleteGroupPresenter
+import im.vector.riotx.features.autocomplete.member.AutocompleteMemberPresenter
+import im.vector.riotx.features.autocomplete.room.AutocompleteRoomPresenter
+import im.vector.riotx.features.command.Command
+import im.vector.riotx.features.home.AvatarRenderer
+import im.vector.riotx.features.html.PillImageSpan
+import im.vector.riotx.features.themes.ThemeUtils
+
+class AutoCompleter @AssistedInject constructor(
+        @Assisted val roomId: String,
+        private val avatarRenderer: AvatarRenderer,
+        private val commandAutocompletePolicy: CommandAutocompletePolicy,
+        private val autocompleteCommandPresenter: AutocompleteCommandPresenter,
+        private val autocompleteMemberPresenterFactory: AutocompleteMemberPresenter.Factory,
+        private val autocompleteRoomPresenter: AutocompleteRoomPresenter,
+        private val autocompleteGroupPresenter: AutocompleteGroupPresenter,
+        private val autocompleteEmojiPresenter: AutocompleteEmojiPresenter
+) {
+
+    @AssistedInject.Factory
+    interface Factory {
+        fun create(roomId: String): AutoCompleter
+    }
+
+    private lateinit var editText: EditText
+
+    fun enterSpecialMode() {
+        commandAutocompletePolicy.enabled = false
+    }
+
+    fun exitSpecialMode() {
+        commandAutocompletePolicy.enabled = true
+    }
+
+    private val glideRequests by lazy {
+        GlideApp.with(editText)
+    }
+
+    fun setup(editText: EditText) {
+        this.editText = editText
+        val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(editText.context, R.attr.riotx_background))
+        setupCommands(backgroundDrawable, editText)
+        setupMembers(backgroundDrawable, editText)
+        setupGroups(backgroundDrawable, editText)
+        setupEmojis(backgroundDrawable, editText)
+        setupRooms(backgroundDrawable, editText)
+    }
+
+    private fun setupCommands(backgroundDrawable: Drawable, editText: EditText) {
+        Autocomplete.on(editText)
+                .with(commandAutocompletePolicy)
+                .with(autocompleteCommandPresenter)
+                .with(ELEVATION)
+                .with(backgroundDrawable)
+                .with(object : AutocompleteCallback {
+                    override fun onPopupItemClicked(editable: Editable, item: Command): Boolean {
+                        editable.clear()
+                        editable
+                                .append(item.command)
+                                .append(" ")
+                        return true
+                    }
+
+                    override fun onPopupVisibilityChanged(shown: Boolean) {
+                    }
+                })
+                .build()
+    }
+
+    private fun setupMembers(backgroundDrawable: ColorDrawable, editText: EditText) {
+        val autocompleteMemberPresenter = autocompleteMemberPresenterFactory.create(roomId)
+        Autocomplete.on(editText)
+                .with(CharPolicy('@', true))
+                .with(autocompleteMemberPresenter)
+                .with(ELEVATION)
+                .with(backgroundDrawable)
+                .with(object : AutocompleteCallback {
+                    override fun onPopupItemClicked(editable: Editable, item: RoomMember): Boolean {
+                        insertMatrixItem(editText, editable, "@", item.toMatrixItem())
+                        return true
+                    }
+
+                    override fun onPopupVisibilityChanged(shown: Boolean) {
+                    }
+                })
+                .build()
+    }
+
+    private fun setupRooms(backgroundDrawable: ColorDrawable, editText: EditText) {
+        Autocomplete.on(editText)
+                .with(CharPolicy('#', true))
+                .with(autocompleteRoomPresenter)
+                .with(ELEVATION)
+                .with(backgroundDrawable)
+                .with(object : AutocompleteCallback {
+                    override fun onPopupItemClicked(editable: Editable, item: RoomSummary): Boolean {
+                        insertMatrixItem(editText, editable, "#", item.toRoomAliasMatrixItem())
+                        return true
+                    }
+
+                    override fun onPopupVisibilityChanged(shown: Boolean) {
+                    }
+                })
+                .build()
+    }
+
+    private fun setupGroups(backgroundDrawable: ColorDrawable, editText: EditText) {
+        Autocomplete.on(editText)
+                .with(CharPolicy('+', true))
+                .with(autocompleteGroupPresenter)
+                .with(ELEVATION)
+                .with(backgroundDrawable)
+                .with(object : AutocompleteCallback {
+                    override fun onPopupItemClicked(editable: Editable, item: GroupSummary): Boolean {
+                        insertMatrixItem(editText, editable, "+", item.toMatrixItem())
+                        return true
+                    }
+
+                    override fun onPopupVisibilityChanged(shown: Boolean) {
+                    }
+                })
+                .build()
+    }
+
+    private fun setupEmojis(backgroundDrawable: Drawable, editText: EditText) {
+        Autocomplete.on(editText)
+                .with(CharPolicy(':', false))
+                .with(autocompleteEmojiPresenter)
+                .with(ELEVATION)
+                .with(backgroundDrawable)
+                .with(object : AutocompleteCallback {
+                    override fun onPopupItemClicked(editable: Editable, item: String): Boolean {
+                        // Detect last ":" and remove it
+                        var startIndex = editable.lastIndexOf(":")
+                        if (startIndex == -1) {
+                            startIndex = 0
+                        }
+
+                        // Detect next word separator
+                        var endIndex = editable.indexOf(" ", startIndex)
+                        if (endIndex == -1) {
+                            endIndex = editable.length
+                        }
+
+                        // Replace the word by its completion
+                        editable.replace(startIndex, endIndex, item)
+                        return true
+                    }
+
+                    override fun onPopupVisibilityChanged(shown: Boolean) {
+                    }
+                })
+                .build()
+    }
+
+    private fun insertMatrixItem(editText: EditText, editable: Editable, firstChar: String, matrixItem: MatrixItem) {
+        // Detect last firstChar and remove it
+        var startIndex = editable.lastIndexOf(firstChar)
+        if (startIndex == -1) {
+            startIndex = 0
+        }
+
+        // Detect next word separator
+        var endIndex = editable.indexOf(" ", startIndex)
+        if (endIndex == -1) {
+            endIndex = editable.length
+        }
+
+        // Replace the word by its completion
+        val displayName = matrixItem.getBestName()
+
+        // with a trailing space
+        editable.replace(startIndex, endIndex, "$displayName ")
+
+        // Add the span
+        val span = PillImageSpan(
+                glideRequests,
+                avatarRenderer,
+                editText.context,
+                matrixItem
+        )
+        span.bind(editText)
+
+        editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
+    }
+
+    companion object {
+        private const val ELEVATION = 6f
+    }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/JumpToBottomViewVisibilityManager.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/JumpToBottomViewVisibilityManager.kt
new file mode 100644
index 0000000000..4be5502678
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/JumpToBottomViewVisibilityManager.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.home.room.detail
+
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.floatingactionbutton.FloatingActionButton
+import im.vector.riotx.core.utils.Debouncer
+import timber.log.Timber
+
+/**
+ * Show or hide the jumpToBottomView, depending on the scrolling and if the timeline is displaying the more recent event
+ * - When user scrolls up (i.e. going to the past): hide
+ * - When user scrolls down: show if not displaying last event
+ * - When user stops scrolling: show if not displaying last event
+ */
+class JumpToBottomViewVisibilityManager(
+        private val jumpToBottomView: FloatingActionButton,
+        private val debouncer: Debouncer,
+        recyclerView: RecyclerView,
+        private val layoutManager: LinearLayoutManager) {
+
+    init {
+        recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
+            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+                debouncer.cancel("jump_to_bottom_visibility")
+
+                val scrollingToPast = dy < 0
+
+                if (scrollingToPast) {
+                    jumpToBottomView.hide()
+                } else {
+                    maybeShowJumpToBottomViewVisibility()
+                }
+            }
+
+            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
+                when (newState) {
+                    RecyclerView.SCROLL_STATE_IDLE     -> {
+                        maybeShowJumpToBottomViewVisibilityWithDelay()
+                    }
+                    RecyclerView.SCROLL_STATE_DRAGGING,
+                    RecyclerView.SCROLL_STATE_SETTLING -> Unit
+                }
+            }
+        })
+    }
+
+    fun maybeShowJumpToBottomViewVisibilityWithDelay() {
+        debouncer.debounce("jump_to_bottom_visibility", 250, Runnable {
+            maybeShowJumpToBottomViewVisibility()
+        })
+    }
+
+    private fun maybeShowJumpToBottomViewVisibility() {
+        Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}")
+        if (layoutManager.findFirstVisibleItemPosition() != 0) {
+            jumpToBottomView.show()
+        } else {
+            jumpToBottomView.hide()
+        }
+    }
+}
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 0efbe255fe..eaea0017c6 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
@@ -20,12 +20,10 @@ import android.annotation.SuppressLint
 import android.app.Activity.RESULT_OK
 import android.content.DialogInterface
 import android.content.Intent
-import android.graphics.drawable.ColorDrawable
 import android.net.Uri
 import android.os.Build
 import android.os.Bundle
 import android.os.Parcelable
-import android.text.Editable
 import android.text.Spannable
 import android.view.*
 import android.widget.TextView
@@ -52,25 +50,18 @@ import com.github.piasy.biv.BigImageViewer
 import com.github.piasy.biv.loader.ImageLoader
 import com.google.android.material.snackbar.Snackbar
 import com.google.android.material.textfield.TextInputEditText
-import com.otaliastudios.autocomplete.Autocomplete
-import com.otaliastudios.autocomplete.AutocompleteCallback
-import com.otaliastudios.autocomplete.CharPolicy
 import im.vector.matrix.android.api.permalinks.PermalinkFactory
 import im.vector.matrix.android.api.session.Session
 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.group.model.GroupSummary
 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.message.*
 import im.vector.matrix.android.api.session.room.send.SendState
 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.getLastMessageContent
-import im.vector.matrix.android.api.session.user.model.User
 import im.vector.matrix.android.api.util.MatrixItem
 import im.vector.matrix.android.api.util.toMatrixItem
-import im.vector.matrix.android.api.util.toRoomAliasMatrixItem
 import im.vector.riotx.R
 import im.vector.riotx.core.dialogs.withColoredButton
 import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer
@@ -84,19 +75,11 @@ import im.vector.riotx.core.utils.*
 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.autocomplete.command.AutocompleteCommandPresenter
-import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy
-import im.vector.riotx.features.autocomplete.group.AutocompleteGroupPresenter
-import im.vector.riotx.features.autocomplete.room.AutocompleteRoomPresenter
-import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter
 import im.vector.riotx.features.command.Command
 import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
 import im.vector.riotx.features.home.AvatarRenderer
 import im.vector.riotx.features.home.getColorFromUserId
-import im.vector.riotx.features.home.room.detail.composer.TextComposerAction
 import im.vector.riotx.features.home.room.detail.composer.TextComposerView
-import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel
-import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState
 import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
 import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
 import im.vector.riotx.features.home.room.detail.timeline.action.EventSharedAction
@@ -118,7 +101,6 @@ import im.vector.riotx.features.permalink.PermalinkHandler
 import im.vector.riotx.features.reactions.EmojiReactionPickerActivity
 import im.vector.riotx.features.settings.VectorPreferences
 import im.vector.riotx.features.share.SharedData
-import im.vector.riotx.features.themes.ThemeUtils
 import io.reactivex.android.schedulers.AndroidSchedulers
 import io.reactivex.schedulers.Schedulers
 import kotlinx.android.parcel.Parcelize
@@ -143,23 +125,15 @@ class RoomDetailFragment @Inject constructor(
         private val session: Session,
         private val avatarRenderer: AvatarRenderer,
         private val timelineEventController: TimelineEventController,
-        private val commandAutocompletePolicy: CommandAutocompletePolicy,
-        private val autocompleteCommandPresenter: AutocompleteCommandPresenter,
-        private val autocompleteUserPresenter: AutocompleteUserPresenter,
-        private val autocompleteRoomPresenter: AutocompleteRoomPresenter,
-        private val autocompleteGroupPresenter: AutocompleteGroupPresenter,
+        autoCompleterFactory: AutoCompleter.Factory,
         private val permalinkHandler: PermalinkHandler,
         private val notificationDrawerManager: NotificationDrawerManager,
         val roomDetailViewModelFactory: RoomDetailViewModel.Factory,
-        val textComposerViewModelFactory: TextComposerViewModel.Factory,
         private val eventHtmlRenderer: EventHtmlRenderer,
         private val vectorPreferences: VectorPreferences
 ) :
         VectorBaseFragment(),
         TimelineEventController.Callback,
-        AutocompleteUserPresenter.Callback,
-        AutocompleteRoomPresenter.Callback,
-        AutocompleteGroupPresenter.Callback,
         VectorInviteView.Callback,
         JumpToReadMarkerView.Callback,
         AttachmentTypeSelectorView.Callback,
@@ -189,9 +163,10 @@ class RoomDetailFragment @Inject constructor(
         GlideApp.with(this)
     }
 
+    private val autoCompleter: AutoCompleter by lazy {
+        autoCompleterFactory.create(roomDetailArgs.roomId)
+    }
     private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel()
-    private val textComposerViewModel: TextComposerViewModel by fragmentViewModel()
-
     private val debouncer = Debouncer(createUIHandler())
 
     private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
@@ -203,6 +178,7 @@ class RoomDetailFragment @Inject constructor(
 
     private lateinit var sharedActionViewModel: MessageSharedActionViewModel
     private lateinit var layoutManager: LinearLayoutManager
+    private lateinit var jumpToBottomViewVisibilityManager: JumpToBottomViewVisibilityManager
     private var modelBuildListener: OnModelBuildFinishedListener? = null
 
     private lateinit var attachmentsHelper: AttachmentsHelper
@@ -226,9 +202,9 @@ class RoomDetailFragment @Inject constructor(
         setupNotificationView()
         setupJumpToReadMarkerView()
         setupJumpToBottomView()
+
         roomDetailViewModel.subscribe { renderState(it) }
-        textComposerViewModel.subscribe { renderTextComposerState(it) }
-        roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) }
+        roomDetailViewModel.sendMessageResultLiveData.observeEvent(viewLifecycleOwner) { renderSendMessageResult(it) }
 
         roomDetailViewModel.nonBlockingPopAlert.observeEvent(this) { pair ->
             val message = requireContext().getString(pair.first, *pair.second.toTypedArray())
@@ -271,9 +247,9 @@ class RoomDetailFragment @Inject constructor(
         roomDetailViewModel.selectSubscribe(RoomDetailViewState::sendMode) { mode ->
             when (mode) {
                 is SendMode.REGULAR -> renderRegularMode(mode.text)
-                is SendMode.EDIT    -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text)
-                is SendMode.QUOTE   -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.quote, mode.text)
-                is SendMode.REPLY   -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text)
+                is SendMode.EDIT -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text)
+                is SendMode.QUOTE -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.quote, mode.text)
+                is SendMode.REPLY -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text)
             }
         }
 
@@ -300,9 +276,9 @@ 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.Text -> roomDetailViewModel.handle(RoomDetailAction.SendMessage(sharedData.text, false))
                 is SharedData.Attachments -> roomDetailViewModel.handle(RoomDetailAction.SendMedia(sharedData.attachmentData))
-                null                      -> Timber.v("No share data to process")
+                null -> Timber.v("No share data to process")
             }
         }
     }
@@ -326,14 +302,19 @@ class RoomDetailFragment @Inject constructor(
         jumpToBottomView.setOnClickListener {
             roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState)
             jumpToBottomView.visibility = View.INVISIBLE
-            withState(roomDetailViewModel) { state ->
-                if (state.timeline?.isLive == false) {
-                    state.timeline.restartWithEventId(null)
-                } else {
-                    layoutManager.scrollToPosition(0)
-                }
+            if (!roomDetailViewModel.timeline.isLive) {
+                roomDetailViewModel.timeline.restartWithEventId(null)
+            } else {
+                layoutManager.scrollToPosition(0)
             }
         }
+
+        jumpToBottomViewVisibilityManager = JumpToBottomViewVisibilityManager(
+                jumpToBottomView,
+                debouncer,
+                recyclerView,
+                layoutManager
+        )
     }
 
     private fun setupJumpToReadMarkerView() {
@@ -398,7 +379,7 @@ class RoomDetailFragment @Inject constructor(
     }
 
     private fun renderRegularMode(text: String) {
-        commandAutocompletePolicy.enabled = true
+        autoCompleter.exitSpecialMode()
         composerLayout.collapse()
 
         updateComposerText(text)
@@ -409,7 +390,7 @@ class RoomDetailFragment @Inject constructor(
                                   @DrawableRes iconRes: Int,
                                   @StringRes descriptionRes: Int,
                                   defaultContent: String) {
-        commandAutocompletePolicy.enabled = false
+        autoCompleter.enterSpecialMode()
         // switch to expanded bar
         composerLayout.composerRelatedMessageTitle.apply {
             text = event.getDisambiguatedDisplayName()
@@ -483,6 +464,9 @@ class RoomDetailFragment @Inject constructor(
 // PRIVATE METHODS *****************************************************************************
 
     private fun setupRecyclerView() {
+        timelineEventController.callback = this
+        timelineEventController.timeline = roomDetailViewModel.timeline
+
         val epoxyVisibilityTracker = EpoxyVisibilityTracker()
         epoxyVisibilityTracker.attach(recyclerView)
         layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
@@ -497,27 +481,11 @@ class RoomDetailFragment @Inject constructor(
             it.dispatchTo(scrollOnNewMessageCallback)
             it.dispatchTo(scrollOnHighlightedEventCallback)
             updateJumpToReadMarkerViewVisibility()
-            updateJumpToBottomViewVisibility()
+            jumpToBottomViewVisibilityManager.maybeShowJumpToBottomViewVisibilityWithDelay()
         }
         timelineEventController.addModelBuildListener(modelBuildListener)
         recyclerView.adapter = timelineEventController.adapter
 
-        recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
-            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
-                when (newState) {
-                    RecyclerView.SCROLL_STATE_IDLE     -> {
-                        updateJumpToBottomViewVisibility()
-                    }
-                    RecyclerView.SCROLL_STATE_DRAGGING,
-                    RecyclerView.SCROLL_STATE_SETTLING -> {
-                        jumpToBottomView.hide()
-                    }
-                }
-            }
-        })
-
-        timelineEventController.callback = this
-
         if (vectorPreferences.swipeToReplyIsEnabled()) {
             val quickReplyHandler = object : RoomMessageTouchHelperCallback.QuickReplayHandler {
                 override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
@@ -534,7 +502,7 @@ class RoomDetailFragment @Inject constructor(
                         is MessageTextItem -> {
                             return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED
                         }
-                        else               -> false
+                        else -> false
                     }
                 }
             }
@@ -549,9 +517,9 @@ class RoomDetailFragment @Inject constructor(
             withState(roomDetailViewModel) {
                 val showJumpToUnreadBanner = when (it.unreadState) {
                     UnreadState.Unknown,
-                    UnreadState.HasNoUnread            -> false
+                    UnreadState.HasNoUnread -> false
                     is UnreadState.ReadMarkerNotLoaded -> true
-                    is UnreadState.HasUnread           -> {
+                    is UnreadState.HasUnread -> {
                         if (it.canShowJumpToReadMarker) {
                             val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
                             val positionOfReadMarker = timelineEventController.getPositionOfReadMarker()
@@ -565,182 +533,13 @@ class RoomDetailFragment @Inject constructor(
                         }
                     }
                 }
-                jumpToReadMarkerView.isVisible = showJumpToUnreadBanner
+                jumpToReadMarkerView?.isVisible = showJumpToUnreadBanner
             }
         }
     }
 
-    private fun updateJumpToBottomViewVisibility() {
-        debouncer.debounce("jump_to_bottom_visibility", 250, Runnable {
-            Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}")
-            if (layoutManager.findFirstVisibleItemPosition() != 0) {
-                jumpToBottomView.show()
-            } else {
-                jumpToBottomView.hide()
-            }
-        })
-    }
-
     private fun setupComposer() {
-        val elevation = 6f
-        val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(requireContext(), R.attr.riotx_background))
-        Autocomplete.on(composerLayout.composerEditText)
-                .with(commandAutocompletePolicy)
-                .with(autocompleteCommandPresenter)
-                .with(elevation)
-                .with(backgroundDrawable)
-                .with(object : AutocompleteCallback {
-                    override fun onPopupItemClicked(editable: Editable, item: Command): Boolean {
-                        editable.clear()
-                        editable
-                                .append(item.command)
-                                .append(" ")
-                        return true
-                    }
-
-                    override fun onPopupVisibilityChanged(shown: Boolean) {
-                    }
-                })
-                .build()
-
-        autocompleteRoomPresenter.callback = this
-        Autocomplete.on(composerLayout.composerEditText)
-                .with(CharPolicy('#', true))
-                .with(autocompleteRoomPresenter)
-                .with(elevation)
-                .with(backgroundDrawable)
-                .with(object : AutocompleteCallback {
-                    override fun onPopupItemClicked(editable: Editable, item: RoomSummary): Boolean {
-                        // Detect last '#' and remove it
-                        var startIndex = editable.lastIndexOf("#")
-                        if (startIndex == -1) {
-                            startIndex = 0
-                        }
-
-                        // Detect next word separator
-                        var endIndex = editable.indexOf(" ", startIndex)
-                        if (endIndex == -1) {
-                            endIndex = editable.length
-                        }
-
-                        // Replace the word by its completion
-                        val matrixItem = item.toRoomAliasMatrixItem()
-                        val displayName = matrixItem.getBestName()
-
-                        // with a trailing space
-                        editable.replace(startIndex, endIndex, "$displayName ")
-
-                        // Add the span
-                        val span = PillImageSpan(
-                                glideRequests,
-                                avatarRenderer,
-                                requireContext(),
-                                matrixItem
-                        )
-                        span.bind(composerLayout.composerEditText)
-
-                        editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
-
-                        return true
-                    }
-
-                    override fun onPopupVisibilityChanged(shown: Boolean) {
-                    }
-                })
-                .build()
-
-        autocompleteGroupPresenter.callback = this
-        Autocomplete.on(composerLayout.composerEditText)
-                .with(CharPolicy('+', true))
-                .with(autocompleteGroupPresenter)
-                .with(elevation)
-                .with(backgroundDrawable)
-                .with(object : AutocompleteCallback {
-                    override fun onPopupItemClicked(editable: Editable, item: GroupSummary): Boolean {
-                        // Detect last '+' and remove it
-                        var startIndex = editable.lastIndexOf("+")
-                        if (startIndex == -1) {
-                            startIndex = 0
-                        }
-
-                        // Detect next word separator
-                        var endIndex = editable.indexOf(" ", startIndex)
-                        if (endIndex == -1) {
-                            endIndex = editable.length
-                        }
-
-                        // Replace the word by its completion
-                        val matrixItem = item.toMatrixItem()
-                        val displayName = matrixItem.getBestName()
-
-                        // with a trailing space
-                        editable.replace(startIndex, endIndex, "$displayName ")
-
-                        // Add the span
-                        val span = PillImageSpan(
-                                glideRequests,
-                                avatarRenderer,
-                                requireContext(),
-                                matrixItem
-                        )
-                        span.bind(composerLayout.composerEditText)
-
-                        editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
-
-                        return true
-                    }
-
-                    override fun onPopupVisibilityChanged(shown: Boolean) {
-                    }
-                })
-                .build()
-
-        autocompleteUserPresenter.callback = this
-        Autocomplete.on(composerLayout.composerEditText)
-                .with(CharPolicy('@', true))
-                .with(autocompleteUserPresenter)
-                .with(elevation)
-                .with(backgroundDrawable)
-                .with(object : AutocompleteCallback {
-                    override fun onPopupItemClicked(editable: Editable, item: User): Boolean {
-                        // Detect last '@' and remove it
-                        var startIndex = editable.lastIndexOf("@")
-                        if (startIndex == -1) {
-                            startIndex = 0
-                        }
-
-                        // Detect next word separator
-                        var endIndex = editable.indexOf(" ", startIndex)
-                        if (endIndex == -1) {
-                            endIndex = editable.length
-                        }
-
-                        // Replace the word by its completion
-                        val matrixItem = item.toMatrixItem()
-                        val displayName = matrixItem.getBestName()
-
-                        // with a trailing space
-                        editable.replace(startIndex, endIndex, "$displayName ")
-
-                        // Add the span
-                        val span = PillImageSpan(
-                                glideRequests,
-                                avatarRenderer,
-                                requireContext(),
-                                matrixItem
-                        )
-                        span.bind(composerLayout.composerEditText)
-
-                        editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
-
-                        return true
-                    }
-
-                    override fun onPopupVisibilityChanged(shown: Boolean) {
-                    }
-                })
-                .build()
-
+        autoCompleter.setup(composerLayout.composerEditText)
         composerLayout.callback = object : TextComposerView.Callback {
             override fun onAddAttachment() {
                 if (!::attachmentTypeSelector.isInitialized) {
@@ -795,7 +594,7 @@ class RoomDetailFragment @Inject constructor(
         val summary = state.asyncRoomSummary()
         val inviter = state.asyncInviter()
         if (summary?.membership == Membership.JOIN) {
-            scrollOnHighlightedEventCallback.timeline = state.timeline
+            scrollOnHighlightedEventCallback.timeline = roomDetailViewModel.timeline
             timelineEventController.update(state)
             inviteView.visibility = View.GONE
             val uid = session.myUserId
@@ -810,9 +609,10 @@ class RoomDetailFragment @Inject constructor(
         } else if (state.asyncInviter.complete) {
             vectorBaseActivity.finish()
         }
+        val isRoomEncrypted = summary?.isEncrypted ?: false
         if (state.tombstoneEvent == null) {
             composerLayout.visibility = View.VISIBLE
-            composerLayout.setRoomEncrypted(state.isEncrypted)
+            composerLayout.setRoomEncrypted(isRoomEncrypted)
             notificationAreaView.render(NotificationAreaView.State.Hidden)
         } else {
             composerLayout.visibility = View.GONE
@@ -835,12 +635,6 @@ class RoomDetailFragment @Inject constructor(
         }
     }
 
-    private fun renderTextComposerState(state: TextComposerViewState) {
-        autocompleteUserPresenter.render(state.asyncUsers)
-        autocompleteRoomPresenter.render(state.asyncRooms)
-        autocompleteGroupPresenter.render(state.asyncGroups)
-    }
-
     private fun renderTombstoneEventHandling(async: Async) {
         when (async) {
             is Loading -> {
@@ -853,7 +647,7 @@ class RoomDetailFragment @Inject constructor(
                 navigator.openRoom(vectorBaseActivity, async())
                 vectorBaseActivity.finish()
             }
-            is Fail    -> {
+            is Fail -> {
                 vectorBaseActivity.hideWaitingView()
                 vectorBaseActivity.toast(errorFormatter.toHumanReadable(async.error))
             }
@@ -862,23 +656,23 @@ class RoomDetailFragment @Inject constructor(
 
     private fun renderSendMessageResult(sendMessageResult: SendMessageResult) {
         when (sendMessageResult) {
-            is SendMessageResult.MessageSent                -> {
+            is SendMessageResult.MessageSent -> {
                 updateComposerText("")
             }
-            is SendMessageResult.SlashCommandHandled        -> {
+            is SendMessageResult.SlashCommandHandled -> {
                 sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) }
                 updateComposerText("")
             }
-            is SendMessageResult.SlashCommandError          -> {
+            is SendMessageResult.SlashCommandError -> {
                 displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command))
             }
-            is SendMessageResult.SlashCommandUnknown        -> {
+            is SendMessageResult.SlashCommandUnknown -> {
                 displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command))
             }
-            is SendMessageResult.SlashCommandResultOk       -> {
+            is SendMessageResult.SlashCommandResultOk -> {
                 updateComposerText("")
             }
-            is SendMessageResult.SlashCommandResultError    -> {
+            is SendMessageResult.SlashCommandResultError -> {
                 displayCommandError(sendMessageResult.throwable.localizedMessage)
             }
             is SendMessageResult.SlashCommandNotImplemented -> {
@@ -916,7 +710,7 @@ class RoomDetailFragment @Inject constructor(
 
     private fun displayRoomDetailActionResult(result: Async) {
         when (result) {
-            is Fail    -> {
+            is Fail -> {
                 AlertDialog.Builder(requireActivity())
                         .setTitle(R.string.dialog_title_error)
                         .setMessage(errorFormatter.toHumanReadable(result.error))
@@ -927,7 +721,7 @@ class RoomDetailFragment @Inject constructor(
                 when (val data = result.invoke()) {
                     is RoomDetailAction.ReportContent             -> {
                         when {
-                            data.spam          -> {
+                            data.spam -> {
                                 AlertDialog.Builder(requireActivity())
                                         .setTitle(R.string.content_reported_as_spam_title)
                                         .setMessage(R.string.content_reported_as_spam_content)
@@ -949,7 +743,7 @@ class RoomDetailFragment @Inject constructor(
                                         .show()
                                         .withColoredButton(DialogInterface.BUTTON_NEGATIVE)
                             }
-                            else               -> {
+                            else -> {
                                 AlertDialog.Builder(requireActivity())
                                         .setTitle(R.string.content_reported_title)
                                         .setMessage(R.string.content_reported_content)
@@ -1077,14 +871,14 @@ class RoomDetailFragment @Inject constructor(
     override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
         if (allGranted(grantResults)) {
             when (requestCode) {
-                PERMISSION_REQUEST_CODE_DOWNLOAD_FILE   -> {
+                PERMISSION_REQUEST_CODE_DOWNLOAD_FILE -> {
                     val action = roomDetailViewModel.pendingAction
                     if (action != null) {
                         roomDetailViewModel.pendingAction = null
                         roomDetailViewModel.handle(action)
                     }
                 }
-                PERMISSION_REQUEST_CODE_INCOMING_URI    -> {
+                PERMISSION_REQUEST_CODE_INCOMING_URI -> {
                     val pendingUri = roomDetailViewModel.pendingUri
                     if (pendingUri != null) {
                         roomDetailViewModel.pendingUri = null
@@ -1185,43 +979,25 @@ class RoomDetailFragment @Inject constructor(
         roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState)
     }
 
-    // AutocompleteUserPresenter.Callback
-
-    override fun onQueryUsers(query: CharSequence?) {
-        textComposerViewModel.handle(TextComposerAction.QueryUsers(query))
-    }
-
-    // AutocompleteRoomPresenter.Callback
-
-    override fun onQueryRooms(query: CharSequence?) {
-        textComposerViewModel.handle(TextComposerAction.QueryRooms(query))
-    }
-
-    // AutocompleteGroupPresenter.Callback
-
-    override fun onQueryGroups(query: CharSequence?) {
-        textComposerViewModel.handle(TextComposerAction.QueryGroups(query))
-    }
-
     private fun handleActions(action: EventSharedAction) {
         when (action) {
-            is EventSharedAction.AddReaction                -> {
+            is EventSharedAction.AddReaction -> {
                 startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), action.eventId), REACTION_SELECT_REQUEST_CODE)
             }
-            is EventSharedAction.ViewReactions              -> {
+            is EventSharedAction.ViewReactions -> {
                 ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData)
                         .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
             }
-            is EventSharedAction.Copy                       -> {
+            is EventSharedAction.Copy -> {
                 // I need info about the current selected message :/
                 copyToClipboard(requireContext(), action.content, false)
                 val msg = requireContext().getString(R.string.copied_to_clipboard)
                 showSnackWithMessage(msg, Snackbar.LENGTH_SHORT)
             }
-            is EventSharedAction.Delete                     -> {
+            is EventSharedAction.Delete -> {
                 roomDetailViewModel.handle(RoomDetailAction.RedactAction(action.eventId, context?.getString(R.string.event_redacted_by_user_reason)))
             }
-            is EventSharedAction.Share                      -> {
+            is EventSharedAction.Share -> {
                 // TODO current data communication is too limited
                 // Need to now the media type
                 // TODO bad, just POC
@@ -1249,10 +1025,10 @@ class RoomDetailFragment @Inject constructor(
                         }
                 )
             }
-            is EventSharedAction.ViewEditHistory            -> {
+            is EventSharedAction.ViewEditHistory -> {
                 onEditedDecorationClicked(action.messageInformationData)
             }
-            is EventSharedAction.ViewSource                 -> {
+            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
@@ -1263,7 +1039,7 @@ class RoomDetailFragment @Inject constructor(
                         .setPositiveButton(R.string.ok, null)
                         .show()
             }
-            is EventSharedAction.ViewDecryptedSource        -> {
+            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
@@ -1274,31 +1050,31 @@ class RoomDetailFragment @Inject constructor(
                         .setPositiveButton(R.string.ok, null)
                         .show()
             }
-            is EventSharedAction.QuickReact                 -> {
+            is EventSharedAction.QuickReact -> {
                 // eventId,ClickedOn,Add
                 roomDetailViewModel.handle(RoomDetailAction.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add))
             }
-            is EventSharedAction.Edit                       -> {
+            is EventSharedAction.Edit -> {
                 roomDetailViewModel.handle(RoomDetailAction.EnterEditMode(action.eventId, composerLayout.text.toString()))
             }
-            is EventSharedAction.Quote                      -> {
+            is EventSharedAction.Quote -> {
                 roomDetailViewModel.handle(RoomDetailAction.EnterQuoteMode(action.eventId, composerLayout.text.toString()))
             }
-            is EventSharedAction.Reply                      -> {
+            is EventSharedAction.Reply -> {
                 roomDetailViewModel.handle(RoomDetailAction.EnterReplyMode(action.eventId, composerLayout.text.toString()))
             }
-            is EventSharedAction.CopyPermalink              -> {
+            is EventSharedAction.CopyPermalink -> {
                 val permalink = PermalinkFactory.createPermalink(roomDetailArgs.roomId, action.eventId)
                 copyToClipboard(requireContext(), permalink, false)
                 showSnackWithMessage(requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT)
             }
-            is EventSharedAction.Resend                     -> {
+            is EventSharedAction.Resend -> {
                 roomDetailViewModel.handle(RoomDetailAction.ResendMessage(action.eventId))
             }
-            is EventSharedAction.Remove                     -> {
+            is EventSharedAction.Remove -> {
                 roomDetailViewModel.handle(RoomDetailAction.RemoveFailedEcho(action.eventId))
             }
-            is EventSharedAction.ReportContentSpam          -> {
+            is EventSharedAction.ReportContentSpam -> {
                 roomDetailViewModel.handle(RoomDetailAction.ReportContent(
                         action.eventId, action.senderId, "This message is spam", spam = true))
             }
@@ -1306,19 +1082,19 @@ class RoomDetailFragment @Inject constructor(
                 roomDetailViewModel.handle(RoomDetailAction.ReportContent(
                         action.eventId, action.senderId, "This message is inappropriate", inappropriate = true))
             }
-            is EventSharedAction.ReportContentCustom        -> {
+            is EventSharedAction.ReportContentCustom -> {
                 promptReasonToReportContent(action)
             }
-            is EventSharedAction.IgnoreUser                 -> {
+            is EventSharedAction.IgnoreUser -> {
                 roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(action.senderId))
             }
-            is EventSharedAction.OnUrlClicked               -> {
+            is EventSharedAction.OnUrlClicked -> {
                 onUrlClicked(action.url)
             }
-            is EventSharedAction.OnUrlLongClicked           -> {
+            is EventSharedAction.OnUrlLongClicked -> {
                 onUrlLongClicked(action.url)
             }
-            else                                            -> {
+            else -> {
                 Toast.makeText(context, "Action $action is not implemented yet", Toast.LENGTH_LONG).show()
             }
         }
@@ -1426,10 +1202,10 @@ class RoomDetailFragment @Inject constructor(
 
     private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) {
         when (type) {
-            AttachmentTypeSelectorView.Type.CAMERA  -> attachmentsHelper.openCamera()
-            AttachmentTypeSelectorView.Type.FILE    -> attachmentsHelper.selectFile()
+            AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera()
+            AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile()
             AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery()
-            AttachmentTypeSelectorView.Type.AUDIO   -> attachmentsHelper.selectAudio()
+            AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio()
             AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact()
             AttachmentTypeSelectorView.Type.STICKER -> vectorBaseActivity.notImplemented("Adding stickers")
         }
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 6168a2bd5a..79f41fb450 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
@@ -20,7 +20,12 @@ import android.net.Uri
 import androidx.annotation.IdRes
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
-import com.airbnb.mvrx.*
+import com.airbnb.mvrx.Async
+import com.airbnb.mvrx.Fail
+import com.airbnb.mvrx.FragmentViewModelContext
+import com.airbnb.mvrx.MvRxViewModelFactory
+import com.airbnb.mvrx.Success
+import com.airbnb.mvrx.ViewModelContext
 import com.jakewharton.rxrelay2.BehaviorRelay
 import com.jakewharton.rxrelay2.PublishRelay
 import com.squareup.inject.assisted.Assisted
@@ -87,20 +92,21 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
     private val visibleEventsObservable = BehaviorRelay.create()
     private val timelineSettings = if (userPreferencesProvider.shouldShowHiddenEvents()) {
         TimelineSettings(30,
-                filterEdits = false,
-                filterTypes = true,
-                allowedTypes = TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES,
-                buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
+                         filterEdits = false,
+                         filterTypes = true,
+                         allowedTypes = TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES,
+                         buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
     } else {
         TimelineSettings(30,
-                filterEdits = true,
-                filterTypes = true,
-                allowedTypes = TimelineDisplayableEvents.DISPLAYABLE_TYPES,
-                buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
+                         filterEdits = true,
+                         filterTypes = true,
+                         allowedTypes = TimelineDisplayableEvents.DISPLAYABLE_TYPES,
+                         buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
     }
 
     private var timelineEvents = PublishRelay.create>()
-    private var timeline = room.createTimeline(eventId, timelineSettings)
+    var timeline = room.createTimeline(eventId, timelineSettings)
+        private set
 
     private val _viewEvents = PublishDataSource()
     val viewEvents: DataSource = _viewEvents
@@ -136,18 +142,17 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
     }
 
     init {
+        timeline.start()
+        timeline.addListener(this)
+        observeRoomSummary()
+        observeSummaryState()
         getUnreadState()
         observeSyncState()
-        observeRoomSummary()
         observeEventDisplayedActions()
-        observeSummaryState()
         observeDrafts()
         observeUnreadState()
+        room.getRoomSummaryLive()
         room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear()
-        timeline.addListener(this)
-        timeline.start()
-        setState { copy(timeline = this@RoomDetailViewModel.timeline) }
-
         // Inform the SDK that the room is displayed
         session.onRoomDisplayed(initialState.roomId)
     }
@@ -234,23 +239,23 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
                         copy(
                                 // Create a sendMode from a draft and retrieve the TimelineEvent
                                 sendMode = when (draft) {
-                                    is UserDraft.REGULAR -> SendMode.REGULAR(draft.text)
-                                    is UserDraft.QUOTE   -> {
-                                        room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
-                                            SendMode.QUOTE(timelineEvent, draft.text)
-                                        }
-                                    }
-                                    is UserDraft.REPLY   -> {
-                                        room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
-                                            SendMode.REPLY(timelineEvent, draft.text)
-                                        }
-                                    }
-                                    is UserDraft.EDIT    -> {
-                                        room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
-                                            SendMode.EDIT(timelineEvent, draft.text)
-                                        }
-                                    }
-                                } ?: SendMode.REGULAR("")
+                                               is UserDraft.REGULAR -> SendMode.REGULAR(draft.text)
+                                               is UserDraft.QUOTE   -> {
+                                                   room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
+                                                       SendMode.QUOTE(timelineEvent, draft.text)
+                                                   }
+                                               }
+                                               is UserDraft.REPLY   -> {
+                                                   room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
+                                                       SendMode.REPLY(timelineEvent, draft.text)
+                                                   }
+                                               }
+                                               is UserDraft.EDIT    -> {
+                                                   room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
+                                                       SendMode.EDIT(timelineEvent, draft.text)
+                                                   }
+                                               }
+                                           } ?: SendMode.REGULAR("")
                         )
                     }
                 }
@@ -259,7 +264,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
 
     private fun handleTombstoneEvent(action: RoomDetailAction.HandleTombstoneEvent) {
         val tombstoneContent = action.event.getClearContent().toModel()
-                ?: return
+                               ?: return
 
         val roomId = tombstoneContent.replacementRoom ?: ""
         val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN
@@ -311,7 +316,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
         else                     -> false
     }
 
-    // PRIVATE METHODS *****************************************************************************
+// PRIVATE METHODS *****************************************************************************
 
     private fun handleSendMessage(action: RoomDetailAction.SendMessage) {
         withState { state ->
@@ -414,7 +419,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
                 is SendMode.EDIT    -> {
                     // is original event a reply?
                     val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel()?.relatesTo?.inReplyTo?.eventId
-                            ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId
+                                    ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId
                     if (inReplyTo != null) {
                         // TODO check if same content?
                         room.getTimeLineEvent(inReplyTo)?.let {
@@ -423,13 +428,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
                     } else {
                         val messageContent: MessageContent? =
                                 state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
-                                        ?: state.sendMode.timelineEvent.root.getClearContent().toModel()
+                                ?: state.sendMode.timelineEvent.root.getClearContent().toModel()
                         val existingBody = messageContent?.body ?: ""
                         if (existingBody != action.text) {
                             room.editTextMessage(state.sendMode.timelineEvent.root.eventId ?: "",
-                                    messageContent?.type ?: MessageType.MSGTYPE_TEXT,
-                                    action.text,
-                                    action.autoMarkdown)
+                                                 messageContent?.type ?: MessageType.MSGTYPE_TEXT,
+                                                 action.text,
+                                                 action.autoMarkdown)
                         } else {
                             Timber.w("Same message content, do not send edition")
                         }
@@ -440,7 +445,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
                 is SendMode.QUOTE   -> {
                     val messageContent: MessageContent? =
                             state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
-                                    ?: state.sendMode.timelineEvent.root.getClearContent().toModel()
+                            ?: state.sendMode.timelineEvent.root.getClearContent().toModel()
                     val textMsg = messageContent?.body
 
                     val finalText = legacyRiotQuoteText(textMsg, action.text.toString())
@@ -556,7 +561,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
             when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) {
                 null -> room.sendMedias(attachments)
                 else -> _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(tooBigFile.name
-                        ?: tooBigFile.path, tooBigFile.size, maxUploadFileSize)))
+                                                                             ?: tooBigFile.path, tooBigFile.size, maxUploadFileSize)))
             }
         }
     }
@@ -746,7 +751,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
                 .filter { it.isNotEmpty() }
                 .subscribeBy(onNext = { actions ->
                     val bufferedMostRecentDisplayedEvent = actions.maxBy { it.event.displayIndex }?.event
-                            ?: return@subscribeBy
+                                                           ?: return@subscribeBy
                     val globalMostRecentDisplayedEvent = mostRecentDisplayedEvent
                     if (trackUnreadMessages.get()) {
                         if (globalMostRecentDisplayedEvent == null) {
@@ -829,10 +834,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
         room.rx().liveRoomSummary()
                 .unwrap()
                 .execute { async ->
-                    copy(
-                            asyncRoomSummary = async,
-                            isEncrypted = room.isEncrypted()
-                    )
+                    copy(asyncRoomSummary = async)
                 }
     }
 
@@ -918,7 +920,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
 
     override fun onCleared() {
         timeline.dispose()
-        timeline.removeListener(this)
+        timeline.removeAllListeners()
         super.onCleared()
     }
 }
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt
index b2ad29668e..165ef7b625 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt
@@ -21,7 +21,6 @@ import com.airbnb.mvrx.MvRxState
 import com.airbnb.mvrx.Uninitialized
 import im.vector.matrix.android.api.session.events.model.Event
 import im.vector.matrix.android.api.session.room.model.RoomSummary
-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.sync.SyncState
 import im.vector.matrix.android.api.session.user.model.User
@@ -51,11 +50,9 @@ sealed class UnreadState {
 data class RoomDetailViewState(
         val roomId: String,
         val eventId: String?,
-        val timeline: Timeline? = null,
         val asyncInviter: Async = Uninitialized,
         val asyncRoomSummary: Async = Uninitialized,
         val sendMode: SendMode = SendMode.REGULAR(""),
-        val isEncrypted: Boolean = false,
         val tombstoneEvent: Event? = null,
         val tombstoneEventHandling: Async = Uninitialized,
         val syncState: SyncState = SyncState.Idle,
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt
deleted file mode 100644
index f7ec78c6c4..0000000000
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * Copyright 2019 New Vector Ltd
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package im.vector.riotx.features.home.room.detail.composer
-
-import arrow.core.Option
-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.session.Session
-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.user.model.User
-import im.vector.matrix.rx.rx
-import im.vector.riotx.core.platform.VectorViewModel
-import im.vector.riotx.features.home.room.detail.RoomDetailFragment
-import io.reactivex.Observable
-import io.reactivex.functions.BiFunction
-import java.util.concurrent.TimeUnit
-
-typealias AutocompleteQuery = CharSequence
-
-class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: TextComposerViewState,
-                                                        private val session: Session
-) : VectorViewModel(initialState) {
-
-    private val room = session.getRoom(initialState.roomId)!!
-
-    private val usersQueryObservable = BehaviorRelay.create>()
-    private val roomsQueryObservable = BehaviorRelay.create>()
-    private val groupsQueryObservable = BehaviorRelay.create>()
-
-    @AssistedInject.Factory
-    interface Factory {
-        fun create(initialState: TextComposerViewState): TextComposerViewModel
-    }
-
-    companion object : MvRxViewModelFactory {
-
-        @JvmStatic
-        override fun create(viewModelContext: ViewModelContext, state: TextComposerViewState): TextComposerViewModel? {
-            val fragment: RoomDetailFragment = (viewModelContext as FragmentViewModelContext).fragment()
-            return fragment.textComposerViewModelFactory.create(state)
-        }
-    }
-
-    init {
-        observeUsersQuery()
-        observeRoomsQuery()
-        observeGroupsQuery()
-    }
-
-    override fun handle(action: TextComposerAction) {
-        when (action) {
-            is TextComposerAction.QueryUsers  -> handleQueryUsers(action)
-            is TextComposerAction.QueryRooms  -> handleQueryRooms(action)
-            is TextComposerAction.QueryGroups -> handleQueryGroups(action)
-        }
-    }
-
-    private fun handleQueryUsers(action: TextComposerAction.QueryUsers) {
-        val query = Option.fromNullable(action.query)
-        usersQueryObservable.accept(query)
-    }
-
-    private fun handleQueryRooms(action: TextComposerAction.QueryRooms) {
-        val query = Option.fromNullable(action.query)
-        roomsQueryObservable.accept(query)
-    }
-
-    private fun handleQueryGroups(action: TextComposerAction.QueryGroups) {
-        val query = Option.fromNullable(action.query)
-        groupsQueryObservable.accept(query)
-    }
-
-    private fun observeUsersQuery() {
-        Observable.combineLatest, Option, List>(
-                room.rx().liveRoomMemberIds(),
-                usersQueryObservable.throttleLast(300, TimeUnit.MILLISECONDS),
-                BiFunction { roomMemberIds, query ->
-                    val users = roomMemberIds.mapNotNull { session.getUser(it) }
-
-                    val filter = query.orNull()
-                    if (filter.isNullOrBlank()) {
-                        users
-                    } else {
-                        users.filter {
-                            it.displayName?.contains(filter, ignoreCase = true) ?: false
-                        }
-                    }
-                            .sortedBy { it.displayName }
-                }
-        ).execute { async ->
-            copy(
-                    asyncUsers = async
-            )
-        }
-    }
-
-    private fun observeRoomsQuery() {
-        Observable.combineLatest, Option, List>(
-                session.rx().liveRoomSummaries(),
-                roomsQueryObservable.throttleLast(300, TimeUnit.MILLISECONDS),
-                BiFunction { roomSummaries, query ->
-                    val filter = query.orNull() ?: ""
-                    // Keep only room with a canonical alias
-                    roomSummaries
-                            .filter {
-                                it.canonicalAlias?.contains(filter, ignoreCase = true) == true
-                            }
-                            .sortedBy { it.displayName }
-                }
-        ).execute { async ->
-            copy(
-                    asyncRooms = async
-            )
-        }
-    }
-
-    private fun observeGroupsQuery() {
-        Observable.combineLatest, Option, List>(
-                session.rx().liveGroupSummaries(),
-                groupsQueryObservable.throttleLast(300, TimeUnit.MILLISECONDS),
-                BiFunction { groupSummaries, query ->
-                    val filter = query.orNull()
-                    if (filter.isNullOrBlank()) {
-                        groupSummaries
-                    } else {
-                        groupSummaries
-                                .filter {
-                                    it.groupId.contains(filter, ignoreCase = true)
-                                }
-                    }
-                            .sortedBy { it.displayName }
-                }
-        ).execute { async ->
-            copy(
-                    asyncGroups = async
-            )
-        }
-    }
-}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewState.kt
deleted file mode 100644
index e863970afe..0000000000
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewState.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright 2019 New Vector Ltd
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package im.vector.riotx.features.home.room.detail.composer
-
-import com.airbnb.mvrx.Async
-import com.airbnb.mvrx.MvRxState
-import com.airbnb.mvrx.Uninitialized
-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.user.model.User
-import im.vector.riotx.features.home.room.detail.RoomDetailArgs
-
-data class TextComposerViewState(val roomId: String,
-                                 val asyncUsers: Async> = Uninitialized,
-                                 val asyncRooms: Async> = Uninitialized,
-                                 val asyncGroups: Async> = Uninitialized
-) : MvRxState {
-
-    constructor(args: RoomDetailArgs) : this(roomId = args.roomId)
-}
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 b41188bf94..0cdf51673b 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
@@ -99,12 +99,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
     private val modelCache = arrayListOf()
     private var currentSnapshot: List = emptyList()
     private var inSubmitList: Boolean = false
-    private var timeline: Timeline? = null
     private var unreadState: UnreadState = UnreadState.Unknown
     private var positionOfReadMarker: Int? = null
     private var eventIdToHighlight: String? = null
 
     var callback: Callback? = null
+    var timeline: Timeline? = null
 
     private val listUpdateCallback = object : ListUpdateCallback {
 
@@ -180,10 +180,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
     }
 
     fun update(viewState: RoomDetailViewState) {
-        if (timeline?.timelineID != viewState.timeline?.timelineID) {
-            timeline = viewState.timeline
-            timeline?.addListener(this)
-        }
         var requestModelBuild = false
         if (eventIdToHighlight != viewState.highlightedEventId) {
             // Clear cache to force a refresh
@@ -209,6 +205,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
 
     override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
         super.onAttachedToRecyclerView(recyclerView)
+        timeline?.addListener(this)
         timelineMediaSizeProvider.recyclerView = recyclerView
     }
 
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt
index 939564e780..9a2fb4b6de 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt
@@ -93,7 +93,7 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid
         }
 
         // Action
-        state.actions()?.forEachIndexed { index, action ->
+        state.actions.forEachIndexed { index, action ->
             if (action is EventSharedAction.Separator) {
                 bottomSheetSeparatorItem {
                     id("separator_$index")
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 3c7ecfc97a..51b8d81e49 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,8 +28,7 @@ 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
 import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited
-import im.vector.matrix.android.api.util.Optional
-import im.vector.matrix.rx.RxRoom
+import im.vector.matrix.rx.rx
 import im.vector.matrix.rx.unwrap
 import im.vector.riotx.R
 import im.vector.riotx.core.extensions.canReact
@@ -39,6 +38,8 @@ import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventForm
 import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
 import im.vector.riotx.features.html.EventHtmlRenderer
 import im.vector.riotx.features.html.VectorHtmlCompressor
+import im.vector.riotx.features.reactions.data.EmojiDataSource
+import im.vector.riotx.features.settings.VectorPreferences
 import java.text.SimpleDateFormat
 import java.util.*
 
@@ -59,7 +60,7 @@ data class MessageActionState(
         // For quick reactions
         val quickStates: Async> = Uninitialized,
         // For actions
-        val actions: Async> = Uninitialized,
+        val actions: List = emptyList(),
         val expendedReportContentMenu: Boolean = false
 ) : MvRxState {
 
@@ -83,7 +84,8 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
                                                           private val htmlCompressor: VectorHtmlCompressor,
                                                           private val session: Session,
                                                           private val noticeEventFormatter: NoticeEventFormatter,
-                                                          private val stringProvider: StringProvider
+                                                          private val stringProvider: StringProvider,
+                                                          private val vectorPreferences: VectorPreferences
 ) : VectorViewModel(initialState) {
 
     private val eventId = initialState.eventId
@@ -96,9 +98,6 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
     }
 
     companion object : MvRxViewModelFactory {
-
-        val quickEmojis = listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀")
-
         @JvmStatic
         override fun create(viewModelContext: ViewModelContext, state: MessageActionState): MessageActionsViewModel? {
             val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
@@ -109,7 +108,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
     init {
         observeEvent()
         observeReactions()
-        observeEventAction()
+        observeTimelineEventState()
     }
 
     override fun handle(action: MessageActionsAction) {
@@ -128,35 +127,20 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
 
     private fun observeEvent() {
         if (room == null) return
-        RxRoom(room)
+        room.rx()
                 .liveTimelineEvent(eventId)
                 .unwrap()
                 .execute {
-                    copy(
-                            timelineEvent = it,
-                            messageBody = computeMessageBody(it)
-                    )
-                }
-    }
-
-    private fun observeEventAction() {
-        if (room == null) return
-        RxRoom(room)
-                .liveTimelineEvent(eventId)
-                .map {
-                    actionsForEvent(it)
-                }
-                .execute {
-                    copy(actions = it)
+                    copy(timelineEvent = it)
                 }
     }
 
     private fun observeReactions() {
         if (room == null) return
-        RxRoom(room)
+        room.rx()
                 .liveAnnotationSummary(eventId)
                 .map { annotations ->
-                    quickEmojis.map { emoji ->
+                    EmojiDataSource.quickEmojis.map { emoji ->
                         ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe ?: false)
                     }
                 }
@@ -165,11 +149,19 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
                 }
     }
 
-    private fun computeMessageBody(timelineEvent: Async): CharSequence? {
-        return when (timelineEvent()?.root?.getClearType()) {
+    private fun observeTimelineEventState() {
+        asyncSubscribe(MessageActionState::timelineEvent) { timelineEvent ->
+            val computedMessage = computeMessageBody(timelineEvent)
+            val actions = actionsForEvent(timelineEvent)
+            setState { copy(messageBody = computedMessage, actions = actions) }
+        }
+    }
+
+    private fun computeMessageBody(timelineEvent: TimelineEvent): CharSequence? {
+        return when (timelineEvent.root.getClearType()) {
             EventType.MESSAGE,
             EventType.STICKER     -> {
-                val messageContent: MessageContent? = timelineEvent()?.getLastMessageContent()
+                val messageContent: MessageContent? = timelineEvent.getLastMessageContent()
                 if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
                     val html = messageContent.formattedBody
                             ?.takeIf { it.isNotBlank() }
@@ -192,41 +184,39 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
             EventType.CALL_INVITE,
             EventType.CALL_HANGUP,
             EventType.CALL_ANSWER -> {
-                timelineEvent()?.let { noticeEventFormatter.format(it) }
+                noticeEventFormatter.format(timelineEvent)
             }
             else                  -> null
         }
     }
 
-    private fun actionsForEvent(optionalEvent: Optional): List {
-        val event = optionalEvent.getOrNull() ?: return emptyList()
-
-        val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent.toModel()
-                ?: event.root.getClearContent().toModel()
+    private fun actionsForEvent(timelineEvent: TimelineEvent): List {
+        val messageContent: MessageContent? = timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
+                ?: timelineEvent.root.getClearContent().toModel()
         val type = messageContent?.type
 
         return arrayListOf().apply {
-            if (event.root.sendState.hasFailed()) {
-                if (canRetry(event)) {
+            if (timelineEvent.root.sendState.hasFailed()) {
+                if (canRetry(timelineEvent)) {
                     add(EventSharedAction.Resend(eventId))
                 }
                 add(EventSharedAction.Remove(eventId))
-            } else if (event.root.sendState.isSending()) {
+            } else if (timelineEvent.root.sendState.isSending()) {
                 // TODO is uploading attachment?
-                if (canCancel(event)) {
+                if (canCancel(timelineEvent)) {
                     add(EventSharedAction.Cancel(eventId))
                 }
-            } else if (event.root.sendState == SendState.SYNCED) {
-                if (!event.root.isRedacted()) {
-                    if (canReply(event, messageContent)) {
+            } else if (timelineEvent.root.sendState == SendState.SYNCED) {
+                if (!timelineEvent.root.isRedacted()) {
+                    if (canReply(timelineEvent, messageContent)) {
                         add(EventSharedAction.Reply(eventId))
                     }
 
-                    if (canEdit(event, session.myUserId)) {
+                    if (canEdit(timelineEvent, session.myUserId)) {
                         add(EventSharedAction.Edit(eventId))
                     }
 
-                    if (canRedact(event, session.myUserId)) {
+                    if (canRedact(timelineEvent, session.myUserId)) {
                         add(EventSharedAction.Delete(eventId))
                     }
 
@@ -235,19 +225,19 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
                         add(EventSharedAction.Copy(messageContent!!.body))
                     }
 
-                    if (event.canReact()) {
+                    if (timelineEvent.canReact()) {
                         add(EventSharedAction.AddReaction(eventId))
                     }
 
-                    if (canQuote(event, messageContent)) {
+                    if (canQuote(timelineEvent, messageContent)) {
                         add(EventSharedAction.Quote(eventId))
                     }
 
-                    if (canViewReactions(event)) {
+                    if (canViewReactions(timelineEvent)) {
                         add(EventSharedAction.ViewReactions(informationData))
                     }
 
-                    if (event.hasBeenEdited()) {
+                    if (timelineEvent.hasBeenEdited()) {
                         add(EventSharedAction.ViewEditHistory(informationData))
                     }
 
@@ -260,29 +250,30 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
                         // TODO
                     }
 
-                    if (event.root.sendState == SendState.SENT) {
+                    if (timelineEvent.root.sendState == SendState.SENT) {
                         // TODO Can be redacted
 
                         // TODO sent by me or sufficient power level
                     }
                 }
 
-                add(EventSharedAction.ViewSource(event.root.toContentStringWithIndent()))
-                if (event.isEncrypted()) {
-                    val decryptedContent = event.root.toClearContentStringWithIndent()
-                            ?: stringProvider.getString(R.string.encryption_information_decryption_error)
-                    add(EventSharedAction.ViewDecryptedSource(decryptedContent))
+                if (vectorPreferences.developerMode()) {
+                    add(EventSharedAction.ViewSource(timelineEvent.root.toContentStringWithIndent()))
+                    if (timelineEvent.isEncrypted()) {
+                        val decryptedContent = timelineEvent.root.toClearContentStringWithIndent()
+                                ?: stringProvider.getString(R.string.encryption_information_decryption_error)
+                        add(EventSharedAction.ViewDecryptedSource(decryptedContent))
+                    }
                 }
                 add(EventSharedAction.CopyPermalink(eventId))
-
-                if (session.myUserId != event.root.senderId) {
+                if (session.myUserId != timelineEvent.root.senderId) {
                     // not sent by me
-                    if (event.root.getClearType() == EventType.MESSAGE) {
-                        add(EventSharedAction.ReportContent(eventId, event.root.senderId))
+                    if (timelineEvent.root.getClearType() == EventType.MESSAGE) {
+                        add(EventSharedAction.ReportContent(eventId, timelineEvent.root.senderId))
                     }
 
                     add(EventSharedAction.Separator)
-                    add(EventSharedAction.IgnoreUser(event.root.senderId))
+                    add(EventSharedAction.IgnoreUser(timelineEvent.root.senderId))
                 }
             }
         }
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt
index 696cc7d904..3080dcb2f4 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt
@@ -69,7 +69,7 @@ class EncryptionItemFactory @Inject constructor(private val stringProvider: Stri
 
     private fun buildNoticeText(event: Event, senderName: String?): CharSequence? {
         return when {
-            EventType.ENCRYPTION == event.getClearType() -> {
+            EventType.STATE_ROOM_ENCRYPTION == event.getClearType() -> {
                 val content = event.content.toModel() ?: return null
                 stringProvider.getString(R.string.notice_end_to_end, senderName, content.algorithm)
             }
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
index 3ff4af27c2..d3503300e1 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
@@ -55,7 +55,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
                 EventType.CALL_ANSWER,
                 EventType.REACTION,
                 EventType.REDACTION,
-                EventType.ENCRYPTION                    -> noticeItemFactory.create(event, highlight, callback)
+                EventType.STATE_ROOM_ENCRYPTION         -> noticeItemFactory.create(event, highlight, callback)
                 // State room create
                 EventType.STATE_ROOM_CREATE             -> roomCreateItemFactory.create(event, callback)
                 // Crypto
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 c8058d0fa8..ae03a8c2f4 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
@@ -135,8 +135,8 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
     }
 
     private fun formatRoomMemberEvent(event: Event, senderName: String?): String? {
-        val eventContent: RoomMember? = event.getClearContent().toModel()
-        val prevEventContent: RoomMember? = event.prevContent.toModel()
+        val eventContent: RoomMemberContent? = event.getClearContent().toModel()
+        val prevEventContent: RoomMemberContent? = event.prevContent.toModel()
         val isMembershipEvent = prevEventContent?.membership != eventContent?.membership
         return if (isMembershipEvent) {
             buildMembershipNotice(event, senderName, eventContent, prevEventContent)
@@ -173,7 +173,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
                 ?: sp.getString(R.string.notice_room_canonical_alias_unset, senderName)
     }
 
-    private fun buildProfileNotice(event: Event, senderName: String?, eventContent: RoomMember?, prevEventContent: RoomMember?): String {
+    private fun buildProfileNotice(event: Event, senderName: String?, eventContent: RoomMemberContent?, prevEventContent: RoomMemberContent?): String {
         val displayText = StringBuilder()
         // Check display name has been changed
         if (eventContent?.displayName != prevEventContent?.displayName) {
@@ -205,7 +205,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
         return displayText.toString()
     }
 
-    private fun buildMembershipNotice(event: Event, senderName: String?, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
+    private fun buildMembershipNotice(event: Event, senderName: String?, eventContent: RoomMemberContent?, prevEventContent: RoomMemberContent?): String? {
         val senderDisplayName = senderName ?: event.senderId ?: ""
         val targetDisplayName = eventContent?.displayName ?: prevEventContent?.displayName ?: event.stateKey ?: ""
         return when (eventContent?.membership) {
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt
index 2864fe6802..a09f61b0a4 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt
@@ -34,7 +34,7 @@ object TimelineDisplayableEvents {
             EventType.CALL_HANGUP,
             EventType.CALL_ANSWER,
             EventType.ENCRYPTED,
-            EventType.ENCRYPTION,
+            EventType.STATE_ROOM_ENCRYPTION,
             EventType.STATE_ROOM_THIRD_PARTY_INVITE,
             EventType.STICKER,
             EventType.STATE_ROOM_CREATE,
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt
index 5ee0576be7..fabdf22d14 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt
@@ -49,7 +49,7 @@ abstract class MessageTextItem : AbsMessageItem() {
         holder.messageView.setOnClickListener(attributes.itemClickListener)
         holder.messageView.setOnLongClickListener(attributes.itemLongClickListener)
         if (searchForPills) {
-            message?.findPillsAndProcess { it.bind(holder.messageView) }
+            message?.findPillsAndProcess(coroutineScope) { it.bind(holder.messageView) }
         }
         val textFuture = PrecomputedTextCompat.getTextFuture(
                 message ?: "",
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 492248985e..043763fd8e 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,14 +25,11 @@ 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.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
+import kotlinx.coroutines.*
 import me.saket.bettermovementmethod.BetterLinkMovementMethod
 
-fun CharSequence.findPillsAndProcess(processBlock: (PillImageSpan) -> Unit) {
-    GlobalScope.launch(Dispatchers.Main) {
+fun CharSequence.findPillsAndProcess(scope: CoroutineScope, processBlock: (PillImageSpan) -> Unit) {
+    scope.launch(Dispatchers.Main) {
         withContext(Dispatchers.IO) {
             toSpannable().let { spannable ->
                 spannable.getSpans(0, spannable.length, PillImageSpan::class.java)
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 e272c1423f..122b95aa52 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
@@ -59,7 +59,8 @@ data class RoomListParams(
 class RoomListFragment @Inject constructor(
         private val roomController: RoomSummaryController,
         val roomListViewModelFactory: RoomListViewModel.Factory,
-        private val notificationDrawerManager: NotificationDrawerManager
+        private val notificationDrawerManager: NotificationDrawerManager,
+        private val sharedViewPool: RecyclerView.RecycledViewPool
 
 ) : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, FabMenuView.Listener {
 
@@ -95,7 +96,6 @@ class RoomListFragment @Inject constructor(
         setupCreateRoomButton()
         setupRecyclerView()
         sharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java)
-
         roomListViewModel.subscribe { renderState(it) }
         roomListViewModel.viewEvents
                 .observe()
@@ -193,6 +193,8 @@ class RoomListFragment @Inject constructor(
         val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
         roomListView.layoutManager = layoutManager
         roomListView.itemAnimator = RoomListAnimator()
+        roomListView.setRecycledViewPool(sharedViewPool)
+        layoutManager.recycleChildrenOnDetach = true
         roomController.listener = this
         modelBuildListener = OnModelBuildFinishedListener { it.dispatchTo(stateRestorer) }
         roomController.addModelBuildListener(modelBuildListener)
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt
index 60a26c8151..5fc33ffbe9 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt
@@ -46,6 +46,7 @@ data class RoomListActionsArgs(
 class RoomListQuickActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), RoomListQuickActionsEpoxyController.Listener {
 
     private lateinit var sharedActionViewModel: RoomListQuickActionsSharedActionViewModel
+    @Inject lateinit var sharedViewPool: RecyclerView.RecycledViewPool
     @Inject lateinit var roomListActionsViewModelFactory: RoomListQuickActionsViewModel.Factory
     @Inject lateinit var roomListActionsEpoxyController: RoomListQuickActionsEpoxyController
     @Inject lateinit var navigator: Navigator
@@ -70,7 +71,7 @@ class RoomListQuickActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), R
     override fun onActivityCreated(savedInstanceState: Bundle?) {
         super.onActivityCreated(savedInstanceState)
         sharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java)
-        recyclerView.configureWith(roomListActionsEpoxyController, hasFixedSize = false)
+        recyclerView.configureWith(roomListActionsEpoxyController, viewPool = sharedViewPool, hasFixedSize = false)
         // Disable item animation
         recyclerView.itemAnimator = null
         roomListActionsEpoxyController.listener = this
diff --git a/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt b/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt
index a609541a62..3d3dcbea94 100644
--- a/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt
+++ b/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt
@@ -88,25 +88,27 @@ class PillImageSpan(private val glideRequests: GlideRequests,
     }
 
     internal fun updateAvatarDrawable(drawable: Drawable?) {
-        pillDrawable.apply {
-            chipIcon = drawable
-        }
-        tv?.get()?.apply {
-            invalidate()
-        }
+        pillDrawable.chipIcon = drawable
+        tv?.get()?.invalidate()
     }
 
     // Private methods *****************************************************************************
 
     private fun createChipDrawable(): ChipDrawable {
         val textPadding = context.resources.getDimension(R.dimen.pill_text_padding)
+        val icon = try {
+            avatarRenderer.getCachedDrawable(glideRequests, matrixItem)
+        } catch (exception: Exception) {
+            avatarRenderer.getPlaceholderDrawable(context, matrixItem)
+        }
+
         return ChipDrawable.createFromResource(context, R.xml.pill_view).apply {
             text = matrixItem.getBestName()
             textEndPadding = textPadding
             textStartPadding = textPadding
             setChipMinHeightResource(R.dimen.pill_min_height)
             setChipIconSizeResource(R.dimen.pill_avatar_size)
-            chipIcon = avatarRenderer.getPlaceholderDrawable(context, matrixItem)
+            chipIcon = icon
             setBounds(0, 0, intrinsicWidth, intrinsicHeight)
         }
     }
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 08ff11217d..48422056b4 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
@@ -120,8 +120,8 @@ class DefaultNavigator @Inject constructor(
         context.startActivity(intent)
     }
 
-    override fun openSettings(context: Context) {
-        val intent = VectorSettingsActivity.getIntent(context)
+    override fun openSettings(context: Context, directAccess: Int) {
+        val intent = VectorSettingsActivity.getIntent(context, directAccess)
         context.startActivity(intent)
     }
 
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 278c8fdba0..60045984c3 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
@@ -19,6 +19,7 @@ package im.vector.riotx.features.navigation
 import android.app.Activity
 import android.content.Context
 import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
+import im.vector.riotx.features.settings.VectorSettingsActivity
 import im.vector.riotx.features.share.SharedData
 
 interface Navigator {
@@ -39,7 +40,7 @@ interface Navigator {
 
     fun openRoomsFiltering(context: Context)
 
-    fun openSettings(context: Context)
+    fun openSettings(context: Context, directAccess: Int = VectorSettingsActivity.EXTRA_DIRECT_ACCESS_ROOT)
 
     fun openDebug(context: Context)
 
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 e38e7d548a..11d770adc4 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
@@ -23,7 +23,7 @@ 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.Membership
-import im.vector.matrix.android.api.session.room.model.RoomMember
+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
@@ -163,7 +163,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
     }
 
     private fun resolveStateRoomEvent(event: Event, session: Session): NotifiableEvent? {
-        val content = event.content?.toModel() ?: return null
+        val content = event.content?.toModel() ?: return null
         val roomId = event.roomId ?: return null
         val dName = event.senderId?.let { session.getUser(it)?.displayName }
         if (Membership.INVITE == content.membership) {
diff --git a/vector/src/main/java/im/vector/riotx/features/rageshake/RageShake.kt b/vector/src/main/java/im/vector/riotx/features/rageshake/RageShake.kt
index 39749be8c2..effed19c59 100644
--- a/vector/src/main/java/im/vector/riotx/features/rageshake/RageShake.kt
+++ b/vector/src/main/java/im/vector/riotx/features/rageshake/RageShake.kt
@@ -19,33 +19,32 @@ package im.vector.riotx.features.rageshake
 import android.content.Context
 import android.hardware.Sensor
 import android.hardware.SensorManager
-import android.preference.PreferenceManager
 import androidx.appcompat.app.AlertDialog
 import androidx.appcompat.app.AppCompatActivity
-import androidx.core.content.edit
 import com.squareup.seismic.ShakeDetector
 import im.vector.riotx.R
+import im.vector.riotx.core.hardware.vibrate
+import im.vector.riotx.features.navigation.Navigator
+import im.vector.riotx.features.settings.VectorPreferences
+import im.vector.riotx.features.settings.VectorSettingsActivity
 import javax.inject.Inject
 
 class RageShake @Inject constructor(private val activity: AppCompatActivity,
-                                    private val bugReporter: BugReporter) : ShakeDetector.Listener {
+                                    private val bugReporter: BugReporter,
+                                    private val navigator: Navigator,
+                                    private val vectorPreferences: VectorPreferences) : ShakeDetector.Listener {
 
     private var shakeDetector: ShakeDetector? = null
 
     private var dialogDisplayed = false
 
+    var interceptor: (() -> Unit)? = null
+
     fun start() {
-        if (!isEnable(activity)) {
-            return
-        }
-
-        val sensorManager = activity.getSystemService(AppCompatActivity.SENSOR_SERVICE) as? SensorManager
-
-        if (sensorManager == null) {
-            return
-        }
+        val sensorManager = activity.getSystemService(AppCompatActivity.SENSOR_SERVICE) as? SensorManager ?: return
 
         shakeDetector = ShakeDetector(this).apply {
+            setSensitivity(vectorPreferences.getRageshakeSensitivity())
             start(sensorManager)
         }
     }
@@ -54,52 +53,43 @@ class RageShake @Inject constructor(private val activity: AppCompatActivity,
         shakeDetector?.stop()
     }
 
-    /**
-     * Enable the feature, and start it
-     */
-    fun enable() {
-        PreferenceManager.getDefaultSharedPreferences(activity).edit {
-            putBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, true)
-        }
-
-        start()
-    }
-
-    /**
-     * Disable the feature, and stop it
-     */
-    fun disable() {
-        PreferenceManager.getDefaultSharedPreferences(activity).edit {
-            putBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, false)
-        }
-
-        stop()
+    fun setSensitivity(sensitivity: Int) {
+        shakeDetector?.setSensitivity(sensitivity)
     }
 
     override fun hearShake() {
-        if (dialogDisplayed) {
-            // Filtered!
-            return
+        val i = interceptor
+        if (i != null) {
+            vibrate(activity)
+            i.invoke()
+        } else {
+            if (dialogDisplayed) {
+                // Filtered!
+                return
+            }
+
+            vibrate(activity)
+            dialogDisplayed = true
+
+            AlertDialog.Builder(activity)
+                    .setMessage(R.string.send_bug_report_alert_message)
+                    .setPositiveButton(R.string.yes) { _, _ -> openBugReportScreen() }
+                    .setNeutralButton(R.string.settings) { _, _ -> openSettings() }
+                    .setOnDismissListener { dialogDisplayed = false }
+                    .setNegativeButton(R.string.no, null)
+                    .show()
         }
-
-        dialogDisplayed = true
-
-        AlertDialog.Builder(activity)
-                .setMessage(R.string.send_bug_report_alert_message)
-                .setPositiveButton(R.string.yes) { _, _ -> openBugReportScreen() }
-                .setNeutralButton(R.string.disable) { _, _ -> disable() }
-                .setOnDismissListener { dialogDisplayed = false }
-                .setNegativeButton(R.string.no, null)
-                .show()
     }
 
     private fun openBugReportScreen() {
         bugReporter.openBugReportScreen(activity)
     }
 
-    companion object {
-        private const val SETTINGS_USE_RAGE_SHAKE_KEY = "SETTINGS_USE_RAGE_SHAKE_KEY"
+    private fun openSettings() {
+        navigator.openSettings(activity, VectorSettingsActivity.EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS)
+    }
 
+    companion object {
         /**
          * Check if the feature is available
          */
@@ -107,12 +97,5 @@ class RageShake @Inject constructor(private val activity: AppCompatActivity,
             return (context.getSystemService(AppCompatActivity.SENSOR_SERVICE) as? SensorManager)
                     ?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null
         }
-
-        /**
-         * Check if the feature is enable (enabled by default)
-         */
-        private fun isEnable(context: Context): Boolean {
-            return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, true)
-        }
     }
 }
diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt
index 01debac5ed..8aa03d9b22 100644
--- a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt
@@ -56,26 +56,10 @@ class EmojiSearchResultViewModel @AssistedInject constructor(
     }
 
     private fun updateQuery(action: EmojiSearchAction.UpdateQuery) {
-        val words = action.queryString.split("\\s".toRegex())
         setState {
             copy(
                     query = action.queryString,
-                    // First add emojis with name matching query, sorted by name
-                    // Then emojis with keyword matching any of the word in the query, sorted by name
-                    results = dataSource.rawData.emojis
-                            .values
-                            .filter { emojiItem ->
-                                emojiItem.name.contains(action.queryString, true)
-                            }
-                            .sortedBy { it.name }
-                            + dataSource.rawData.emojis
-                            .values
-                            .filter { emojiItem ->
-                                words.fold(true, { prev, word ->
-                                    prev && emojiItem.keywords.any { keyword -> keyword.contains(word, true) }
-                                })
-                            }
-                            .sortedBy { it.name }
+                    results = dataSource.filterWith(action.queryString)
             )
         }
     }
diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt
index a326828112..1d7338e2a4 100644
--- a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt
+++ b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt
@@ -18,10 +18,10 @@ package im.vector.riotx.features.reactions.data
 import android.content.res.Resources
 import com.squareup.moshi.Moshi
 import im.vector.riotx.R
-import im.vector.riotx.core.di.ScreenScope
 import javax.inject.Inject
+import javax.inject.Singleton
 
-@ScreenScope
+@Singleton
 class EmojiDataSource @Inject constructor(
         resources: Resources
 ) {
@@ -33,4 +33,51 @@ class EmojiDataSource @Inject constructor(
                         .fromJson(input.bufferedReader().use { it.readText() })
             }
             ?: EmojiData(emptyList(), emptyMap(), emptyMap())
+
+    private val quickReactions = mutableListOf()
+
+    fun filterWith(query: String): List {
+        val words = query.split("\\s".toRegex())
+
+        // First add emojis with name matching query, sorted by name
+        return (rawData.emojis.values
+                .asSequence()
+                .filter { emojiItem ->
+                    emojiItem.name.contains(query, true)
+                }
+                .sortedBy { it.name } +
+                // Then emojis with keyword matching any of the word in the query, sorted by name
+                rawData.emojis.values
+                        .filter { emojiItem ->
+                            words.fold(true, { prev, word ->
+                                prev && emojiItem.keywords.any { keyword -> keyword.contains(word, true) }
+                            })
+                        }
+                        .sortedBy { it.name })
+                // and ensure they will not be present twice
+                .distinct()
+                .toList()
+    }
+
+    fun getQuickReactions(): List {
+        if (quickReactions.isEmpty()) {
+            listOf(
+                    "+1", // 👍
+                    "-1", // 👎
+                    "grinning", // 😄
+                    "tada", // 🎉
+                    "confused", // 😕
+                    "heart", // ❤️
+                    "rocket", // 🚀
+                    "eyes" // 👀
+            )
+                    .mapNotNullTo(quickReactions) { rawData.emojis[it] }
+        }
+
+        return quickReactions
+    }
+
+    companion object {
+        val quickEmojis = listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀")
+    }
 }
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 dcd64c6a46..c4a91a520a 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
@@ -29,6 +29,7 @@ import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRooms
 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.RoomDirectoryData
+import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
 import im.vector.matrix.android.api.util.Cancelable
 import im.vector.matrix.rx.rx
 import im.vector.riotx.core.extensions.postLiveEvent
@@ -79,13 +80,14 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
     }
 
     private fun observeJoinedRooms() {
+        val queryParams = roomSummaryQueryParams {
+            memberships = listOf(Membership.JOIN)
+        }
         session
                 .rx()
-                .liveRoomSummaries()
+                .liveRoomSummaries(queryParams)
                 .subscribe { list ->
                     val joinedRoomIds = list
-                            // Keep only joined room
-                            ?.filter { it.membership == Membership.JOIN }
                             ?.map { it.roomId }
                             ?.toSet()
                             ?: emptySet()
@@ -106,9 +108,9 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
     override fun handle(action: RoomDirectoryAction) {
         when (action) {
             is RoomDirectoryAction.SetRoomDirectoryData -> setRoomDirectoryData(action)
-            is RoomDirectoryAction.FilterWith           -> filterWith(action)
-            RoomDirectoryAction.LoadMore                -> loadMore()
-            is RoomDirectoryAction.JoinRoom             -> joinRoom(action)
+            is RoomDirectoryAction.FilterWith -> filterWith(action)
+            RoomDirectoryAction.LoadMore -> loadMore()
+            is RoomDirectoryAction.JoinRoom -> joinRoom(action)
         }
     }
 
diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomActivity.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomActivity.kt
index a83208c98a..2831457224 100644
--- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomActivity.kt
@@ -22,6 +22,7 @@ import android.os.Bundle
 import androidx.appcompat.widget.Toolbar
 import com.airbnb.mvrx.viewModel
 import im.vector.riotx.R
+import im.vector.riotx.core.di.ScreenComponent
 import im.vector.riotx.core.extensions.addFragment
 import im.vector.riotx.core.platform.ToolbarConfigurable
 import im.vector.riotx.core.platform.VectorBaseActivity
@@ -45,6 +46,10 @@ class CreateRoomActivity : VectorBaseActivity(), ToolbarConfigurable {
         configureToolbar(toolbar)
     }
 
+    override fun injectWith(injector: ScreenComponent) {
+        injector.inject(this)
+    }
+
     override fun initUiAndData() {
         if (isFirstCreation()) {
             addFragment(R.id.simpleFragmentContainer, CreateRoomFragment::class.java)
diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/picker/RoomDirectoryListCreator.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/picker/RoomDirectoryListCreator.kt
index 17693d9ad6..0b02101ecb 100644
--- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/picker/RoomDirectoryListCreator.kt
+++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/picker/RoomDirectoryListCreator.kt
@@ -26,13 +26,11 @@ import javax.inject.Inject
 class RoomDirectoryListCreator @Inject constructor(private val stringArrayProvider: StringArrayProvider,
                                                    private val session: Session) {
 
-    private val credentials = session.sessionParams.credentials
-
     fun computeDirectories(thirdPartyProtocolData: Map): List {
         val result = ArrayList()
 
         // Add user homeserver name
-        val userHsName = credentials.userId.substring(credentials.userId.indexOf(":") + 1)
+        val userHsName = session.myUserId.substringAfter(":")
 
         result.add(RoomDirectoryData(
                 displayName = userHsName,
@@ -48,6 +46,7 @@ class RoomDirectoryListCreator @Inject constructor(private val stringArrayProvid
             if (it != userHsName) {
                 // Use the server name as a default display name
                 result.add(RoomDirectoryData(
+                        homeServer = it,
                         displayName = it,
                         includeAllNetworks = true
                 ))
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 54c86537d2..3de5cb4334 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
@@ -24,6 +24,7 @@ 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.room.model.Membership
+import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
 import im.vector.matrix.rx.rx
 import im.vector.riotx.core.platform.VectorViewModel
 import im.vector.riotx.features.roomdirectory.JoinState
@@ -53,14 +54,15 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted initialState: R
     }
 
     private fun observeJoinedRooms() {
+        val queryParams = roomSummaryQueryParams {
+            memberships = listOf(Membership.JOIN)
+        }
         session
                 .rx()
-                .liveRoomSummaries()
+                .liveRoomSummaries(queryParams)
                 .subscribe { list ->
                     withState { state ->
                         val isRoomJoined = list
-                                // Keep only joined room
-                                ?.filter { it.membership == Membership.JOIN }
                                 ?.map { it.roomId }
                                 ?.toList()
                                 ?.contains(state.roomId) == true
diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt
index ee8c0530d9..72f8cf01dd 100755
--- a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt
+++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt
@@ -23,7 +23,7 @@ import android.net.Uri
 import android.provider.MediaStore
 import androidx.core.content.edit
 import androidx.preference.PreferenceManager
-import im.vector.riotx.BuildConfig
+import com.squareup.seismic.ShakeDetector
 import im.vector.riotx.R
 import im.vector.riotx.features.homeserver.ServerUrlsRepository
 import im.vector.riotx.features.themes.ThemeUtils
@@ -63,8 +63,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
         const val SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY"
         const val SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY"
         const val SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY"
-        const val SETTINGS_DEVICES_LIST_PREFERENCE_KEY = "SETTINGS_DEVICES_LIST_PREFERENCE_KEY"
-        const val SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY = "SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY"
         const val SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY = "SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY"
         const val SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY = "SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY"
         const val SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY"
@@ -150,12 +148,17 @@ class VectorPreferences @Inject constructor(private val context: Context) {
 
         const val SETTINGS_LABS_ALLOW_EXTENDED_LOGS = "SETTINGS_LABS_ALLOW_EXTENDED_LOGS"
 
+        private const val SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY = "SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY"
         private const val SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY = "SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY"
         private const val SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY = "SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY"
+        private const val SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY = "SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY"
 
         // analytics
         const val SETTINGS_USE_ANALYTICS_KEY = "SETTINGS_USE_ANALYTICS_KEY"
+
+        // Rageshake
         const val SETTINGS_USE_RAGE_SHAKE_KEY = "SETTINGS_USE_RAGE_SHAKE_KEY"
+        const val SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY = "SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY"
 
         // other
         const val SETTINGS_MEDIA_SAVING_PERIOD_KEY = "SETTINGS_MEDIA_SAVING_PERIOD_KEY"
@@ -248,8 +251,12 @@ class VectorPreferences @Inject constructor(private val context: Context) {
         }
     }
 
+    fun developerMode(): Boolean {
+        return defaultPrefs.getBoolean(SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY, false)
+    }
+
     fun shouldShowHiddenEvents(): Boolean {
-        return defaultPrefs.getBoolean(SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY, false)
+        return developerMode() && defaultPrefs.getBoolean(SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY, false)
     }
 
     fun swipeToReplyIsEnabled(): Boolean {
@@ -257,7 +264,11 @@ class VectorPreferences @Inject constructor(private val context: Context) {
     }
 
     fun labAllowedExtendedLogging(): Boolean {
-        return defaultPrefs.getBoolean(SETTINGS_LABS_ALLOW_EXTENDED_LOGS, BuildConfig.DEBUG)
+        return developerMode() && defaultPrefs.getBoolean(SETTINGS_LABS_ALLOW_EXTENDED_LOGS, false)
+    }
+
+    fun failFast(): Boolean {
+        return developerMode() && defaultPrefs.getBoolean(SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY, false)
     }
 
     /**
@@ -731,14 +742,10 @@ class VectorPreferences @Inject constructor(private val context: Context) {
     }
 
     /**
-     * Update the rage shake  status.
-     *
-     * @param isEnabled true to enable the rage shake
+     * Get the rage shake sensitivity.
      */
-    fun setUseRageshake(isEnabled: Boolean) {
-        defaultPrefs.edit {
-            putBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, isEnabled)
-        }
+    fun getRageshakeSensitivity(): Int {
+        return defaultPrefs.getInt(SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY, ShakeDetector.SENSITIVITY_MEDIUM)
     }
 
     /**
diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt
index 16484224af..490805ea3c 100755
--- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt
@@ -54,7 +54,12 @@ class VectorSettingsActivity : VectorBaseActivity(),
 
         if (isFirstCreation()) {
             // display the fragment
-            replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG)
+            when (intent.getIntExtra(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOT)) {
+                EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS ->
+                    replaceFragment(R.id.vector_settings_page, VectorSettingsAdvancedSettingsFragment::class.java, null, FRAGMENT_TAG)
+                else                                  ->
+                    replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG)
+            }
         }
 
         supportFragmentManager.addOnBackStackChangedListener(this)
@@ -111,7 +116,13 @@ class VectorSettingsActivity : VectorBaseActivity(),
     }
 
     companion object {
-        fun getIntent(context: Context) = Intent(context, VectorSettingsActivity::class.java)
+        fun getIntent(context: Context, directAccess: Int) = Intent(context, VectorSettingsActivity::class.java)
+                .apply { putExtra(EXTRA_DIRECT_ACCESS, directAccess) }
+
+        private const val EXTRA_DIRECT_ACCESS = "EXTRA_DIRECT_ACCESS"
+
+        const val EXTRA_DIRECT_ACCESS_ROOT = 0
+        const val EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS = 1
 
         private const val FRAGMENT_TAG = "VectorSettingsPreferencesFragment"
     }
diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsAdvancedSettingsFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsAdvancedSettingsFragment.kt
new file mode 100644
index 0000000000..43adcf6335
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsAdvancedSettingsFragment.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2019 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.features.settings
+
+import androidx.preference.Preference
+import androidx.preference.SeekBarPreference
+import im.vector.riotx.R
+import im.vector.riotx.core.platform.VectorBaseActivity
+import im.vector.riotx.core.preference.VectorSwitchPreference
+import im.vector.riotx.features.rageshake.RageShake
+
+class VectorSettingsAdvancedSettingsFragment : VectorSettingsBaseFragment() {
+
+    override var titleRes = R.string.settings_advanced_settings
+    override val preferenceXmlRes = R.xml.vector_settings_advanced_settings
+
+    private var rageshake: RageShake? = null
+
+    override fun onResume() {
+        super.onResume()
+
+        rageshake = (activity as? VectorBaseActivity)?.rageShake
+        rageshake?.interceptor = {
+            (activity as? VectorBaseActivity)?.showSnackbar(getString(R.string.rageshake_detected))
+        }
+    }
+
+    override fun onPause() {
+        super.onPause()
+        rageshake?.interceptor = null
+        rageshake = null
+    }
+
+    override fun bindPref() {
+        val isRageShakeAvailable = RageShake.isAvailable(requireContext())
+
+        if (isRageShakeAvailable) {
+            findPreference(VectorPreferences.SETTINGS_USE_RAGE_SHAKE_KEY)!!
+                    .onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
+
+                if (newValue as? Boolean == true) {
+                    rageshake?.start()
+                } else {
+                    rageshake?.stop()
+                }
+
+                true
+            }
+
+            findPreference(VectorPreferences.SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY)!!
+                    .onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
+                (activity as? VectorBaseActivity)?.let {
+                    val newValueAsInt = newValue as? Int ?: return@OnPreferenceChangeListener true
+
+                    rageshake?.setSensitivity(newValueAsInt)
+                }
+
+                true
+            }
+        } else {
+            findPreference("SETTINGS_RAGE_SHAKE_CATEGORY_KEY")!!.isVisible = false
+        }
+    }
+}
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 6ce928c05d..6c10b8695d 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
@@ -20,13 +20,13 @@ import android.content.Intent
 import android.net.Uri
 import android.provider.Settings
 import androidx.preference.Preference
-import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
 import im.vector.matrix.android.api.Matrix
 import im.vector.riotx.R
 import im.vector.riotx.core.preference.VectorPreference
 import im.vector.riotx.core.utils.copyToClipboard
 import im.vector.riotx.core.utils.displayInWebView
 import im.vector.riotx.features.version.VersionProvider
+import im.vector.riotx.openOssLicensesMenuActivity
 import javax.inject.Inject
 
 class VectorSettingsHelpAboutFragment @Inject constructor(
@@ -107,10 +107,11 @@ class VectorSettingsHelpAboutFragment @Inject constructor(
             false
         }
 
+        // Note: preference is not visible on F-Droid build
         findPreference(VectorPreferences.SETTINGS_OTHER_THIRD_PARTY_NOTICES_PREFERENCE_KEY)!!
                 .onPreferenceClickListener = Preference.OnPreferenceClickListener {
             // See https://developers.google.com/android/guides/opensource
-            startActivity(Intent(requireActivity(), OssLicensesMenuActivity::class.java))
+            openOssLicensesMenuActivity(requireActivity())
             false
         }
     }
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 b7ec443ea0..cf5273d5a4 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
@@ -18,12 +18,8 @@ package im.vector.riotx.features.settings
 
 import android.annotation.SuppressLint
 import android.app.Activity
-import android.content.DialogInterface
 import android.content.Intent
-import android.graphics.Typeface
-import android.view.KeyEvent
 import android.widget.Button
-import android.widget.EditText
 import android.widget.TextView
 import androidx.appcompat.app.AlertDialog
 import androidx.core.view.isVisible
@@ -33,30 +29,19 @@ import androidx.preference.SwitchPreference
 import com.google.android.material.textfield.TextInputEditText
 import im.vector.matrix.android.api.MatrixCallback
 import im.vector.matrix.android.api.extensions.getFingerprintHumanReadable
-import im.vector.matrix.android.api.extensions.sortByLastSeen
-import im.vector.matrix.android.api.failure.Failure
-import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
 import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
 import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
-import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
 import im.vector.riotx.R
 import im.vector.riotx.core.dialogs.ExportKeysDialog
 import im.vector.riotx.core.intent.ExternalIntentData
 import im.vector.riotx.core.intent.analyseIntent
 import im.vector.riotx.core.intent.getFilenameFromUri
 import im.vector.riotx.core.platform.SimpleTextWatcher
-import im.vector.riotx.core.preference.ProgressBarPreference
 import im.vector.riotx.core.preference.VectorPreference
-import im.vector.riotx.core.preference.VectorPreferenceDivider
 import im.vector.riotx.core.utils.*
 import im.vector.riotx.features.crypto.keys.KeysExporter
 import im.vector.riotx.features.crypto.keys.KeysImporter
 import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
-import timber.log.Timber
-import java.text.DateFormat
-import java.text.SimpleDateFormat
-import java.util.Date
-import java.util.Locale
 import javax.inject.Inject
 
 class VectorSettingsSecurityPrivacyFragment @Inject constructor(
@@ -66,9 +51,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
     override var titleRes = R.string.settings_security_and_privacy
     override val preferenceXmlRes = R.xml.vector_settings_security_privacy
 
-    // used to avoid requesting to enter the password for each deletion
-    private var mAccountPassword: String = ""
-
     // devices: device IDs and device names
     private val mDevicesNameList: MutableList = mutableListOf()
 
@@ -78,29 +60,14 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
     private val mCryptographyCategory by lazy {
         findPreference(VectorPreferences.SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY)!!
     }
-    private val mCryptographyCategoryDivider by lazy {
-        findPreference(VectorPreferences.SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY)!!
-    }
     // cryptography manage
     private val mCryptographyManageCategory by lazy {
         findPreference(VectorPreferences.SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY)!!
     }
-    private val mCryptographyManageCategoryDivider by lazy {
-        findPreference(VectorPreferences.SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY)!!
-    }
     // displayed pushers
-    private val mPushersSettingsDivider by lazy {
-        findPreference(VectorPreferences.SETTINGS_NOTIFICATIONS_TARGET_DIVIDER_PREFERENCE_KEY)!!
-    }
     private val mPushersSettingsCategory by lazy {
         findPreference(VectorPreferences.SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY)!!
     }
-    private val mDevicesListSettingsCategory by lazy {
-        findPreference(VectorPreferences.SETTINGS_DEVICES_LIST_PREFERENCE_KEY)!!
-    }
-    private val mDevicesListSettingsCategoryDivider by lazy {
-        findPreference(VectorPreferences.SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY)!!
-    }
     private val cryptoInfoDeviceNamePreference by lazy {
         findPreference(VectorPreferences.SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY)!!
     }
@@ -129,13 +96,16 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
         findPreference(VectorPreferences.SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY)!!
     }
 
+    override fun onResume() {
+        super.onResume()
+        // My device name may have been updated
+        refreshMyDevice()
+    }
+
     override fun bindPref() {
         // Push target
         refreshPushersList()
 
-        // Device list
-        refreshDevicesList()
-
         // Refresh Key Management section
         refreshKeysManagementSection()
 
@@ -151,16 +121,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
                 true
             }
         }
-
-        // Rageshake Management
-        findPreference(VectorPreferences.SETTINGS_USE_RAGE_SHAKE_KEY)!!.let {
-            it.isChecked = vectorPreferences.useRageshake()
-
-            it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
-                vectorPreferences.setUseRageshake(newValue as Boolean)
-                true
-            }
-        }
     }
 
     override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
@@ -353,11 +313,9 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
     private fun removeCryptographyPreference() {
         preferenceScreen.let {
             it.removePreference(mCryptographyCategory)
-            it.removePreference(mCryptographyCategoryDivider)
 
             // Also remove keys management section
             it.removePreference(mCryptographyManageCategory)
-            it.removePreference(mCryptographyManageCategoryDivider)
         }
     }
 
@@ -375,7 +333,8 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
             cryptoInfoDeviceNamePreference.summary = aMyDeviceInfo.displayName
 
             cryptoInfoDeviceNamePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
-                displayDeviceRenameDialog(aMyDeviceInfo)
+                // TODO device can be rename only from the device list screen for the moment
+                // displayDeviceRenameDialog(aMyDeviceInfo)
                 true
             }
 
@@ -428,342 +387,22 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
     // devices list
     // ==============================================================================================================
 
-    private fun removeDevicesPreference() {
-        preferenceScreen.let {
-            it.removePreference(mDevicesListSettingsCategory)
-            it.removePreference(mDevicesListSettingsCategoryDivider)
-        }
-    }
-
-    /**
-     * Force the refresh of the devices list.

- * The devices list is the list of the devices where the user as looged in. - * It can be any mobile device, as any browser. - */ - private fun refreshDevicesList() { - if (session.isCryptoEnabled() && !session.sessionParams.credentials.deviceId.isNullOrEmpty()) { - // display a spinner while loading the devices list - if (0 == mDevicesListSettingsCategory.preferenceCount) { - activity?.let { - val preference = ProgressBarPreference(it) - mDevicesListSettingsCategory.addPreference(preference) - } - } - - session.getDevicesList(object : MatrixCallback { - override fun onSuccess(data: DevicesListResponse) { - if (!isAdded) { - return - } - - if (data.devices?.isEmpty() == true) { - removeDevicesPreference() - } else { - buildDevicesSettings(data.devices!!) - } - } - + private fun refreshMyDevice() { + // TODO Move to a ViewModel... + session.sessionParams.credentials.deviceId?.let { + session.getDeviceInfo(it, object : MatrixCallback { override fun onFailure(failure: Throwable) { - if (!isAdded) { - return - } + // Ignore for this time?... + } - removeDevicesPreference() - onCommonDone(failure.message) + override fun onSuccess(data: DeviceInfo) { + mMyDeviceInfo = data + refreshCryptographyPreference(data) } }) - } else { - removeDevicesPreference() - removeCryptographyPreference() } } - /** - * Build the devices portion of the settings.

- * Each row correspond to a device ID and its corresponding device name. Clicking on the row - * display a dialog containing: the device ID, the device name and the "last seen" information. - * - * @param aDeviceInfoList the list of the devices - */ - private fun buildDevicesSettings(aDeviceInfoList: List) { - var preference: VectorPreference - var typeFaceHighlight: Int - var isNewList = true - val myDeviceId = session.sessionParams.credentials.deviceId - - if (aDeviceInfoList.size == mDevicesNameList.size) { - isNewList = !mDevicesNameList.containsAll(aDeviceInfoList) - } - - if (isNewList) { - var prefIndex = 0 - mDevicesNameList.clear() - mDevicesNameList.addAll(aDeviceInfoList) - - // sort before display: most recent first - mDevicesNameList.sortByLastSeen() - - // start from scratch: remove the displayed ones - mDevicesListSettingsCategory.removeAll() - - for (deviceInfo in mDevicesNameList) { - // set bold to distinguish current device ID - if (null != myDeviceId && myDeviceId == deviceInfo.deviceId) { - mMyDeviceInfo = deviceInfo - typeFaceHighlight = Typeface.BOLD - } else { - typeFaceHighlight = Typeface.NORMAL - } - - // add the edit text preference - preference = VectorPreference(requireActivity()).apply { - mTypeface = typeFaceHighlight - } - - if (null == deviceInfo.deviceId && null == deviceInfo.displayName) { - continue - } else { - if (null != deviceInfo.deviceId) { - preference.title = deviceInfo.deviceId - } - - // display name parameter can be null (new JSON API) - if (null != deviceInfo.displayName) { - preference.summary = deviceInfo.displayName - } - } - - preference.key = DEVICES_PREFERENCE_KEY_BASE + prefIndex - prefIndex++ - - // onClick handler: display device details dialog - preference.onPreferenceClickListener = Preference.OnPreferenceClickListener { - displayDeviceDetailsDialog(deviceInfo) - true - } - - mDevicesListSettingsCategory.addPreference(preference) - } - - refreshCryptographyPreference(mMyDeviceInfo) - } - } - - /** - * Display a dialog containing the device ID, the device name and the "last seen" information.<> - * This dialog allow to delete the corresponding device (see [.displayDeviceDeletionDialog]) - * - * @param aDeviceInfo the device information - */ - private fun displayDeviceDetailsDialog(aDeviceInfo: DeviceInfo) { - activity?.let { - val builder = AlertDialog.Builder(it) - val inflater = it.layoutInflater - val layout = inflater.inflate(R.layout.dialog_device_details, null) - var textView = layout.findViewById(R.id.device_id) - - textView.text = aDeviceInfo.deviceId - - // device name - textView = layout.findViewById(R.id.device_name) - val displayName = if (aDeviceInfo.displayName.isNullOrEmpty()) LABEL_UNAVAILABLE_DATA else aDeviceInfo.displayName - textView.text = displayName - - // last seen info - textView = layout.findViewById(R.id.device_last_seen) - - val lastSeenIp = aDeviceInfo.lastSeenIp?.takeIf { ip -> ip.isNotBlank() } ?: "-" - - val lastSeenTime = aDeviceInfo.lastSeenTs?.let { ts -> - val dateFormatTime = SimpleDateFormat("HH:mm:ss", Locale.ROOT) - val date = Date(ts) - - val time = dateFormatTime.format(date) - val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault()) - - dateFormat.format(date) + ", " + time - } ?: "-" - - val lastSeenInfo = getString(R.string.devices_details_last_seen_format, lastSeenIp, lastSeenTime) - textView.text = lastSeenInfo - - // title & icon - builder.setTitle(R.string.devices_details_dialog_title) - .setIcon(android.R.drawable.ic_dialog_info) - .setView(layout) - .setPositiveButton(R.string.rename) { _, _ -> displayDeviceRenameDialog(aDeviceInfo) } - - // disable the deletion for our own device - if (session.getMyDevice().deviceId != aDeviceInfo.deviceId) { - builder.setNegativeButton(R.string.delete) { _, _ -> deleteDevice(aDeviceInfo) } - } - - builder.setNeutralButton(R.string.cancel, null) - .setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event -> - if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { - dialog.cancel() - return@OnKeyListener true - } - false - }) - .show() - } - } - - /** - * Display an alert dialog to rename a device - * - * @param aDeviceInfoToRename device info - */ - private fun displayDeviceRenameDialog(aDeviceInfoToRename: DeviceInfo) { - activity?.let { - val inflater = it.layoutInflater - val layout = inflater.inflate(R.layout.dialog_base_edit_text, null) - - val input = layout.findViewById(R.id.edit_text) - input.setText(aDeviceInfoToRename.displayName) - - AlertDialog.Builder(it) - .setTitle(R.string.devices_details_device_name) - .setView(layout) - .setPositiveButton(R.string.ok) { _, _ -> - displayLoadingView() - - val newName = input.text.toString() - - session.setDeviceName(aDeviceInfoToRename.deviceId!!, newName, object : MatrixCallback { - override fun onSuccess(data: Unit) { - hideLoadingView() - - // search which preference is updated - val count = mDevicesListSettingsCategory.preferenceCount - - for (i in 0 until count) { - val pref = mDevicesListSettingsCategory.getPreference(i) - - if (aDeviceInfoToRename.deviceId == pref.title) { - pref.summary = newName - } - } - - // detect if the updated device is the current account one - if (cryptoInfoDeviceIdPreference.summary == aDeviceInfoToRename.deviceId) { - cryptoInfoDeviceNamePreference.summary = newName - } - - // Also change the display name in aDeviceInfoToRename, in case of multiple renaming - aDeviceInfoToRename.displayName = newName - } - - override fun onFailure(failure: Throwable) { - onCommonDone(failure.localizedMessage) - } - }) - } - .setNegativeButton(R.string.cancel, null) - .show() - } - } - - /** - * Try to delete a device. - * - * @param deviceInfo the device to delete - */ - private fun deleteDevice(deviceInfo: DeviceInfo) { - val deviceId = deviceInfo.deviceId - if (deviceId == null) { - Timber.e("## displayDeviceDeletionDialog(): sanity check failure") - return - } - - displayLoadingView() - session.deleteDevice(deviceId, object : MatrixCallback { - override fun onSuccess(data: Unit) { - hideLoadingView() - // force settings update - refreshDevicesList() - } - - override fun onFailure(failure: Throwable) { - var isPasswordRequestFound = false - - if (failure is Failure.RegistrationFlowError) { - // We only support LoginFlowTypes.PASSWORD - // Check if we can provide the user password - failure.registrationFlowResponse.flows?.forEach { interactiveAuthenticationFlow -> - isPasswordRequestFound = isPasswordRequestFound || interactiveAuthenticationFlow.stages?.any { it == LoginFlowTypes.PASSWORD } == true - } - - if (isPasswordRequestFound) { - maybeShowDeleteDeviceWithPasswordDialog(deviceId, failure.registrationFlowResponse.session) - } - } - - if (!isPasswordRequestFound) { - // LoginFlowTypes.PASSWORD not supported, and this is the only one RiotX supports so far... - onCommonDone(failure.localizedMessage) - } - } - }) - } - - /** - * Show a dialog to ask for user password, or use a previously entered password. - */ - private fun maybeShowDeleteDeviceWithPasswordDialog(deviceId: String, authSession: String?) { - if (mAccountPassword.isNotEmpty()) { - deleteDeviceWithPassword(deviceId, authSession, mAccountPassword) - } else { - activity?.let { - val inflater = it.layoutInflater - val layout = inflater.inflate(R.layout.dialog_device_delete, null) - val passwordEditText = layout.findViewById(R.id.delete_password) - - AlertDialog.Builder(it) - .setIcon(android.R.drawable.ic_dialog_alert) - .setTitle(R.string.devices_delete_dialog_title) - .setView(layout) - .setPositiveButton(R.string.devices_delete_submit_button_label, DialogInterface.OnClickListener { _, _ -> - if (passwordEditText.toString().isEmpty()) { - it.toast(R.string.error_empty_field_your_password) - return@OnClickListener - } - mAccountPassword = passwordEditText.text.toString() - deleteDeviceWithPassword(deviceId, authSession, mAccountPassword) - }) - .setNegativeButton(R.string.cancel) { _, _ -> - hideLoadingView() - } - .setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event -> - if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { - dialog.cancel() - hideLoadingView() - return@OnKeyListener true - } - false - }) - .show() - } - } - } - - private fun deleteDeviceWithPassword(deviceId: String, authSession: String?, accountPassword: String) { - session.deleteDeviceWithUserPassword(deviceId, authSession, accountPassword, object : MatrixCallback { - override fun onSuccess(data: Unit) { - hideLoadingView() - // force settings update - refreshDevicesList() - } - - override fun onFailure(failure: Throwable) { - // Password is maybe not good - onCommonDone(failure.localizedMessage) - mAccountPassword = "" - } - }) - } - // ============================================================================================================== // pushers list management // ============================================================================================================== @@ -860,6 +499,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( private const val DEVICES_PREFERENCE_KEY_BASE = "DEVICES_PREFERENCE_KEY_BASE" // TODO i18n - private const val LABEL_UNAVAILABLE_DATA = "none" + const val LABEL_UNAVAILABLE_DATA = "none" } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt new file mode 100644 index 0000000000..b6c84ade9a --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.settings.devices + +import android.graphics.Typeface +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.view.isVisible +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.* + +/** + * A list item for Device. + */ +@EpoxyModelClass(layout = R.layout.item_device) +abstract class DeviceItem : VectorEpoxyModel() { + + @EpoxyAttribute + lateinit var deviceInfo: DeviceInfo + + @EpoxyAttribute + var currentDevice = false + + @EpoxyAttribute + var buttonsVisible = false + + @EpoxyAttribute + var itemClickAction: (() -> Unit)? = null + + @EpoxyAttribute + var renameClickAction: (() -> Unit)? = null + + @EpoxyAttribute + var deleteClickAction: (() -> Unit)? = null + + override fun bind(holder: Holder) { + holder.root.setOnClickListener { itemClickAction?.invoke() } + + holder.displayNameText.text = deviceInfo.displayName ?: "" + holder.deviceIdText.text = deviceInfo.deviceId ?: "" + + val lastSeenIp = deviceInfo.lastSeenIp?.takeIf { ip -> ip.isNotBlank() } ?: "-" + + val lastSeenTime = deviceInfo.lastSeenTs?.let { ts -> + val dateFormatTime = SimpleDateFormat("HH:mm:ss", Locale.ROOT) + val date = Date(ts) + + val time = dateFormatTime.format(date) + val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault()) + + dateFormat.format(date) + ", " + time + } ?: "-" + + holder.deviceLastSeenText.text = holder.root.context.getString(R.string.devices_details_last_seen_format, lastSeenIp, lastSeenTime) + + listOf( + holder.displayNameLabelText, + holder.displayNameText, + holder.deviceIdLabelText, + holder.deviceIdText, + holder.deviceLastSeenLabelText, + holder.deviceLastSeenText + ).map { + it.setTypeface(null, if (currentDevice) Typeface.BOLD else Typeface.NORMAL) + } + + holder.buttonDelete.isVisible = !currentDevice + + holder.buttons.isVisible = buttonsVisible + + holder.buttonRename.setOnClickListener { renameClickAction?.invoke() } + holder.buttonDelete.setOnClickListener { deleteClickAction?.invoke() } + } + + class Holder : VectorEpoxyHolder() { + val root by bind(R.id.itemDeviceRoot) + val displayNameLabelText by bind(R.id.itemDeviceDisplayNameLabel) + val displayNameText by bind(R.id.itemDeviceDisplayName) + val deviceIdLabelText by bind(R.id.itemDeviceIdLabel) + val deviceIdText by bind(R.id.itemDeviceId) + val deviceLastSeenLabelText by bind(R.id.itemDeviceLastSeenLabel) + val deviceLastSeenText by bind(R.id.itemDeviceLastSeen) + val buttons by bind(R.id.itemDeviceButtons) + val buttonDelete by bind(R.id.itemDeviceDelete) + val buttonRename by bind(R.id.itemDeviceRename) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt new file mode 100644 index 0000000000..18c0965f86 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt @@ -0,0 +1,129 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.settings.devices + +import com.airbnb.epoxy.EpoxyController +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.extensions.sortByLastSeen +import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.errorWithRetryItem +import im.vector.riotx.core.epoxy.loadingItem +import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.core.ui.list.genericItemHeader +import javax.inject.Inject + +class DevicesController @Inject constructor(private val errorFormatter: ErrorFormatter, + private val stringProvider: StringProvider) : EpoxyController() { + + var callback: Callback? = null + private var viewState: DevicesViewState? = null + + init { + requestModelBuild() + } + + fun update(viewState: DevicesViewState) { + this.viewState = viewState + requestModelBuild() + } + + override fun buildModels() { + val nonNullViewState = viewState ?: return + buildDevicesModels(nonNullViewState) + } + + private fun buildDevicesModels(state: DevicesViewState) { + when (val devices = state.devices) { + is Loading, + is Uninitialized -> + loadingItem { + id("loading") + } + is Fail -> + errorWithRetryItem { + id("error") + text(errorFormatter.toHumanReadable(devices.error)) + listener { callback?.retry() } + } + is Success -> + buildDevicesList(devices(), state.myDeviceId, state.currentExpandedDeviceId) + } + } + + private fun buildDevicesList(devices: List, myDeviceId: String, currentExpandedDeviceId: String?) { + // Current device + genericItemHeader { + id("current") + text(stringProvider.getString(R.string.devices_current_device)) + } + + devices + .filter { + it.deviceId == myDeviceId + } + .forEachIndexed { idx, deviceInfo -> + deviceItem { + id("myDevice$idx") + deviceInfo(deviceInfo) + currentDevice(true) + buttonsVisible(deviceInfo.deviceId == currentExpandedDeviceId) + itemClickAction { callback?.onDeviceClicked(deviceInfo) } + renameClickAction { callback?.onRenameDevice(deviceInfo) } + deleteClickAction { callback?.onDeleteDevice(deviceInfo) } + } + } + + // Other devices + if (devices.size > 1) { + genericItemHeader { + id("others") + text(stringProvider.getString(R.string.devices_other_devices)) + } + + devices + .filter { + it.deviceId != myDeviceId + } + // sort before display: most recent first + .sortByLastSeen() + .forEachIndexed { idx, deviceInfo -> + val isCurrentDevice = deviceInfo.deviceId == myDeviceId + deviceItem { + id("device$idx") + deviceInfo(deviceInfo) + currentDevice(isCurrentDevice) + buttonsVisible(deviceInfo.deviceId == currentExpandedDeviceId) + itemClickAction { callback?.onDeviceClicked(deviceInfo) } + renameClickAction { callback?.onRenameDevice(deviceInfo) } + deleteClickAction { callback?.onDeleteDevice(deviceInfo) } + } + } + } + } + + interface Callback { + fun retry() + fun onDeviceClicked(deviceInfo: DeviceInfo) + fun onRenameDevice(deviceInfo: DeviceInfo) + fun onDeleteDevice(deviceInfo: DeviceInfo) + } +} 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 new file mode 100644 index 0000000000..b2b015a3f0 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt @@ -0,0 +1,268 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.settings.devices + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.airbnb.mvrx.* +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.failure.Failure +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.internal.auth.data.LoginFlowTypes +import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo +import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse +import im.vector.riotx.core.extensions.postLiveEvent +import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.platform.VectorViewModelAction +import im.vector.riotx.core.utils.LiveEvent +import timber.log.Timber + +data class DevicesViewState( + val myDeviceId: String = "", + val devices: Async> = Uninitialized, + val currentExpandedDeviceId: String? = null, + val request: Async = Uninitialized +) : MvRxState + +sealed class DevicesAction : VectorViewModelAction { + object Retry : DevicesAction() + data class Delete(val deviceInfo: DeviceInfo) : DevicesAction() + data class Password(val password: String) : DevicesAction() + data class Rename(val deviceInfo: DeviceInfo, val newName: String) : DevicesAction() + data class ToggleDevice(val deviceInfo: DeviceInfo) : DevicesAction() +} + +class DevicesViewModel @AssistedInject constructor(@Assisted initialState: DevicesViewState, + private val session: Session) + : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: DevicesViewState): DevicesViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: DevicesViewState): DevicesViewModel? { + val fragment: VectorSettingsDevicesFragment = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.devicesViewModelFactory.create(state) + } + } + + // temp storage when we ask for the user password + private var _currentDeviceId: String? = null + private var _currentSession: String? = null + + private val _requestPasswordLiveData = MutableLiveData>() + val requestPasswordLiveData: LiveData> + get() = _requestPasswordLiveData + + init { + refreshDevicesList() + } + + /** + * Force the refresh of the devices list. + * The devices list is the list of the devices where the user is logged in. + * It can be any mobile devices, and any browsers. + */ + private fun refreshDevicesList() { + if (session.isCryptoEnabled() && !session.sessionParams.credentials.deviceId.isNullOrEmpty()) { + setState { + copy( + devices = Loading() + ) + } + + session.getDevicesList(object : MatrixCallback { + override fun onSuccess(data: DevicesListResponse) { + setState { + copy( + myDeviceId = session.sessionParams.credentials.deviceId ?: "", + devices = Success(data.devices.orEmpty()) + ) + } + } + + override fun onFailure(failure: Throwable) { + setState { + copy( + devices = Fail(failure) + ) + } + } + }) + } else { + // Should not happen + } + } + + override fun handle(action: DevicesAction) { + return when (action) { + is DevicesAction.Retry -> refreshDevicesList() + is DevicesAction.Delete -> handleDelete(action) + is DevicesAction.Password -> handlePassword(action) + is DevicesAction.Rename -> handleRename(action) + is DevicesAction.ToggleDevice -> handleToggleDevice(action) + } + } + + private fun handleToggleDevice(action: DevicesAction.ToggleDevice) { + withState { + setState { + copy( + currentExpandedDeviceId = if (it.currentExpandedDeviceId == action.deviceInfo.deviceId) null else action.deviceInfo.deviceId + ) + } + } + } + + private fun handleRename(action: DevicesAction.Rename) { + session.setDeviceName(action.deviceInfo.deviceId!!, action.newName, object : MatrixCallback { + override fun onSuccess(data: Unit) { + setState { + copy( + request = Success(data) + ) + } + // force settings update + refreshDevicesList() + } + + override fun onFailure(failure: Throwable) { + setState { + copy( + request = Fail(failure) + ) + } + + _requestErrorLiveData.postLiveEvent(failure) + } + }) + } + + /** + * Try to delete a device. + */ + private fun handleDelete(action: DevicesAction.Delete) { + val deviceId = action.deviceInfo.deviceId + if (deviceId == null) { + Timber.e("## handleDelete(): sanity check failure") + return + } + + setState { + copy( + request = Loading() + ) + } + + session.deleteDevice(deviceId, object : MatrixCallback { + override fun onFailure(failure: Throwable) { + var isPasswordRequestFound = false + + if (failure is Failure.RegistrationFlowError) { + // We only support LoginFlowTypes.PASSWORD + // Check if we can provide the user password + failure.registrationFlowResponse.flows?.forEach { interactiveAuthenticationFlow -> + isPasswordRequestFound = isPasswordRequestFound || interactiveAuthenticationFlow.stages?.any { it == LoginFlowTypes.PASSWORD } == true + } + + if (isPasswordRequestFound) { + _currentDeviceId = deviceId + _currentSession = failure.registrationFlowResponse.session + + setState { + copy( + request = Success(Unit) + ) + } + + _requestPasswordLiveData.postLiveEvent(Unit) + } + } + + if (!isPasswordRequestFound) { + // LoginFlowTypes.PASSWORD not supported, and this is the only one RiotX supports so far... + setState { + copy( + request = Fail(failure) + ) + } + + _requestErrorLiveData.postLiveEvent(failure) + } + } + + override fun onSuccess(data: Unit) { + setState { + copy( + request = Success(data) + ) + } + // force settings update + refreshDevicesList() + } + }) + } + + private fun handlePassword(action: DevicesAction.Password) { + val currentDeviceId = _currentDeviceId + if (currentDeviceId.isNullOrBlank()) { + // Abort + return + } + + setState { + copy( + request = Loading() + ) + } + + session.deleteDeviceWithUserPassword(currentDeviceId, _currentSession, action.password, object : MatrixCallback { + override fun onSuccess(data: Unit) { + _currentDeviceId = null + _currentSession = null + + setState { + copy( + request = Success(data) + ) + } + // force settings update + refreshDevicesList() + } + + override fun onFailure(failure: Throwable) { + _currentDeviceId = null + _currentSession = null + + // Password is maybe not good + setState { + copy( + request = Fail(failure) + ) + } + + _requestErrorLiveData.postLiveEvent(failure) + } + }) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt new file mode 100644 index 0000000000..465b3ba0fb --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt @@ -0,0 +1,181 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.settings.devices + +import android.content.DialogInterface +import android.os.Bundle +import android.view.KeyEvent +import android.view.View +import android.widget.EditText +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo +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.observeEvent +import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.utils.toast +import kotlinx.android.synthetic.main.fragment_generic_recycler.* +import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* +import javax.inject.Inject + +/** + * Display the list of the user's device + */ +class VectorSettingsDevicesFragment @Inject constructor( + val devicesViewModelFactory: DevicesViewModel.Factory, + private val devicesController: DevicesController +) : VectorBaseFragment(), DevicesController.Callback { + + // used to avoid requesting to enter the password for each deletion + private var mAccountPassword: String = "" + + override fun getLayoutResId() = R.layout.fragment_generic_recycler + + private val devicesViewModel: DevicesViewModel by fragmentViewModel() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + waiting_view_status_text.setText(R.string.please_wait) + waiting_view_status_text.isVisible = true + devicesController.callback = this + recyclerView.configureWith(devicesController, showDivider = true) + devicesViewModel.requestErrorLiveData.observeEvent(this) { + displayErrorDialog(it) + // Password is maybe not good, for safety measure, reset it here + mAccountPassword = "" + } + devicesViewModel.requestPasswordLiveData.observeEvent(this) { + maybeShowDeleteDeviceWithPasswordDialog() + } + } + + override fun onDestroyView() { + devicesController.callback = null + recyclerView.cleanup() + super.onDestroyView() + } + + override fun onResume() { + super.onResume() + + (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_devices_list) + } + + private fun displayErrorDialog(throwable: Throwable) { + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.dialog_title_error) + .setMessage(errorFormatter.toHumanReadable(throwable)) + .setPositiveButton(R.string.ok, null) + .show() + } + + override fun onDeviceClicked(deviceInfo: DeviceInfo) { + devicesViewModel.handle(DevicesAction.ToggleDevice(deviceInfo)) + } + + override fun onDeleteDevice(deviceInfo: DeviceInfo) { + devicesViewModel.handle(DevicesAction.Delete(deviceInfo)) + } + + override fun onRenameDevice(deviceInfo: DeviceInfo) { + displayDeviceRenameDialog(deviceInfo) + } + + override fun retry() { + devicesViewModel.handle(DevicesAction.Retry) + } + + /** + * Display an alert dialog to rename a device + * + * @param deviceInfo device info + */ + private fun displayDeviceRenameDialog(deviceInfo: DeviceInfo) { + val inflater = requireActivity().layoutInflater + val layout = inflater.inflate(R.layout.dialog_base_edit_text, null) + + val input = layout.findViewById(R.id.edit_text) + input.setText(deviceInfo.displayName) + + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.devices_details_device_name) + .setView(layout) + .setPositiveButton(R.string.ok) { _, _ -> + val newName = input.text.toString() + + devicesViewModel.handle(DevicesAction.Rename(deviceInfo, newName)) + } + .setNegativeButton(R.string.cancel, null) + .show() + } + + /** + * Show a dialog to ask for user password, or use a previously entered password. + */ + private fun maybeShowDeleteDeviceWithPasswordDialog() { + if (mAccountPassword.isNotEmpty()) { + devicesViewModel.handle(DevicesAction.Password(mAccountPassword)) + } else { + val inflater = requireActivity().layoutInflater + val layout = inflater.inflate(R.layout.dialog_device_delete, null) + val passwordEditText = layout.findViewById(R.id.delete_password) + + AlertDialog.Builder(requireActivity()) + .setIcon(android.R.drawable.ic_dialog_alert) + .setTitle(R.string.devices_delete_dialog_title) + .setView(layout) + .setPositiveButton(R.string.devices_delete_submit_button_label, DialogInterface.OnClickListener { _, _ -> + if (passwordEditText.toString().isEmpty()) { + requireActivity().toast(R.string.error_empty_field_your_password) + return@OnClickListener + } + mAccountPassword = passwordEditText.text.toString() + devicesViewModel.handle(DevicesAction.Password(mAccountPassword)) + }) + .setNegativeButton(R.string.cancel, null) + .setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event -> + if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { + dialog.cancel() + return@OnKeyListener true + } + false + }) + .show() + } + } + + override fun invalidate() = withState(devicesViewModel) { state -> + devicesController.update(state) + + handleRequestStatus(state.request) + } + + private fun handleRequestStatus(unIgnoreRequest: Async) { + when (unIgnoreRequest) { + is Loading -> waiting_view.isVisible = true + else -> waiting_view.isVisible = false + } + } +} 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 72c98cdc45..bfe08a5c52 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 @@ -22,6 +22,7 @@ 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.session.room.roomSummaryQueryParams import im.vector.matrix.rx.rx import im.vector.riotx.ActiveSessionDataSource import im.vector.riotx.core.platform.EmptyAction @@ -59,10 +60,11 @@ class IncomingShareViewModel @AssistedInject constructor(@Assisted initialState: } private fun observeRoomSummaries() { + val queryParams = roomSummaryQueryParams() sessionObservableStore.observe() .observeOn(AndroidSchedulers.mainThread()) .switchMap { - it.orNull()?.rx()?.liveRoomSummaries() + it.orNull()?.rx()?.liveRoomSummaries(queryParams) ?: Observable.just(emptyList()) } .throttleLast(300, TimeUnit.MILLISECONDS) diff --git a/vector/src/main/res/layout/dialog_device_details.xml b/vector/src/main/res/layout/dialog_device_details.xml deleted file mode 100644 index b3b5c5aff7..0000000000 --- a/vector/src/main/res/layout/dialog_device_details.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/layout/item_autocomplete_emoji.xml b/vector/src/main/res/layout/item_autocomplete_emoji.xml new file mode 100644 index 0000000000..c34ab0d452 --- /dev/null +++ b/vector/src/main/res/layout/item_autocomplete_emoji.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_autocomplete_more_result.xml b/vector/src/main/res/layout/item_autocomplete_more_result.xml new file mode 100644 index 0000000000..d04f515ed0 --- /dev/null +++ b/vector/src/main/res/layout/item_autocomplete_more_result.xml @@ -0,0 +1,9 @@ + + diff --git a/vector/src/main/res/layout/item_bottom_sheet_action.xml b/vector/src/main/res/layout/item_bottom_sheet_action.xml index 0ad7a211da..66a096799d 100644 --- a/vector/src/main/res/layout/item_bottom_sheet_action.xml +++ b/vector/src/main/res/layout/item_bottom_sheet_action.xml @@ -38,7 +38,7 @@ diff --git a/vector/src/main/res/layout/item_device.xml b/vector/src/main/res/layout/item_device.xml new file mode 100644 index 0000000000..bebaf156d9 --- /dev/null +++ b/vector/src/main/res/layout/item_device.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/vector_preference_divider.xml b/vector/src/main/res/layout/vector_preference_divider.xml deleted file mode 100644 index 81f7a091e5..0000000000 --- a/vector/src/main/res/layout/vector_preference_divider.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/values-az/strings.xml b/vector/src/main/res/values-az/strings.xml new file mode 100644 index 0000000000..a6b3daec93 --- /dev/null +++ b/vector/src/main/res/values-az/strings.xml @@ -0,0 +1,2 @@ + + \ 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 e98bcd1a5f..eee8e5d153 100644 --- a/vector/src/main/res/values-bg/strings.xml +++ b/vector/src/main/res/values-bg/strings.xml @@ -1558,7 +1558,7 @@ \n \nRiotX поддържа: • Вход в съществуващ акаунт • Създаване на стая и влизане в публични стаи • Приемане и отхвърляне на покани • Показване на списък със стаите • Преглеждане на информация за стая • Изпращане на текстови съобщения • Изпращане на прикачени файлове • Четене и писане на съобщения в шифровани стаи • Шифроване: резервни копия на E2E ключове, потвърждение на устройства, заявяване и отговаряне на заявки за споделяне на ключове • Уведомления • Светла, Тъмна и Черна тема \n -\nЗасега не всички функции на Riot са налични в RiotX. Основни липсващи (и скоро пристигащи!) функции са: • Създаване на нов профил • Настройки на стаи (показване на членове и т.н.) • Обаждания • Приспособления • …
+\nЗасега не всички функции на Riot са налични в RiotX. Основни липсващи (и скоро пристигащи!) функции са: • Настройки на стаи (показване на членове и т.н.) • Обаждания • Приспособления • …
Директни съобщения @@ -1759,4 +1759,11 @@ В момента няма връзка с мрежата + Това не е валиден адрес на Matrix сървър + Потвърдете паролата + Не може да направите това от мобилно приложение на Riot + Нужна е автентикация + + + Интеграции diff --git a/vector/src/main/res/values-de/strings.xml b/vector/src/main/res/values-de/strings.xml index 55fa5d1488..a3dee0eb0b 100644 --- a/vector/src/main/res/values-de/strings.xml +++ b/vector/src/main/res/values-de/strings.xml @@ -998,8 +998,8 @@ Beachte: Diese Aktion wird die App neu starten und einige Zeit brauchen.
%dh
- 1d - %dd + 1T + %dT Jetzt %1$s @@ -1654,7 +1654,7 @@ Wenn du diese neue Wiederherstellungsmethode nicht eingerichtet hast, kann ein A Ablehnen - app_id: + App-ID: Überprüfung Keine Widerrufen @@ -1687,8 +1687,8 @@ Wenn du diese neue Wiederherstellungsmethode nicht eingerichtet hast, kann ein A Sie versuchen anscheinend, eine Verbindung zu einem anderen Homeserver herzustellen. Möchten Sie sich abmelden\? - push_key: - app_display_name: + Push-Key: + App-Anzeigename: Url: Nutzungsbedingungen Nutzungsbedingungen überprüfen @@ -1710,4 +1710,84 @@ Wenn du diese neue Wiederherstellungsmethode nicht eingerichtet hast, kann ein A Konnte keine Verbindung zum Heimserver herstellen Latn + Bitte frage den Administrator deines Home-Servers (%1$s) um einen TURN server einzurichten, damit Anrufe zuverlässig funktionieren. +\n +\nAlternativ kannst du einen öffentlichen Server auf %2$s nutzen doch wird das nicht zu zuverlässig sein und es wird deine IP-Adresse mit dem Server teilen. Du kannst dies auch in den Einstellungen konfigurieren. + Dies ist keine Adresse eines Matrixservers + Kann Home-Server nicht bei dieser URL erreichen. Bitte überprüfen + Wir nutzen %s als Assistenten wenn dein Home-Server keinen anbietet (Deine IP-Adresse wird während des Anrufs geteilt) + Führe einen Identitätsserver in deinen Einstellungen hinzu um diese Aktion auszuführen. + Passwort bestätigen + Du kannst dies nicht auf einem mobilen Riot tun + Authentifizierung benötigt + + + Hintergrundsynchronisierungsmodus (experimentell) + Riot wird sich im Hintergrund auf eine Art synchronisieren die die Ressourcen des Geräts schont (Akku). +\nAbhängig von dem Ressourcen-Statuses deines Geräts kann dein System die Synchronisierung verschieben. + Riot wird sich im Hintergrund periodisch zu einem bestimmten Zeitpunkt synchronisieren (konfigurierbar). +\nDies wird Funk- und Akkunutzung beeinflussen. Es wird wird eine permanente Benachrichtigung geben, die sagt, dass Riot auf Ereignisse lauscht. + %s +\nDie Synchronisierung kann aufgrund deiner Ressourcen (Akku) oder Gerätezustands (schlafend) verschoben werden. + Integrationen + Benutze einen Integrations-Manager um Bots, Brpcken, Widgets und Sticker-Pakete zu verwalten. +\nIntegrations-Manager erhalten Konfigurationsdaten und können Widgets verändern, Raum-Einladungen senden und in deinem Namen Berechtigungslevel setzen. + Erlaube Integrationen + Widget + Widget laden + Dieses Widget wurde hinzugefügt von: + Konnte Widget nicht laden. +\n%s + Widget erneut laden + Im Browser öffnen + Dein Anzeigename + Deine Profilbild-Adresse + Deine Benutzer-ID + Dein Design + Widget-ID + Raum-ID + + + Dieses Widget möchte folgende Ressourcen benutzen: + Erlauben + Alle blockieren + Kamera benutzen + Mikrofon benutzen + Lese DRM-geschützte Medien + + Frühere Versionen von Riot hatten einen Sicherheitsproblem, welches dem Identitätsserver (%1$s) Zugriff auf deinen Account geben konnte. Wenn du %2$s vertraust, kannst du dies ignorieren – ansonsten logge dich bitte aus und wieder ein. +\n +\nWeitere Details gibt es hier (Englisch): +\nhttps://medium.com/@RiotChat/36b4792ea0d6 + + Du wirst nicht über eingehende Nachrichten benachrichtigt, wenn die App im Hintergrund ist. + Verwalte deine Erkennungseinstellungen. + Zugriff für mich zurückziehen + + Gerätename: + Format: + + RiotX ist ein neuer Client für das Matrix-Protokoll (matrix.org): Ein offenes Netzwerk für sichere, dezentrale Kommunikation. +\nRiotX ist der Riot-Android-Client der auf dem matrix-android-sdk basiert - aber beides komplett neu geschrieben. +\n +\nHinweis: Dies ist eine Beta-Version. RiotX wird aktuell aktiv entwickelt und enthält Einschränkungen und (wir hoffen nicht zu viele) Fehler. Jede Rückmeldung ist willkommen! +\n +\nRiotX unterstützt: • Anmelden an ein existierendes Konto • Erstelle Räume und trete öffentlichen Räumen bei • Akzeptiere und lehne Einladungen ab • Zeige Raum-Details • Sende Textnachrichten • Sende Anhänge • Lese und Schreibe Nachrichten in verschlüsselten Räumen • Verschlüsselung: Backup der Ende-zu-Ende-Schlüssel, erweiterte Geräteverifizierung, Schlüsseltauschanfragen und -antworten • Push-Benachrichtigungen • Helles, dunkles und schwarzes Thema +\n +\nNicht alle Features in Riot sind bisher in RiotX implementiert. Hauptfunktionen, die noch fehlen (und bald kommen!): • Raum-Einstellungen (Raum-Mitglieder auflisten, etc.) • Anrufe • Widgets • … + + Du nutzt aktuell %1$s um zu entdecken und von dir bekannten Kontakten entdeckt zu werden. + Du benutzt aktuell keinen Identitätsserver. Um zu entdecken und um von dir bekannten Kontakten entdeckt zu werden, richte unten einen ein. + Entdeckbare Telefonnummern + Bitte gebe Adresse des Identitätsserver ein + Identitätsserver hat keine Nutzungsbedingungen + Der Identitätsserver den du ausgewählt hast, hat keine Nutzungsbedingungen. Fahre nur fort, wenn du dem Besitzer des Dienstes vertraust + Eine Textnachricht wurde an %s gesendet. Bitte gebe den Verifizierungscode ein, den sie enthält. + + Aktiviere gesprächige Logs. + Gesprächige Logs wird den Entwicklern helfen indem sie mehr Informationen enthalten, wenn du einen Fehlerbericht sendest. Auch wenn dies aktiviert ist, werden keine Nachrichteninhalte oder andere privaten Daten aufgezeichnet. + + + Bitte erneut versuchen, nachdem du die Nutzungsbedingungendeines Home-Servers akzeptiert hast. + diff --git a/vector/src/main/res/values-eu/strings.xml b/vector/src/main/res/values-eu/strings.xml index 046e5557f5..8915d0e471 100644 --- a/vector/src/main/res/values-eu/strings.xml +++ b/vector/src/main/res/values-eu/strings.xml @@ -1564,7 +1564,7 @@ Abisua: Fitxategi hau ezabatu daiteke aplikazioa desinstalatzen bada.
\n \nRiotX bezeroak honakoa ahalbidetzen du: • Badagoen kontu batean saioa hasi • Gelak sortu eta gela publikoetara elkartu • Gonbidapenak onartu edo ukatu • Erabiltzailearen gelak zerrendatu • Gelaren xehetasunak ikusi • Testuzko mezuak bidali • Eranskinak bidali • Zifratutako geletan mezuak irakurri eta idatzi • Zifratzea: E2Egakoen babeskopia, gailuaren egiaztaketa aurreratua, gakoa partekatzeko eskaria eta erantzuna • Push jakinarazpena • Gai argia, iluna eta beltza \n -\nEz dira oraindik Riot bezeroaren ezaugarri guztiak ezarri RiotX bezeroan. Falta diren (eta laster etorriko direnen) artean nabarmenak dira: • Kontua sortzea • Gelaren ezarpenak (gelako kideak zerrendatzea, eta abar.) • Deiak • Trepetak • …
+\nEz dira oraindik Riot bezeroaren ezaugarri guztiak ezarri RiotX bezeroan. Falta diren (eta laster etorriko direnen) artean nabarmenak dira: • Gelaren ezarpenak (gelako kideak zerrendatzea, eta abar.) • Deiak • Trepetak • … app_display_name: Mezu zuzenak @@ -1741,7 +1741,7 @@ Abisua: Fitxategi hau ezabatu daiteke aplikazioa desinstalatzen bada. Spama da Desegokia da - Salaketa pertsonalizatua + Salaketa pertsonalizatua… Salatu eduki hau Eduki hau salatzeko arrazoia SALATU @@ -1801,4 +1801,180 @@ Abisua: Fitxategi hau ezabatu daiteke aplikazioa desinstalatzen bada. Mikrofonoa erabili DRM bidez babestutako multimedia irakurri + Hau ez da baliozko Matrix zerbitzari helbide bat + Blokeatu erabiltzailea + + Mezu guztiak (ozen) + Mezu guztiak + Aipamenak besterik ez + Mututu + Ezarpenak + Atera gelatik + %1$s erabiltzaileak ez du aldaketarik egin + Emandako mezua izorraki gisa bidaltzen du + Izorrakia + Idatzi hitz gakoak erreakzio bat aurkitzeko. + + Ez duzu erabiltzailerik ezikusten + + Sakatu luze gela batean aukera gehiago ikusteko + + + %1$s erabiltzaileak gela publikoa bihurtu du esteka dakien edonorentzat. + %1$s erabiltzaileak gela soilik gonbidatuentzat bihurtu du. + Irakurri gabeko mezuak + + Askatu zure komunikazioa + Txateatu beste taldeetako jendearekin + Mantendu elkarrizketak pribatu zifratzearekin + Hedatu eta pertsonalizatu zure esperientzia + Hasi + + Hautatu zerbitzari bat + E-mailarekin bezala, kontuek etxe bat dute, baina edonorekin hitz egin dezakezu + Elkartu milioiekin aske zerbitzari publiko handienean + Ordainpeko ostatatzea elkarteentzat + Ikasi gehiago + Beste bat + Ezarpen pertsonalizatu eta aurreratuak + + Jarraitu + Konektatu %1$s zerbitzarira + Konektatu Modular-era + Konektatu zerbitzari pertsonalizatu batera + Hasi saioa %1$s zerbitzarian + Erregistratu + Hasi saioa + Jarraitu SSO-rekin + + Modular helbidea + Helbidea + Ordainpeko ostatatzea elkarteentzat + Sartu erabili nahi duzun Modular Riot edo zerbitzariaren helbidea + Sartu konektatu nahi duzun zerbitzari edo Riot-aren helbidea + + Errore bat gertatu da orria kargatzean: %1$s (%2$d) + Aplikazioak ezin du hasiera-zerbitzari honetan saioa hasi. Hasiera-zerbitzariak honako saio mota onartzen du: %1$s. +\n +\nWeb bezero batekin hasi nahi duzu saioa\? + Sentitzen dugu, zerbitzari honek ez ditu kontu berriak onartzen. + Aplikazioak ezin du kontu berri bat sortu hasiera-zerbitzari honetan. +\n +\nWeb bezero bat erabiliz erregistratu nahi duzu\? + + E-mail hau ez dago kontu batera lotuta. + + Berrezarri %1$s zerbitzariko pasahitza + Egiaztaketa e-mail bat bidaliko zaizu zure pasahitz berriaren ezarpena baieztatzeko. + Hurrengoa + E-mail + Pasahitz berria + + Abisua! + Zure pasahitza aldatzeak zure gailu guztietako muturretik-muturrerako zifratzerako gakoak berrezarriko ditu, eta aurretik zifratutako mezuen historiala ezin izango da irakurri. Ezarri gakoen babes-kopia edo esportatu zure geletako gakoak beste gailu batetik pasahitza aldatu aurretik. + Jarraitu + + E-mail hau ez dago kontu batera lotuta + + Egiaztatu zure sarrera ontzia + Egiaztaketa e-mail bat bidali da %1$s helbidera. + Sakatu estekan zure pasahitz berria baieztatzeko. Behin dakarren esteka jarraitu duzula, sakatu hemen azpian. + Nire e-mail helbidea baieztatu dut + + Ongi! + Zure pasahitza berrezarri da. + Gailu guztietan saioa amaitu duzu eta ez duzu push jakinarazpenik jasoko. Jakinarazpenak berriro aktibatzeko, hasi saioa gailuetan. + Itzuli saio hasierara + + Abisua + Zure pasahitza ez da oraindik aldatu. +\n +\nUtzi pasahitza aldatzeko prozesua\? + + Ezarri e-mail helbidea + Ezarri e-mail bat zure kontua berreskuratzeko. Geroago, nahiez gero jendeak zure e-mail helbidearen bidez zu aurkitzea ahalbidetu dezakezu. + E-mail + E-mail (aukerakoa) + Hurrengoa + + Ezarri telefono zenbakia + Ezarri aukeran telefono zenbakia, honen bidez jendeak zu aurkitzea ahalbidetzeko. + Erabili formatu internazionala. + Telefono zenbakia + Telefono zenbakia (aukerakoa) + Hurrengoa + + Baieztatu telefono zenbakia + Kode bat bidali dugu %1$s zenbakira. Sartu hemen azpian zu zarela baieztatzeko. + Sartu kodea + Bidali berriro + Hurrengoa + + Telefono zenbaki internazionalak \'+\' batekin hasten dira + Telefono zenbakia baliogabea dirudi. Egiaztatu ezazu + + Erregistratu %1$s zerbitzarian + Erabiltzaile-izena edo e-maila + Pasahitza + Hurrengoa + Erabiltzaile-izen hori hartuta dago + Abisua + Zure kontua ez da oraindik sortu. +\n +\nUtzi erregistratze prozesua\? + + Hautatu matrix.org + Hautatu modular + Hautatu hasiera-zerbitzari pertsonalizatu bat + Bete captcha erronka + Onartu baldintzak jarraitzeko + + Egiaztatu zure e-maila + E-mail bat bidali dizugu %1$s helbidera. +\nSakatu dakarren esteka kontuaren sorrerarekin jarraitzeko. + Sartutako kodea ez da zuzena. Egiaztatu ezazu. + Zaharkitutako hasiera-zerbitzaria + Hasiera-zerbitzari honek konektatzeko zaharregia den bertsio bat darabil. Eskatu administratzaileari eguneratu dezala. + + + Eskaera gehiegi bidali dira. Segundo 1$d barru saiatu zaitezke berriro… + Eskaera gehiegi bidali dira. 1$d segundo barru saiatu zaitezke berriro… + + + Hauek ikusia + + Saioa amaitu duzu + Hainbat arrazoiengatik izan daiteke: +\n +\n• Pasahitza aldatu duzu beste gailu batean. +\n +\n• Gailu hau ezabatu duzu beste gailu batetik. +\n +\n• Zure zerbitzariko administratzaileak zure sarbidea baliogabetu du segurtasun arrazoiengatik. + Hasi saioa berriro + + Saioa amaitu duzu + Hasi saioa + Zure hasiera zerbitzariaren administratzaileak (%1$s) zure %2$s kontuaren saioa amaitu du (%3$s). + Hasi saioa gailu honetan besterik gorde ez diren zifratze gakoak berreskuratzeko. Zure mezu seguruak beste gailuetan irakurri ahal izateko behar dituzu. + Hasi saioa + Pasahitza + Garbitu datu pertsonalak + Abisua: Zure datu pertsonalak (zure zifratze gakoak barne) gailu honetan daude oraindik. +\n +\nGarbitu ezazu gailu hau erabiltzen bukatu duzunean, edo beste kontu batekin saioa hasi nahi duzunean. + Garbitu datu guztiak + + Garbitu datuak + Gailu honetan gordetako datu guztiak ezabatu\? +\n +\nHasi saioa berriro zure kontuaren datuak eta mezuak atzitzeko. + Zure mezu zifratuetara sarbidea galduko duzu ez baduzu saioa hasten zifratze gakoak berreskuratzeko. + Garbitu datuak + Oraingo saioa %1$s erabiltzailearena da eta %2$s erabiltzailearen kredentzialak eman dituzu. RiotX-k ez du hau onartzen. +\nAurretik garbitu datuak, gero hasi saioa berriro beste kontu batekin. + + Zure matrix.to esteka gaizki osatua dago + Deskripzio hau laburregia da + diff --git a/vector/src/main/res/values-fi/strings.xml b/vector/src/main/res/values-fi/strings.xml index ea8a6abc5a..309e1a0756 100644 --- a/vector/src/main/res/values-fi/strings.xml +++ b/vector/src/main/res/values-fi/strings.xml @@ -1801,7 +1801,7 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös \n \nRiotX tukee: • Kirjaudu olemassaolevalle tunnukselle • Luo huoneita ja liity julkisiin huoneisiin • Hyväksy ja hylkää kutsuja • Listaa käyttäjän huoneet • Katso huoneen tietoja • Lähetä tekstiviestejä • Lähetä liitteitä • Lue ja kirjoita viestejä salatuissa huoneissa • Salaus: osapuolten välisen salauksen avaimien varmuuskopiointi, edistynyt laitteiden varmennus, avainten jakopyynnöt ja vastaus • Viesti-ilmoitukset • Vaalea, tumma ja musta teema \n -\nKaikkia Riotin ominaisuuksia ei ole vielä toteutettu RiotX:ssä. Tärkeimmät puuttuvat (ja pian saapuvat!) ominaisuudet: • Tunnusten luonti • Huoneen asetukset (listaa huoneen jäsenet jne.) • Puhelut • Sovelmat • … +\nKaikkia Riotin ominaisuuksia ei ole vielä toteutettu RiotX:ssä. Tärkeimmät puuttuvat (ja pian saapuvat!) ominaisuudet: • Huoneen asetukset (listaa huoneen jäsenet jne.) • Puhelut • Sovelmat • … Salataan pikkukuvaa… Lähetetään pikkukuvaa (%1$s / %2$s) @@ -1828,7 +1828,7 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös Ääni Jakotiedon käsittely epäonnistui - Muokattu ilmianto + Muokattu ilmianto… Ilmianna tämä sisältö Sisällön ilmiannon syy ILMIANNA @@ -1851,4 +1851,179 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös \n \nSalli pääsy tiedostoihin seuraavassa ponnahdusikkunassa, jotta voit viedä avaimesi käsin. + Tämä ei ole kelvollinen Matrix-palvelimen osoite + Estä käyttäjä + + Kaikki viestit (äänekäs) + Kaikki viestit + Vain maininnat + Vaimenna + Asetukset + Poistu huoneesta + %1$s ei tehnyt muutoksia + Lähettää annetun viestin ilonpilaajana + Ilonpilaaja + Syötä avainsanat löytääksesi reaktion. + + Et jätä yhtään käyttäjää huomiotta + + Tee pitkä klikkaus huoneelle nähdäksesi lisää asetuksia + + + %1$s asetti huoneen julkiseksi kelle tahansa, joka tietää huoneen osoitteen. + %1$s muutti huoneeseen liittymisen vaatimaan kutsua. + Lukemattomia viestejä + + Vapauta keskustelusi + Keskustele kaksistaan tai ryhmissä + Pidä keskustelut yksityisinä ja salattuina + Laajenna ja muokkaa kokemustasi + Aloita + + Valitse palvelin + Kuten sähköpostissa, tunnuksilla on yksi koti, mutta voit keskustella koko maailman kanssa + Liity miljoonien joukkoon suurimmalla julkisella palvelimella + Korkealuokkaista isännöintiä organisaatioille + Lue lisää + Muut + Mukautetut ja monimutkaiset asetukset + + Jatka + Yhdistä palvelimeen %1$s + Yhdistä Modulariin + Yhdistä itse määritettyyn palvelimeen + Kirjaudu sisään palvelimeen %1$s + Rekisteröidy + Kirjaudu sisään + Jatka kertakirjautumiseen + + Modularin osoite + Osoite + Korkealuokkaista isännöintiä organisaatioille + Syötä Modular Riotin tai haluamasi palvelimen osoite + Sivun lataamisessa tapahtui virhe: %1$s (%2$d) + Sovellus ei pysty kirjautumaan sisään tälle kotipalvelimelle. Tämä kotipalvelin tukee seuraavia kirjautumistyyppejä: %1$s. +\n +\nHaluatko kirjautua sisään web-klientillä\? + Valitettavasti tämä palvelin ei hyväksy uusia tunnuksia. + Sovellus ei pysty luomaan uusia tunnuksia tälle kotipalvelimelle. +\n +\nHaluatko rekisteröityä web-klientillä\? + + Tämä sähköpostiosoite ei ole liitettynä mihinkään tunnukseen. + + Palauta salasana palvelimella %1$s + Sähköpostiisi lähetetään viesti uuden salananan asettamiseksi. + Seuraava + Sähköposti + Uusi salasana + + Varoitus! + Salasanan vaihtaminen nollaa kaikki osapuolten välisen salauksen avaimet kaikilla laitteillasi, joka estää sinua lukemasta vanhoja viestejä. Ota käyttöön avainten varmuuskopiointi tai vie huoneen avaimet toiselta laitteelta ennen kuin vaihdat salasanasi. + Jatka + + Tämä sähköposti ei ole liitettynä mihinkään tunnukseen + + Tarkista sähköpostisi + Vahvistusviesti lähetettiin osoitteeseen %1$s. + Näpäytä linkkiä vahvistaaksesi uuden salasanasi. Seurattuasi siinä olevaa linkkiä, klikkaa alapuolelta. + Olen vahvistanut sähköpostiosoitteeni + + Valmis! + Salasanasi on vaihdettu. + Olet kirjautunut ulos kaikilta laitteilta, etkä saa enää viesti-ilmoituksia. Ottaaksesi viesti-ilmoitukset uudelleen käyttöön, kirjaudu sisään jokaisella laitteellasi. + Takaisin sisäänkirjautumiseen + + Varoitus + Salasanaasi ei ole vielä vaihdettu. +\n +\nPeru salasananvaihtoprosessi\? + + Aseta sähköpostiosoite + Aseta sähköpostiosoite palauttaaksesi tunnuksesi. Myöhemmin, voit antaa muiden löytää sinut sähköpostillasi. + Sähköposti + Sähköposti (vapaaehtoinen) + Seuraava + + Aseta puhelinnumero + Aseta puhelinnumero antaaksesi muiden löytää sinut puhellinumerosi perusteella. + Käytä maailmanlaajuista puhelinnumeron muotoa. + Puhelinnumero + Puhelinnumero (vapaaehtoinen) + Seuraava + + Vahvista puhelinnumero + Lähetimme sinulle koodin numeroon %1$s. Syötä se alapuolelle vahvistaaksesi numeron. + Syötä koodi + Lähetä uudelleen + Seuraava + + Kansainvälisten puhelinnumeroiden pitää alkaa merkillä ”+” + Puhelinnumero vaikuttaa epäkelvolta. Tarkista numero + + Rekisteröidy palvelimelle %1$s + Käyttäjätunnus tai sähköpostiosoite + Salasana + Seuraava + Käyttäjätunnus on varattu + Varoitus + Tunnustasi ei ole vielä luotu. +\n +\nPeru rekisteröintiprosessi\? + + Valitse matrix.org + Valitse modular + Valitse muu kotipalvelin + Ratkaise seuraava kuvavarmennushaaste + Hyväksy ehdot jatkaaksesi + + Tarkista sähköpostisi + Lähetimme sähköpostin osoitteeseen %1$s. +\nKlikkaa siinä olevaa linkkiä jatkaaksesi tunnuksen luontia. + Syöttämäsi koodi ei ole kelvollinen. Tarkista se. + Vanhentunut kotipalvelin + Tämä kotipalvelin pyörii liian vanhalla versiolla, jotta pystyisimme yhdistämään siihen. Pyydä kotipalvelimesi ylläpitäjää päivittämään palvelimensa. + + + Liian monta pyyntöä lähetettiin. Voit yrittää uudelleen 1 sekunnissa… + Liian monta pyyntöä lähetettiin. Voit yrittää uudelleen %1$d sekunnissa… + + + Nähty toimesta + + Olet kirjautunut ulos + Kirjaudu sisään uudelleen + + Olet kirjautunut ulos + Kirjaudu sisään + Kotipalvelimen (%1$s) ylläpitäjä on kirjannut sinut ulos tunnukseltasi %2$s (%3$s). + Kirjaudu sisään + Salasana + Poista henkilökohtaiset tiedot + Varoitus: henkilökohtaiset tietosi (sisältää salausavaimesi) ovat vielä tallennettuna tälle laitteelle. +\n +\nPoista ne jos olet lopettanut tämän laitteen käytön, tai haluat kirjautua sisään toiselle tunnukselle. + Poista kaikki tiedot + + Poista tiedot + Poista kaikki tälle laitteelle tallennetut tiedot\? +\nKirjaudu sisään päästäksesi käsiksi tunnuksesi tietoihin ja viesteihin. + Menetät pääsyn salattuihin viesteihisi ellet kirjaudu sisään palauttaaksesi salausavaimesi. + Poista tiedot + Nykyinen istunto on käyttäjälle %1$s, ja yritit kirjautuas isään käyttäjälle %2$s. RiotX ei tue tätä. +\nPoista ensin tietosi ja kirjaudu sen jälkeen toisella tunnuksella. Voit vaihtoehtoisesti käyttää Riotin selainversiota. + + matrix.to-linkkisi oli epämuodostunut + Kuvaus on liian lyhyt + + Syötä palvelin tai sen Riotin osoite, mihin haluat yhdistää + + Se voi johtua monesta eri syystä: +\n +\n• olet vaihtanut salasanasi toisella laitteella +\n +\n• olet poistanut tämän laitteen toisella laitteella +\n +\n• palvelimen ylläpitäjä on estänyt pääsysi turvallisuussyistä. + Kirjaudu sisään palauttaaksesi salausavaimesi, jotka ovat tallessa vain tällä laitteella. Tarvitset niitä lukeaksi kaikki salatut viestisi millä tahansa laitteella. diff --git a/vector/src/main/res/values-fr/strings.xml b/vector/src/main/res/values-fr/strings.xml index cc81f595d2..124f4eabc5 100644 --- a/vector/src/main/res/values-fr/strings.xml +++ b/vector/src/main/res/values-fr/strings.xml @@ -1570,7 +1570,7 @@ Si vous n’avez pas configuré de nouvelle méthode de récupération, un attaq \n \nRiotX prend en charge : • Se connecter à un compte existant • Créer de salons et rejoindre des salons publics • Accepter et refuser des invitations • Lister les salons des utilisateurs • Voir les informations des salons • Envoyer des messages texte • Envoyer des pièces jointes • Lire et écrire des messages dans les salons chiffrés • Chiffrement : sauvegarde des clés de chiffrement, vérification avancée des appareils, demande et réponse de partage de clé • Notifications • Thèmes clair, sombre et noir \n -\nToutes les fonctionnalités de Riot ne sont pas encore implémentées dans RiotX. Principales fonctionnalités manquantes (et qui arrivent bientôt !) : • Création de compte • Réglages des salons (lister les membres du salon etc.) • Appels • Widgets • … +\nToutes les fonctionnalités de Riot ne sont pas encore implémentées dans RiotX. Principales fonctionnalités manquantes (et qui arrivent bientôt !) : • Réglages des salons (lister les membres du salon etc.) • Appels • Widgets • … Messages directs @@ -1745,7 +1745,7 @@ Si vous n’avez pas configuré de nouvelle méthode de récupération, un attaq C’est du pourriel C’est inapproprié - Signalement personnalisé + Signalement personnalisé… Signaler ce contenu Motif de signalement de ce contenu SIGNALER @@ -1805,4 +1805,179 @@ Si vous n’avez pas configuré de nouvelle méthode de récupération, un attaq Utiliser le micro Lire des médias protégés par des DRM + Ce n’est pas une adresse de serveur Matrix valide + Bloquer l’utilisateur + + Tous les messages (sonore) + Tous les messages + Seulement les mentions + Sourdine + Paramètres + Quitter le salon + %1$s n’a effectué aucun changement + Envoie le message fourni comme un spoiler + Spoiler + Saisir des mots-clés pour trouver une réaction. + + Vous n’ignorez aucun utilisateur + + Clic long sur un salon pour voir plus d’options + + + %1$s a rendu le salon public à tous ceux qui en connaissent le lien. + %1$s a rendu le salon accessible uniquement par invitation. + Messages non lus + + Libérez votre communication + Discutez directement avec des personnes ou avec des groupes + Gardez vos conversations privées avec le chiffrement + Étendez et personnalisez votre expérience + Démarrer + + Sélectionner un serveur + Comme les e-mails, les comptes ont une maison, même si vous pouvez parler à n’importe qui + Rejoignez des millions de personnes gratuitement sur le plus grand serveur public + Hébergement premium pour les organisations + En savoir plus + Autre + Paramètres personnalisés et avancés + + Continuer + Se connecter à %1$s + Se connecter à Modular + Se connecter à un serveur personnalisé + S’authentifier sur %1$s + S’inscrire + S’authentifier + Continuer avec l’authentification unique + + Adresse Modular + Adresse + Hébergement privé pour les organisations + Saisir l’adresse de Riot ou du serveur de Modular que vous voulez utiliser + Saisir l’adresse d’un serveur ou d’un Riot auquel vous voulez vous connecter + + Une erreur est survenue pendant le chargement de la page : %1$s (%2$d) + L’application ne peut pas s’authentifier sur ce serveur d’accueil. Le serveur d’accueil prend en charge le(s) type(s) d’authentification suivant(s) : %1$s. +\n +\nVoulez-vous vous connecter en utilisant un client web \? + Désolé, ce serveur n’accepte pas de nouveau compte. + L’application ne peut pas créer de compte sur ce serveur d’accueil. +\n +\nVoulez-vous vous inscrire en utilisant un client web \? + + Cet e-mail n’est associé à aucun compte. + + Réinitialiser le mot de passe sur %1$s + Un e-mail de vérification sera envoyé à votre adresse pour confirmer la configuration de votre nouveau mot de passe. + Suivant + E-mail + Nouveau mot de passe + + Attention ! + Le changement de mot de passe réinitialisera toutes les clés de chiffrement sur tous vos appareils, rendant l’historique des discussions chiffrées illisibles. Configurez la sauvegarde de clés ou exportez vos clés de salon depuis un autre appareil avant de réinitialiser votre mot de passe. + Continuer + + Cet e-mail n’est lié à aucun compte + + Vérifiez votre boîte de réception + Un e-mail de vérification a été envoyé à %1$s. + Touchez le lien pour confirmer votre nouveau mot de passe. Après avoir suivi le lien qu’il contient, cliquez ci-dessous. + J’ai vérifié mon adresse e-mail + + Réussi ! + Votre mot de passe a été réinitialisé. + Vous avez été déconnecté de tous les appareils et ne recevrez plus de notification. Pour réactiver les notifications, reconnectez-vous sur chaque appareil. + Retourner à l’authentification + + Attention + Votre mot de passe n’a pas encore été changé. +\n +\nArrêter le processus de changement \? + + Définir l’adresse e-mail + Définir une adresse e-mail pour récupérer votre compte. Plus tard, vous pourrez éventuellement autoriser des personnes à vous retrouver avec votre adresse e-mail. + E-mail + E-mail (facultatif) + Suivant + + Définir le numéro de téléphone + Définir un numéro de téléphone pour autoriser éventuellement des personnes à vous découvrir. + Veuillez utiliser le format international. + Numéro de téléphone + Numéro de téléphone (facultatif) + Suivant + + Confirmer le numéro de téléphone + Nous avons envoyé un code à %1$s. Saisissez-le ci-dessous pour vérifier que c’est bien vous. + Saisir le code + Renvoyer + Suivant + + Les numéros de téléphone internationaux doivent commencer par « + » + 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 + Mot de passe + Suivant + Ce nom d’utilisateur est déjà pris + Attention + Votre compte n’est pas encore crée. +\n +\nArrêter le processus de création \? + + Sélectionner matrix.org + Sélectionner Modular + Sélectionner un serveur d’accueil personnalisé + Veuillez compléter le captcha + Acceptez les termes pour continuer + + Vérifiez vos e-mails + Nous avons envoyé un e-mail à %1$s. +\nCliquez sur le lien qu’il contient pour continuer la création du compte. + Le code saisi n’est pas correct. Veuillez vérifier. + Serveur d’accueil trop vieux + Ce serveur d’accueil utilise une version trop ancienne pour s’y connecter. Demandez à l’administrateur de votre serveur d’accueil de le mettre à niveau. + + + Trop de requêtes ont été envoyées. Vous pouvez réessayer dans %1$d seconde… + Trop de requêtes ont été envoyées. Vous pouvez réessayer dans %1$d secondes… + + + Vu par + + Vous êtes déconnecté + Cela peut être dû à plusieurs raisons : +\n +\n• Vous avez changé votre mot de passe sur un autre appareil. +\n +\n• Vous avez supprimé cet appareil depuis un autre appareil. +\n +\n• L’administrateur de votre serveur a invalidé votre accès pour des raisons de sécurité. + Se reconnecter + + Vous êtes déconnecté + Se connecter + L’administrateur de votre serveur d’accueil (%1$s) vous a déconnecté de votre compte %2$s (%3$s). + Connectez-vous pour récupérer les clés de chiffrement stockées uniquement sur cet appareil. Vous en avez besoin pour lire tous vos messages sécurisés sur n’importe quel appareil. + Se connecter + Mot de passe + Effacer les données personnelles + Attention : Vos données personnelles (y compris les clés de chiffrement) sont toujours stockées sur cet appareil. +\n +\nEffacez-les si vous n’utilisez plus cet appareil ou si vous voulez vous connecter à un autre compte. + Effacer toutes les données + + Effacer les données + Effacer toutes les données stockées sur cet appareil \? +\nReconnectez-vous pour accéder aux données et aux messages de votre compte. + Vous perdrez l’accès à vos messages sécurisés sauf si vous vous connectez pour récupérer vos clés de chiffrement. + Effacer les données + La session en cours est celle de l’utilisateur %1$s et vous fournissez des identifiants pour l’utilisateur %2$s. Ce n’est pas pris en charge par RiotX. +\nEffacez d’abord les données, puis reconnectez-vous avec un autre compte. + + Votre lien matrix.to était malformé + La description est trop courte + diff --git a/vector/src/main/res/values-hu/strings.xml b/vector/src/main/res/values-hu/strings.xml index 1f518fd590..1f25dc1d61 100644 --- a/vector/src/main/res/values-hu/strings.xml +++ b/vector/src/main/res/values-hu/strings.xml @@ -1569,7 +1569,7 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró \n \nRiotX ezeket támogatja: • Bejelentkezés létező fiókba • Szoba készítés és nyilvános szobába való belépés • Meghívók fogadása és elutasítás • Felhasználók szobáinak listázása • Szoba adatainak megtekintése • Szöveges üzenet küldése • Csatolmány küldése • Titkosított szobákban üzenetek olvasása és írása • Titkosítás: Végponttól végpontig titkosító kulcsok mentése, fejlett eszköz ellenőrzés, kulcs megosztás kérése és válasz • „Push” értesítések • Világos, sötét és fekete téma \n -\nNem minden Riot funkció támogatott a RiotX-ben jelenleg. A fő hiányzó (és hamarosan elérhető!) funkciók: • Felhasználói fiók létrehozása • Szoba beállítások (szoba tagság mutatása, stb…) • Hívások • Kisalkalmazások • … +\nNem minden Riot funkció támogatott a RiotX-ben jelenleg. A fő hiányzó (és hamarosan elérhető!) funkciók: • Szoba beállítások (szoba tagság mutatása, stb…) • Hívások • Kisalkalmazások • … Közvetlen beszélgetés @@ -1744,7 +1744,7 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró Ez nemkívánt (spam) Ez nem idevaló - Egyedi jelentés + Egyedi jelentés… Tartalom bejelentése A tartalom bejelentésének oka JELENTÉS @@ -1805,4 +1805,179 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró Mikrofon használata DRM-mel védett média olvasása + Ez nem egy érvényes Matrix szerver cím + Felhasználó tiltása + + Minden üzenet (hangos) + Minden üzenet + Csak ha megemlítenek + Elnémít + Beállítások + Szoba elhagyása + %1$snem változtatott semmit + A megadott üzenet szpojlerként küldése + Szpojler + Adj meg kulcsszavakat a reakció megtalálásához. + + Nem hagysz figyelmen kívül senkit + + A szoba további beállításait egy hosszú kattintással érheted el + + + %1$s hozzáférhetővé tette a szobát bárkinek, aki ismeri a linket. + %1$s beállította, hogy a szobába csak meghívóval lehessen belépni. + Olvasatlan üzenetek + + Szabadítsd fel a kommunikációdat + Beszélgess másokkal közvetlenül vagy csoportosan + Legyen a beszélgetés bizalmas a titkosítással + Bővítsd és szabd testre a élményt + Kezd el + + Válassz szervert + Mit ha e-mail lenne, egy felhasználód van de bárkivel tudsz beszélgetni + Milliók csatlakoznak ingyen a legnagyobb nyilvános szerveren + Prémium üzemeltetés szervezetek részére + Tudj meg többet + Egyéb + Egyedi és haladó beállítások + + Folytatás + Csatlakozás ide: %1$s + Csatlakozás Modularhoz + Csatlakozás egyedi matrix szerverhez + Bejelentkezés ide: %1$s + Fiók készítés + Bejelentkezés + SSO-val való folytatás + + Modular Cím + Cím + Prémium üzemeltetés szervezetek részére + Add meg a Modular Riot vagy a Szerver cím amit használni szeretnél + Add meg a szerver vagy Riot címét amihez csatlakozni szeretnél + + Az oldal betöltésekor hiba történt: %1$s (%2$d) + Ne haragudj, ez a szerver nem fogad új fiókokat. + Ez az e-mail cím egyik fiókhoz sincs társítva. + + Jelszó visszaállítása itt: %1$s + Egy ellenőrző e-mail lesz elküldve a címedre, hogy megerősíthesd az új jelszó beállításodat. + Következő + E-mail + Új jelszó + + Figyelem! + A jelszóváltoztatás megváltoztatja minden eszközön az összes végponttól végpontig titkosításhoz használt kulcsodat; így a titkosított csevegések olvashatatlanok lesznek. Készíts biztonsági másolatot vagy mentsd ki a szoba kulcsaidat minden eszközödön mielőtt megváltoztatod a jelszavad. + Folytatás + + Ez az e-mail cím egyik fiókhoz sincs társítva + + Nézd meg a bejövő e-mailjeidet + Az ellenőrző e-mail ide küldtük: %1$s. + Ellenőriztem az e-mail címemet + + Sikerült! + A jelszavad újra beállításra került. + Minden eszközödről ki vagy jelentkeztetve és „push” értesítéseket sem fogsz kapni. Az értesítések újbóli engedélyezéséhez újra be kell jelentkezned minden eszközön. + Vissza a belépéshez + + Figyelmeztetés + A jelszavad még nem módosult. +\n +\nMegszakítod a jelszó módosítást\? + + E-mail cím beállítása + E-mail + E-mail (nem kötelező) + Következő + + Telefonszám beállítása + Telefonszám beállítása, hogy az ismerősök megtalálhassanak. + Kérlek a nemzetközi formátumot használd. + Telefonszám + Telefonszám (nem kötelező) + Következő + + Telefonszám ellenőrzése + Elküldtük a kódot ide: %1$s. Add meg itt alul amivel ellenőrizhetjük, hogy te te vagy. + Kód megadása + Küld újra + Következő + + Az alkalmazás nem tud bejelentkezni a Matrix szerverbe. A Matrix szerver ezeket a bejelentkezési módokat támogatja: %1$s. +\n +\nWeb klienssel szeretnél bejelentkezni\? + Az alkalmazás nem tud fiókot készíteni ezen a Matrix szerveren. +\n +\nWeb klienssel szeretnél bejelentkezni\? + + Koppints a linkre az új jelszó megerősítéséhez. Miután követted a linket, kattints alább. + Állíts be egy e-mail címet a fiókod visszaállításához. Később esetleg engedélyezheted, hogy ismerősök e-mail címmel megtalálhassanak. + Nemzetközi telefonszámnak „+” jellel kell kezdődnie + A telefonszám érvénytelennek látszik. Kérlek ellenőrizd + + Bejelentkezés ide: %1$s + Felhasználónév vagy e-mail + Jelszó + Következő + A felhasználónév már használatban van + Figyelmeztetés + A felhasználói fiókod még nincs kész. +\n +\nMegállítód a regisztrációt\? + + matrix.org kiválasztása + Modular kiválasztása + Egyedi matrix szerver kiválasztása + Kérlek old meg a captcha-t + Folytatáshoz fogadd el a feltételeket + + Kérlek ellenőrizd az e-mailed + E-mailt küldtünk ide: %1$s. +\nKérlek kattints a benne lévő linkre a fiók készítés folytatásához. + A beírt kód helytelen. Kérlek ellenőrizd. + Elavult matrix szerver + Ez a matrix szerver túl elavult a csatlakozáshoz. Kérd meg a matrix szerver adminisztrátorát a frissítésre. + + + Túl sok kérés lett elküldve. %1$d másodperc múlva újra próbálhatod… + Túl sok kérés lett elküldve. %1$d másodperc múlva újra próbálhatod… + + + Látták: + + Kijelentkeztél + A következő okok miatt lehet: +\n +\n• Másik eszközön megváltoztattad a jelszavadat. +\n +\n• Törölted ezt az eszközt egy másik eszközről. +\n +\n• A matrix szerver adminisztrátora biztonsági okokból érvénytelenítette a hozzáférésed. + Lépj be újra + + Kijelentkeztél + Bejelentkezés + A matrix szerver (%1$s) adminisztrátora kiléptetett a felhasználói fiókodból %2$s (%3$s). + A csak ezen az eszközön meglévő titkosítási kulcsokhoz való hozzáféréshez be kell jelentkezned. Ahhoz hogy bármelyik eszközön elolvashasd a titkosított üzeneteidet szükséged lesz rájuk. + Bejelentkezés + Jelszó + Személyes adatok törlése + Figyelmeztetés: A személyes adataid (beleértve a titkosító kulcsokat is) továbbra is az eszközön vannak tárolva. +\n +\nHa az eszközt nem használod tovább vagy másik fiókba szeretnél bejelentkezni, töröld őket. + Minden adat törlése + + Adat törlése + Biztos vagy benne, hogy minden az eszközön tárolt adatot törölni szeretnél\? +\nA fiók és az üzeneteid eléréséhez jelentkezz be. + Elveszted a hozzáférésedet a titkosított üzeneteidhez ha nem jelentkezel be a titkosítási kulcsok visszaállításához. + Adat törlése + A jelenlegi munkamenet %1$s felhasználóhoz tartozik és %2$s azonosítási adatait adtad meg. Ez RiotX-ben nem támogatott. +\nElőször töröld az adatokat, majd a másik felhasználói fiókba lépj be. + + A matrix.to linked hibás + A leírás túl rövid + diff --git a/vector/src/main/res/values-it/strings.xml b/vector/src/main/res/values-it/strings.xml index ddc19fe125..f79cca9f70 100644 --- a/vector/src/main/res/values-it/strings.xml +++ b/vector/src/main/res/values-it/strings.xml @@ -1615,7 +1615,7 @@ \n \nRiotX supporta: • Accesso ad account esistente • Crea stanze ed entra in stanze pubbliche • Accetta e rifiuta inviti • Elenca stanze utenti • Vedi dettagli stanza • Invia messaggi di testo • Invia allegati • Leggi e scrivi messaggi in stanze cifrate • Crypto: backup chiavi E2E, verifica avanzata dispositivi, richiesta e risposta condivisione chiavi • Notifiche push • Tema chiaro, scuro e nero \n -\nNon tutte le funzioni di Riot sono già implementate in RiotX. Principali funzioni mancanti (prossimamente!): • Creazione account • Impostazioni stanza (elenca membri stanza, ecc.) • Chiamate • Widget • … +\nNon tutte le funzioni di Riot sono già implementate in RiotX. Principali funzioni mancanti (prossimamente!): • Impostazioni stanza (elenca membri stanza, ecc.) • Chiamate • Widget • … Non hai nulla di nuovo da vedere! Messaggi diretti @@ -1791,7 +1791,7 @@ È spam È inappropriato - Segnalazione personalizzata + Segnalazione personalizzata… Segnala questo contenuto Motivo della segnalazione SEGNALA @@ -1851,4 +1851,179 @@ Usa il microfono Leggi media protetti da DRM + Questo non è un indirizzo di server Matrix valido + Blocca utente + + Tutti i messaggi (rumoroso) + Tutti i messaggi + Solo citazioni + Silenzioso + Impostazioni + Lascia la stanza + %1$s non ha fatto modifiche + Invia il messaggio come spoiler + Spoiler + Digita parole chiave per trovare una reazione. + + Non stai ignorando alcun utente + + Tieni premuto su una stanza per altre opzioni + + + %1$s ha reso pubblica la stanza a chiunque conosca il collegamento. + %1$s ha reso la stanza solo ad invito. + Messaggi non letti + + Libera le tue comunicazioni + Chatta con persone direttamente o in gruppi + Tieni private le conversazioni con la cifratura + Estendi e personalizza la tua esperienza + Inizia + + Seleziona un server + Proprio come le email, gli account hanno una sola origine, ma puoi parlare con chiunque + Unisciti a milioni gratuitamente sul server pubblico più grande + Hosting premium per organizzazioni + Maggiori info + Altro + Impostazioni personalizzate ed avanzate + + Continua + Connetti a %1$s + Connetti a Modular + Connetti ad un server personalizzato + Accedi a %1$s + Registrati + Accedi + Continua con SSO + + Indirizzo Modular + Indirizzo + Hosting premium per organizzazioni + Inserisci l\'indirizzo del Riot Modular o del server che vuoi usare + Inserisci l\'indirizzo di un server o di un Riot a cui vuoi connetterti + + Si è verificato un errore caricando la pagina: %1$s (%2$d) + L\'applicazione non riesce ad accedere a questo homeserver. L\'homeserver supporta i seguenti tipi di accesso: %1$s. +\n +\nVuoi accedere usando un client web\? + Spiacenti, questo server non accetta nuovi account. + L\'applicazione non riesce a creare un account su questo homeserver. +\n +\nVuoi registrarti usando un client web\? + + Questa email non è associata ad alcun account. + + Reimposta password su %1$s + Verrà inviata un\'email di verifica nella tua posta per confermare l\'impostazione della nuova password. + Avanti + Email + Nuova password + + Attenzione! + Cambiare la password reimposterà qualunque chiave di cifratura end-to-end su tutti i tuoi dispositivi, rendendo illeggibile la cronologia delle chat criptate. Imposta il Backup Chiavi o esporta le tue chiavi della stanza da un altro dispositivo prima di reimpostare la password. + Continua + + Questa email non è collegata ad alcun account + + Controlla la tua posta + Un\'email di verifica è stata inviata a %1$s. + Tocca il collegamento per confermare la tua nuova password. Una volta seguito il collegamento contenuto, clicca sotto. + Ho verificato il mio indirizzo email + + Successo! + La tua password è stata reimpostata. + Sei stato disconnesso da tutti i dispositivi e non riceverai più notifiche push. Per riattivare le notifiche, riaccedi su ogni dispositivo. + Torna all\'accesso + + Attenzione + La tua password non è ancora cambiata. +\n +\nFermare il processo di cambio password\? + + Imposta indirizzo email + Imposta un\'email per recuperare il tuo account. Più tardi potrai permettere facoltativamente alle persone che conosci di trovarti tramite la tua email. + Email + Email (facoltativa) + Avanti + + Imposta numero di telefono + Imposta un numero di telefono per permettere facoltativamente alle persone che conosci di trovarti. + Si prega di usare il formato internazionale. + Numero di telefono + Numero di telefono (facoltativo) + Avanti + + Conferma numero di telefono + Abbiamo inviato un codice a %1$s. Inseriscilo sotto per verificare che sei tu. + Inserisci codice + Invia di nuovo + Avanti + + I numeri di telefono internazionali devono iniziare con \'+\' + Il numero di telefono non sembra valido. Ricontrollalo + + Registrati su %1$s + Nome utente o email + Password + Avanti + Quel nome utente esiste già + Attenzione + Il tuo account non è ancora stato creato. +\n +\nFermare il processo di registrazione\? + + Seleziona matrix.org + Seleziona Modular + Seleziona un server personalizzato + Completa la verifica Captcha + Accetta le condizioni per continuare + + Controlla la tua email + Abbiamo inviato un\'email a %1$s. +\nClicca il collegamento contenuto per continuare la creazione dell\'account. + Il codice inserito non è corretto. Ricontrollalo. + Homeserver obsoleto + Questo homerserver è di una versione troppo vecchia per connettersi. Chiedi all\'amministratore dell\'homeserver di aggiornarlo. + + + Sono state inviate troppe richieste. Puoi riprovare in %1$d secondo… + Sono state inviate troppe richieste. Puoi riprovare in %1$d secondi… + + + Visto da + + Sei disconnesso + Può essere dovuto a vari motivi: +\n +\n• Hai cambiato la password su un altro dispositivo. +\n +\n• Hai eliminato questo dispositivo da un altro dispositivo. +\n +\n• L\'amministratore del server ha bloccato il tuo accesso per motivi di sicurezza. + Accedi di nuovo + + Sei disconnesso + Accedi + L\'amministratore dell\'homeserver (%1$s) ti ha disconnesso dall\'account %2$s (%3$s). + Accedi per recuperare le chiavi di cifratura memorizzate esclusivamente su questo dispositivo. Ti servono per leggere tutti i tuoi messaggi sicuri su qualsiasi dispositivo. + Accedi + Password + Elimina i dati personali + Attenzione: i tuoi dati personali (incluse chiavi di cifratura) sono ancora in questo dispositivo. +\n +\nEliminali se hai finito di usare questo dispositivo, o se vuoi accedere ad un altro account. + Elimina tutti i dati + + Elimina i dati + Eliminare tutti i dati attualmente presenti in questo dispositivo\? +\nRiaccedi per avere accesso ai dati dell\'account e ai messaggi. + Perderai l\'accesso ai messaggi sicuri a meno che non accedi per recuperare le tue chiavi di cifratura. + Elimina i dati + La sessione attuale è per l\'utente %1$s e hai fornito le credenziali per l\'utente %2$s. Ciò non è supportato da RiotX. +\nPrima elimina i dati, poi accedi di nuovo con un altro account. + + Il tuo collegamento matrix.to non è corretto + La descrizione è troppo breve + diff --git a/vector/src/main/res/values-ja/strings.xml b/vector/src/main/res/values-ja/strings.xml index 8b83c85e39..974b03e02e 100644 --- a/vector/src/main/res/values-ja/strings.xml +++ b/vector/src/main/res/values-ja/strings.xml @@ -217,7 +217,7 @@ 端末詳細 ID(端末固有番号) - 端末名 + 公開端末名 端末名 最終接続日 %1$s @ %2$s @@ -281,8 +281,8 @@ この部屋のサーバ内識別ID 実験的 これらは予期せぬ不具合が生じるかもしれない実験的機能です. 慎重に使用してください. - End-to-End 暗号 - End-to-End 暗号を使用中 + エンドツーエンド暗号化 + エンドツーエンド暗号化を使用中 暗号を有効にするためにはログアウトする必要があります. 認証された端末のみで暗号化 この部屋では, この端末から認証されていない端末への暗号送信をしません. @@ -485,9 +485,9 @@ 部屋一覧 外観 - End-to-end 暗号についての情報 + エンドツーエンド暗号化についての情報 - 端末名 + 公開端末名 部屋のEnd-to-end暗号鍵を出力 認証 履歴を検索 @@ -585,7 +585,7 @@ Riotアプリがあなたの電話帳へアクセスすることを許可しま 不具合報告 - レセプトリストを読み込む + 開封確認メッセージのリスト ダウンロードファイルに保存しますか? この招待は%sさんに送られましたが、このアカウントには関連づけられていません。 他のアカウントでログインするか、このメールアドレスをあなたののアカウントに追加できます。 @@ -660,7 +660,7 @@ Riotアプリがあなたの電話帳へアクセスすることを許可しま 復号エラー 発信者装置の情報 - 名前 + 公開端末名 デバイスキー Ed25519 フィンガープリント @@ -1016,4 +1016,43 @@ Matrixでのメッセージの可視性は電子メールと同様です。メ \n%1$s または %2$s として参加 音声 ビデオ + 詳細な通知設定 + バックグラウンド同期モード (実験) + バッテリーを考慮して最適化 + リアルタイム性を重視して最適化 + バックグラウンド同期を行わない + 優先同期間隔 + %s +\n同期は、デバイスのリソース (バッテリ残量) または状態 (スリープ) に応じて延期される場合があります。 + 文字入力中通知を送信 + 文字入力中であることを他の参加者に伝えます。 + 開封確認メッセージを表示 + 開封確認メッセージをクリックすると詳細なリストを確認できます。 + エンター入力でメッセージを送信 + ソフトウェアキーボードのエンターボタンを押した際に、改行を追加する代わりにメッセージを送信します。 + + パスワード + パスワードを更新 + パスワードが無効です + メディア + シャッター音を再生 + + 公開端末名 (会話を行うユーザーに表示されます) + 音なし + パスワードを表示 + パスワードを隠す + 新しいパスワード + + パスワード + パスワード + 今ここでサインアウトすると、あなたの暗号化されたメッセージは失われてしまいます + 鍵のバックアップは現在処理中です。処理中にサインアウトすると暗号化されたメッセージにアクセスできなくなります。 + 暗号化されたメッセージにアクセスできなくなることを防ぐため、鍵の安全なバックアップはあなたのデバイス全てで有効化してください。 + 暗号化されたメッセージは不要です + 鍵をバックアップしています… + 鍵のバックアップを使用 + 続行しますか? + バックアップ + サインアウトする前に鍵をバックアップしないと、暗号化されたメッセージにアクセスできなくなります。 + diff --git a/vector/src/main/res/values-ko/strings.xml b/vector/src/main/res/values-ko/strings.xml index 76dafef323..14f7a082af 100644 --- a/vector/src/main/res/values-ko/strings.xml +++ b/vector/src/main/res/values-ko/strings.xml @@ -1495,7 +1495,7 @@ \n \nRiotX 지원: • 존재하는 계정으로 로그인 • 방을 만들고 공공 방에 참가 • 초대를 수락하거나 거절 • 사용자 방 목록 • 방 세부 정보 보기 • 문자 메시지 보내기 • 첨부 파일 보내기 • 암호화된 방에서 메시지 읽고 쓰기 • 암호화: 종단간 암호화 키 백업, 고급 기기 확인, 키 공유 요청과 답장 • 푸시 알림 • 밝은 테마, 어두운 테마 그리고 검정 테마 \n -\n아직 Riot의 모든 기능이 RiotX에 구현되지 않았습니다. 주요 없는 (그리고 곧 나올!) 기능: • 계정 만들기 • 방 설정 (방 구성원 목록 등) • 전화 • 위젯 • … +\n아직 Riot의 모든 기능이 RiotX에 구현되지 않았습니다. 주요 없는 (그리고 곧 나올!) 기능: • 방 설정 (방 구성원 목록 등) • 전화 • 위젯 • … 다이렉트 메시지 diff --git a/vector/src/main/res/values-ru/strings.xml b/vector/src/main/res/values-ru/strings.xml index e815f01b9f..014f298545 100644 --- a/vector/src/main/res/values-ru/strings.xml +++ b/vector/src/main/res/values-ru/strings.xml @@ -1659,7 +1659,7 @@ \n \nПоддержка RiotX: - Войти в существующую учетную запись - Создать комнату и присоединиться к общедоступным комнатам - Принять и отклонить приглашения - Список комнат пользователей - Просмотр сведений о комнате - Отправить текстовые сообщения - Отправить вложение - Читать и писать сообщения в зашифрованных комнатах - Криптография: Резервное копирование клавиш E2E, предварительная проверка устройства, запрос и ответ на общий доступ к ключам - Нажмите уведомление - Светлые и черные темы \n -\nНе все функции Riot пока реализованы в RiotX. Основные отсутствующие (и скоро появятся!) свойства: - Создание учетной записи - Настройки комнат (список членов комнат и т.д.) - Вызовы - Виджеты - .… +\nНе все функции Riot пока реализованы в RiotX. Основные отсутствующие (и скоро появятся!) свойства: - Настройки комнат (список членов комнат и т.д.) - Вызовы - Виджеты - .… Предварительный просмотр открытой комнаты в RiotX пока не поддерживается @@ -1797,4 +1797,17 @@ Откройте навигационный ящик Откройте меню «Создать комнату» + Неверный адрес сервера Matrix + Подтвердите пароль + Требуется аутентификация + + + Интеграции + Разрешить интеграции + Виджет + Загрузить виджет + Перезагрузить виджет + Открыть в браузере + ID виджета + Принять diff --git a/vector/src/main/res/values-sq/strings.xml b/vector/src/main/res/values-sq/strings.xml index f2ff6691f3..b0e3137900 100644 --- a/vector/src/main/res/values-sq/strings.xml +++ b/vector/src/main/res/values-sq/strings.xml @@ -1521,7 +1521,7 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani \n \nRiotX-i mbulon: • Hyrje në një llogari ekzistuese • Krijim dhome dhe pjesëmarrje në dhoma publike • Pranim dhe hedhje poshtë ftesash • Njohje të dhomave të përdoruesve • Parje hollësish dhome • Dërgim mesazhesh tekst • Dërgim bashkëngjitjesh • Lexim dhe shkrim mesazhesh në dhoma të fshehtëzuara • Kriptografi: kopjeruajtje kyçesh E2E, verifikim i thelluar pajisjesh, kërkesa dhe përgjigje për ndarje kyçesh • Njoftime push • Tema të Çelëta, të Errëta dhe të Zeza \n -\nNë RiotX s’janë sendërtuar ende krejt veçoritë e Riot-it. Veçori kryesore që mungojnë (dhe që do të vijnë së shpejti!): • Krijim llogarish • Rregullime dhome (shfaqje anëtarësh dhome, etj.) • Thirrje • Widget-es • … +\nNë RiotX s’janë sendërtuar ende krejt veçoritë e Riot-it. Veçori kryesore që mungojnë (dhe që do të vijnë së shpejti!): • Rregullime dhome (shfaqje anëtarësh dhome, etj.) • Thirrje • Widget-es • … Përgjegjës Integrimesh @@ -1696,7 +1696,7 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani Ngjitës Është e padëshiruar Është e papërshtatshme - Raport vetjak + Raport vetjak… Raportojeni këtë lëndë Arsye për raportimin e kësaj lënde RAPORTOJENI @@ -1757,4 +1757,180 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani Të lexojë Media të mbrojtur me DRM Mbyll banderolë kopjeruajtjeje kyçesh + Kjo s’është adresë e vlefshme shërbyesi Matrix + Hashi i lëndës s’u përputh me atë që pritej + S’trajtoi dot të dhëna ndarjeje + + Bllokoje përdoruesin + + Krejt mesazhet (e zhurmshme) + Krejt mesazhet + Vetëm përmendje + Pa Zë + Rregullime + Braktiseni dhomën + %1$s s’bëri ndryshime + Shtypni ca fjalëkyçe që të gjendet një reagim. + + S’jeni duke shpërfillur ndonjë përdorues + + Kryeni klikim të gjatë në një dhomë që të shihni më tepër mundësi + + + %1$s e bëri dhomën publike për këdo që di lidhjen. + %1$s e bëri dhomën vetëm me ftesa. + Mesazhe të palexuar + + Çlironi komunikimet tuaj + Fjalosuni me njerëzit drejtpërsëdrejti ose në grupe + Mbajini bisedat private, përmes fshehtëzimit + Zgjeroni & përshtatni punimin tuaj + Si t’ia fillohet + + Përzgjidhni një shërbyes + Ashtu si te email-et, llogaritë kanë një shtëpi të tyren, edhe pse mund të bisedoni me këdo + Bashkojuni milionave, falas, në shërbyesin më të madh publik + Strehim me pagesë për ente + Mësoni më tepër + Tjetër + Rregullime vetjake & të mëtejshme + + Vazhdo + Lidhu me %1$s + Lidhu me një shërbyes modular + Lidhu me një shërbyes vetjak + Hyni te %1$s + Regjistrohuni + Hyni + Vazhdoni me SSO + + Adresë Modulari + Adresë + Strehim me pagesë për ente + Jepni adresën e Modular Riot-it ose Shërbyesit që doni të përdoret + Jepni adresën e një shërbyesi ose një instance Riot me të cilën doni të lidheni + + Ndodhi një gabim kur ngarkohej faqja: %1$s (%2$d) + Aplikacioni s’është në gjendje të bëjë hyrje në llogari në këtë shërbyes Home. Shërbyesi Home mbulon llojet vijuese të hyrjes(ve): %1$s. +\n +\nDoni të hyni duke përdorur një klient web\? + Na ndjeni, ky shërbyes s’pranon llogari të reja. + Aplikacioni s’është në gjendje të krijojë llogari në këtë shërbyes Home. +\n +\nDoni të regjistroheni duke përdorur një klient web\? + + Ky emai s’është përshoqëruar me ndonjë llogari. + + Ricaktoni fjalëkalimin në %1$s + Te mesazhet tuaj do të dërgohet një email verifikimi, për të ripohuar caktimin e fjalëkalimit tuaj të ri. + Pasuesi + Email + 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. + Vazhdo + + Ky email s’është i lidhur me ndonjë llogari + + Kontrolloni te mesazhet tuaj të marrë + Një email verifikimi u dërgua te %1$s. + Prekni mbi lidhjen që të ripohohet fjalëkalimi juaj i ri. Pasi të keni ndjekur lidhjen që përmban, klikoni më poshtë. + E kam verifikuar adresën time email + + 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. + Mbrapsht te Hyrja + + Kujdes + Fjalëkalimi juaj s’ka ndryshuar ende. +\n +\nTë ndalet procesi i ndryshimit të fjalëkalimit\? + + Caktoni adresë email + Caktoni një email për rimarrje të llogarisë tuaj. Më vonë, mundeni të lejoni persona që njihni t’ju zbulojnë përmes email-it tuaj. + Email + Email (në daçi) + Pasuesi + + Caktoni numër telefoni + Caktoni një numër telefoni për t’i lejuar, në daçi, nejrëzit t’ju gjejnë. + Ju lutemi, përdorni formatin ndërkombëtar. + Numër telefoni + Numër telefoni (opsionale) + Pasuesi + + Ripohoni numër telefoni + Sapo dërguam një kod te %1$s. Jepeni më poshtë që të verifikohet se jeni ju. + Jepni kod + Ridërgoje + Pasuesi + + Numrat e telefonave ndëkombëtarë duhet të fillojnë me \'+\' + Numri i telefonit duket se është i vlefshëm. Ju lutemi, kontrollojeni + + Regjistrohuni te %1$s + Emër përdoruesi ose email + Fjalëkalim + Pasuesi + Ai emër përdoruesi është i zënë + Kujdes + Llogaria juaj s’është krijuar ende. +\n +\nTë ndalet procesi i regjistrimit\? + + Përzgjidhni matrix.org + Përzgjidhni modular + Përzgjidhni një shërbyes Home vetjak + Ju lutemi, zgjidhni captcha-n + Që të vazhdohet, pranoni terma + + Ju lutemi, kontrolloni email-in tuaj + Sapo dërguam një email te %1$s. +\nJu lutemi, klikoni mbi lidhjen që përmban, që të vazhdohet krijimi i llogarisë. + Kodi që dhatë s’është i saktë. Ju lutemi, kontrollojeni. + Shërbyes Home i vjetruar + Ky shërbyes Home xhiron një version shumë të vjetër për t’u lidhur me të. Kërkojini përgjegjësit të shërbyesit tuaj Home ta përmirësojë. + + + Janë dërgua shumë kërkesa. Mund të riprovoni pas %1$d sekonde… + Janë dërgua shumë kërkesa. Mund të riprovoni pas %1$d sekondash… + + + Parë nga + + Keni bërë dalje + Mund të vijë për arsye të ndryshme: +\n +\n• Keni ndryshuar fjalëkalimin tuaj në një pajisje tjetër. +\n +\n• E keni fshirë këtë pajisje prej një pajisjeje tjetër. +\n +\n• Përgjegjësi i shërbyesit tuaj e ka bërë të pamundur hyrjen tuaj për arsye sigurie. + Ribëni hyrjen + + Keni bërë dalje + Hyni + Përgjegjësi i shërbyesit tuaj Home (%1$s) ju ka nxjerrë jashtë llogarisë tuaj %2$s (%3$s). + Bëni hyrjen, që të rimerrni kyçe fshehtëzimi të depozituar përjashtimisht në këtë pajisje. Do t’ju duhen për të lexuar krejt mesazhet tuaj të siguruar në çfarëdo pajisje. + Hyni + Fjalëkalim + Spastro të dhëna personale + Kujdes: Të dhënat tuaja personale (përfshi kyçe fshehtëzimi) janë ende të depozituara në këtë pajisje. +\n +\nSpastrojini, nëse keni përfunduar së përdoruri këtë pajisje, ose dëshironi të bëni hyrjen në një tjetër llogari. + Spastro krejt të dhënat + + Spastroni të dhëna + Të spastrohen krejt të dhënat e depozituara aktualisht në këtë pajisje\? +\nQë të mund të hyni te të dhëna të llogarisë tuaj dhe te mesazhe, bëni sërish hyrjen. + Do të humbni hyrje te mesazhe të sigurt, veç në hyfshi për të rimarrë kyçet tuaj të fshehtëzimit. + Spastro të dhënat + Sesioni i tanishëm është për përdoruesin %1$s dhe ju jepni kredenciale për përdoruesin %2$s. Kjo nuk mbulohet nga RiotX. +\nJu lutemi, së pari spastroni të dhëna, mandej hyni sërish në një tjetër llogari. + + Lidhja juaj matrix.to është e keqformuar + Përshkrimi është shumë i shkurtër + diff --git a/vector/src/main/res/values-uk/strings.xml b/vector/src/main/res/values-uk/strings.xml index c5f2dc7022..4086598171 100755 --- a/vector/src/main/res/values-uk/strings.xml +++ b/vector/src/main/res/values-uk/strings.xml @@ -1128,4 +1128,5 @@ %1$d/%2$d ключ(і/ів) успішно імпортовано. + Позначити як прочитане diff --git a/vector/src/main/res/values-zh-rTW/strings.xml b/vector/src/main/res/values-zh-rTW/strings.xml index 577c1ef955..b0f66466aa 100644 --- a/vector/src/main/res/values-zh-rTW/strings.xml +++ b/vector/src/main/res/values-zh-rTW/strings.xml @@ -1522,7 +1522,7 @@ Matrix 中的消息可見度類似于電子郵件。我們忘記您的郵件意 \n \nRiotX 支援:• 登入到既有的帳號 • 建立聊天室與加入公開聊天室 • 接受與回絕邀請 • 列出使用者聊天室 • 檢視聊天室詳細資訊 • 傳送文字訊息 • 傳送附件 • 讀取與編寫已加密的聊天室 • 加密:E2E 金鑰備份、進階裝置驗證、金鑰分享請求與回應 • 推送通知 • 亮、暗與黑色主題 \n -\n不是所有 Riot 的功能都已在 RiotX 中實作。主要缺少(會在稍後到來!)的功能:• 建立帳號 • 聊天室設定(列出聊天室成員等) • 通話 • 小工具 • … +\n不是所有 Riot 的功能都已在 RiotX 中實作。主要缺少(會在稍後到來!)的功能 • 聊天室設定(列出聊天室成員等) • 通話 • 小工具 • … 直接訊息 @@ -1695,7 +1695,7 @@ Matrix 中的消息可見度類似于電子郵件。我們忘記您的郵件意 垃圾訊息 不合適 - 自訂回報 + 自訂回報…… 回報此內容 回報此內容的理由 回報 @@ -1757,4 +1757,178 @@ Matrix 中的消息可見度類似于電子郵件。我們忘記您的郵件意 使用麥克風 讀取 DRM 保護的媒體 + 這不是有效的 Matrix 伺服器位置 + 封鎖使用者 + + 所有訊息(吵雜) + 所有訊息 + 僅提及 + 靜音 + 設定 + 離開聊天室 + %1$s 未做出任何變更 + 傳送為擾亂者指定的訊息 + 擾亂者 + 輸入關鍵字以尋找反應。 + + 您未忽略任何使用者 + + 長按聊天室以檢視更多選項 + + + %1$s 將聊天室設為公開給所有知道連結的人。 + %1$s 將聊天室設為僅邀請可進入。 + 未讀訊息 + + 讓您的通訊自由 + 直接或在群組中與夥伴們聊天 + 透過加密讓對話保持隱密 + 擴展並自訂您的體驗 + 開始 + + 選取伺服器 + 就像電子郵件,帳號也有一個家,不過您還是可以跟任何人交談 + 在最大的公開伺服器上免費加入數百萬人之中 + 組織另有專業主機可用 + 了解更多 + 其他 + 自訂與進階設定 + + 繼續 + 連線到 %1$s + 連線到 Modular + 連線到自訂伺服器 + 登入到 %1$s + 註冊 + 登入 + 以 SSO 繼續 + + Modular 位置 + 位置 + 組織有專業主機 + 輸入 Modular Riot 或您想要使用的伺服器位置 + 輸入您想要連線的伺服器或 Riot 的位置 + + 載入頁面時發生錯誤:%1$s (%2$d) + 應用程式無法登入此家伺服器。家伺服器支援以下登入類型:%1$s。 +\n +\n您想要使用網路客戶端登入嗎? + 抱歉,此伺服器不接受新帳號。 + 應用程式無法在此家伺服器上建立帳號。 +\n +\n您想要使用網路客戶端註冊嗎? + + 此電子郵件未關聯到任何帳號。 + + 在 %1$s 上重設密碼 + 驗證郵件已傳送到您的收件匣以確認您要設定新密碼。 + 下一步 + 電子郵件 + 新密碼 + + 警告! + 變更您的密碼將會重設在您所有裝置上任何的端到端加密金鑰,讓已加密的聊天歷史無法讀取。請在重設您的密碼前從其他裝置設定金鑰備份或匯出您的聊天室金鑰。 + 繼續 + + 此電子郵件未被連結到任何帳號 + + 檢查您的收件匣 + 驗證電子郵件已傳送至 %1$s。 + 輕點連結以確認您的新密碼。在您使用了其中包含的連結後,請點擊下方。 + 我已經驗證了我的電子郵件地址 + + 成功! + 您的密碼已被重設。 + 您已登出所有裝置,且不會再收到推播通知。要重新啟用通知,請在裝置上再次登入。 + 返回登入 + + 警告 + 您的密碼未變更。 +\n +\n停止密碼變更流程? + + 設定電子郵件地址 + 設定電子郵件以復原您的帳號。之後您也可以選擇性地讓您認識的人透過您的電子郵件找到您。 + 電子郵件 + 電子郵件(選擇性) + 下一個 + + 設定電話號碼 + 設定電話號碼以讓您認識的人找到您。 + 請使用國際格式。 + 電話號碼 + 電話號碼(選擇性) + 下一個 + + 確認電話號碼 + 我們剛傳送了驗證碼給 %1$s。在下方輸入以驗證是您。 + 輸入驗證碼 + 再次傳送 + 下一個 + + 國際電話號碼必須以加號開頭 + 電話號碼似乎無效。請檢查 + + 註冊至 %1$s + 使用者名稱或電子郵件 + 密碼 + 下一個 + 使用者名稱已被使用 + 警告 + 尚未建立您的帳號。 +\n +\n停止註冊程序? + + 選取 matrix.org + 選取 modular + 選取自訂的家伺服器 + 請執行 captcha 挑戰 + 接受條款以繼續 + + 請檢查您的電子郵件 + 我們剛傳送電子郵件給 %1$s。 +\n請點擊其中所包含的連結以繼續建立帳號。 + 輸入的驗證碼不正確。請檢查。 + 未更新的家伺服器 + 此家伺服器所執行的版本太舊,所以無法連線。請要求您的家伺服器管理員升級。 + + + 傳送了太多請求。您可以在 %1$d 秒後重試…… + + + 檢視由 + + 您已登出 + 這可能有多種原因: +\n +\n• 您已在其他裝置上變更您的密碼。 +\n +\n• 您已從其他裝置刪除此裝置。 +\n +\n• 您伺服器的管理員為了安全性讓您的存取無效。 + 再次登入 + + 您已登出 + 登入 + 您的家伺服器 (%1$s) 管理員已將您的帳號 %2$s (%3$s) 登出。 + 登入以復原僅儲存在此裝置上的加密金鑰。您在任何裝置上都需要它們來讀取您所有的安全訊息。 + 登入 + 密碼 + 清除個人資料 + 警告:您的個人資料(包含加密金鑰)仍儲存在此裝置上。 +\n +\n如果您已不想使用此裝置,請將其清除,或是登入到其他帳號。 + 清除所有資料 + + 清除資料 + 清除目前儲存在此裝置上的所有資料嗎? +\n再次登入以存取您的帳號資料與訊息。 + 除非您登入以復原您的加密金鑰,否則您將會失去對安全訊息的存取權。 + 清除資料 + 使用者 %1$s 目前的工作階段與您提供的使用者 %2$s 憑證。RiotX 並不支援。 +\n請先清除您的資料,然後再以其他帳號登入。 + + 您的 matrix.to 連結格式錯誤 + 描述太短了 + diff --git a/vector/src/main/res/values/attrs.xml b/vector/src/main/res/values/attrs.xml index c30a1d99d9..aca2a7fa5f 100644 --- a/vector/src/main/res/values/attrs.xml +++ b/vector/src/main/res/values/attrs.xml @@ -63,9 +63,6 @@ - - - diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 213600b2b4..a05bc3511d 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1658,7 +1658,6 @@ RiotX supports: • Light, Dark and Black themes Not all features in Riot are implemented in RiotX yet. Main missing (and coming soon!) features: -• Account creation • Room settings (list room members, etc.) • Calls • Widgets @@ -1964,4 +1963,22 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming Your matrix.to link was malformed The description is too short + Initial Sync… + + See all my devices + Advanced settings + Developer mode + The developer mode activates hidden features and may also make the application less stable. For developers only! + Rageshake + Detection threshold + Shake your phone to test the detection threshold + Shake detected! + Settings + Current device + Other devices + + Showing only the first results, type more letters… + + Fail-fast + RiotX may crash more often when an unexpected error occurs diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 6c3833a782..0a5cfc1ede 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -6,7 +6,7 @@ Prepends ¯\\_(ツ)_/¯ to a plain-text message - Initial Sync… + Your email domain is not authorized to register on this server Untrusted sign in They match diff --git a/vector/src/main/res/values/theme_black.xml b/vector/src/main/res/values/theme_black.xml index 7398a4bcb7..7bce009429 100644 --- a/vector/src/main/res/values/theme_black.xml +++ b/vector/src/main/res/values/theme_black.xml @@ -72,9 +72,6 @@ @color/primary_color_dark_black @color/list_divider_color_black - - @color/list_divider_color_black - #FF4D4D4D @drawable/pill_receipt_black diff --git a/vector/src/main/res/values/theme_dark.xml b/vector/src/main/res/values/theme_dark.xml index f61a89482a..a05081eec7 100644 --- a/vector/src/main/res/values/theme_dark.xml +++ b/vector/src/main/res/values/theme_dark.xml @@ -139,9 +139,6 @@ #FF61708b - - @color/list_divider_color_dark - @android:color/white @color/riotx_accent diff --git a/vector/src/main/res/values/theme_light.xml b/vector/src/main/res/values/theme_light.xml index aa343a11fc..9cea0e52b7 100644 --- a/vector/src/main/res/values/theme_light.xml +++ b/vector/src/main/res/values/theme_light.xml @@ -139,9 +139,6 @@ #FF61708b - - @color/list_divider_color_light - @android:color/black @color/riotx_accent diff --git a/vector/src/main/res/values/theme_status.xml b/vector/src/main/res/values/theme_status.xml index 322522c723..421632e64c 100644 --- a/vector/src/main/res/values/theme_status.xml +++ b/vector/src/main/res/values/theme_status.xml @@ -88,9 +88,6 @@ #a0a29f - - #e1e1e1 - @color/accent_color_status @color/riotx_accent diff --git a/vector/src/main/res/xml/vector_settings_advanced_settings.xml b/vector/src/main/res/xml/vector_settings_advanced_settings.xml new file mode 100644 index 0000000000..131b43c8d5 --- /dev/null +++ b/vector/src/main/res/xml/vector_settings_advanced_settings.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/xml/vector_settings_general.xml b/vector/src/main/res/xml/vector_settings_general.xml index d1ffe5bcf1..c49eca825a 100644 --- a/vector/src/main/res/xml/vector_settings_general.xml +++ b/vector/src/main/res/xml/vector_settings_general.xml @@ -45,11 +45,10 @@ - - + android:title="@string/settings_contact" + app:isPreferenceVisible="@bool/false_not_implemented"> - - + tools:summary="https://homeserver.org" /> - - - + + android:title="@string/settings_deactivate_account_section" + app:isPreferenceVisible="@bool/false_not_implemented"> - + - + - + - + - + - + - + - + - - - - - + \ No newline at end of file diff --git a/vector/src/main/res/xml/vector_settings_labs.xml b/vector/src/main/res/xml/vector_settings_labs.xml index 530c207749..2661568f77 100644 --- a/vector/src/main/res/xml/vector_settings_labs.xml +++ b/vector/src/main/res/xml/vector_settings_labs.xml @@ -34,22 +34,11 @@ - - - - diff --git a/vector/src/main/res/xml/vector_settings_notification_advanced_preferences.xml b/vector/src/main/res/xml/vector_settings_notification_advanced_preferences.xml index 32b6a2b499..b5f01d98f6 100644 --- a/vector/src/main/res/xml/vector_settings_notification_advanced_preferences.xml +++ b/vector/src/main/res/xml/vector_settings_notification_advanced_preferences.xml @@ -36,8 +36,6 @@ - - - - + android:title="@string/settings_notifications_targets" /--> - - - - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/xml/vector_settings_preferences.xml b/vector/src/main/res/xml/vector_settings_preferences.xml index 96471cfebe..7698372053 100644 --- a/vector/src/main/res/xml/vector_settings_preferences.xml +++ b/vector/src/main/res/xml/vector_settings_preferences.xml @@ -30,13 +30,15 @@ android:defaultValue="true" android:key="SETTINGS_SHOW_URL_PREVIEW_KEY" android:summary="@string/settings_inline_url_preview_summary" - android:title="@string/settings_inline_url_preview" /> + android:title="@string/settings_inline_url_preview" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_send_typing_notifs" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_always_show_timestamps" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_12_24_timestamps" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_show_read_receipts" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_show_join_leave_messages" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_show_avatar_display_name_changes_messages" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_vibrate_on_mention" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_send_message_with_enter" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_info_area_show" + app:isPreferenceVisible="@bool/false_not_implemented" /> - - + android:title="@string/settings_home_display" + app:isPreferenceVisible="@bool/false_not_implemented"> - - - + + + diff --git a/vector/src/main/res/xml/vector_settings_security_privacy.xml b/vector/src/main/res/xml/vector_settings_security_privacy.xml index 9e88da34a1..234ecbe647 100644 --- a/vector/src/main/res/xml/vector_settings_security_privacy.xml +++ b/vector/src/main/res/xml/vector_settings_security_privacy.xml @@ -1,5 +1,6 @@ - + + android:title="@string/encryption_never_send_to_unverified_devices_title" + app:isPreferenceVisible="@bool/false_not_implemented" /> - + + + + + + - - - - - - - + android:title="@string/settings_analytics" + app:isPreferenceVisible="@bool/false_not_implemented"> - \ No newline at end of file