diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml index 1f93d1feee..93ac86f417 100644 --- a/.idea/dictionaries/bmarty.xml +++ b/.idea/dictionaries/bmarty.xml @@ -12,12 +12,15 @@ fdroid gplay hmac + homeserver ktlint linkified linkify megolm msisdn + msisdns pbkdf + pids pkcs riotx signin diff --git a/CHANGES.md b/CHANGES.md index 869b034f45..588a244a06 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,30 @@ +Changes in RiotX 0.21.0 (2020-05-28) +=================================================== + +Features ✨: + - Identity server support (#607) + - Switch language support (#41) + - Display list of attachments of a room (#860) + +Improvements 🙌: + - Better connectivity lost indicator when airplane mode is on + - Add a setting to hide redacted events (#951) + - Render formatted_body for m.notice and m.emote (#1196) + - Change icon to magnifying-glass to filter room (#1384) + +Bugfix 🐛: + - After jump to unread, newer messages are never loaded (#1008) + - Fix issues with FontScale switch (#69, #645) + - "Seen by" uses 12h time (#1378) + - Enable markdown (if active) when sending emote (#734) + - Screenshots for Rageshake now includes Dialogs such as BottomSheet (#1349) + +SDK API changes ⚠️: + - initialize with proxy configuration + +Other changes: + - support new key agreement method for SAS (#1374) + Changes in RiotX 0.20.0 (2020-05-15) =================================================== diff --git a/docs/identity_server.md b/docs/identity_server.md new file mode 100644 index 0000000000..04127c9ab0 --- /dev/null +++ b/docs/identity_server.md @@ -0,0 +1,92 @@ +# Identity server + +Issue: #607 +PR: #1354 + +## Introduction +Identity Servers support contact discovery on Matrix by letting people look up Third Party Identifiers to see if the owner has publicly linked them with their Matrix ID. + +## Implementation + +The current implementation was Inspired by the code from Riot-Android. + +Difference though (list not exhaustive): +- Only API v2 is supported (see https://matrix.org/docs/spec/identity_service/latest) +- Homeserver has to be up to date to support binding (Versions.isLoginAndRegistrationSupportedBySdk() has to return true) +- The SDK managed the session and client secret when binding ThreePid. Those data are not exposed to the client. +- The SDK supports incremental sendAttempt (this is not used by RiotX) +- The "Continue" button is now under the information, and not as the same place that the checkbox +- The app can cancel a binding. Current data are erased from DB. +- The API (IdentityService) is improved. +- A new DB to store data related to the identity server management. + +Missing features (list not exhaustive): +- Invite by 3Pid (will be in a dedicated PR) +- Add email or phone to account (not P1, can be done on Riot-Web) +- List email and phone of the account (could be done in a dedicated PR) +- Search contact (not P1) +- Logout from identity server when user sign out or deactivate his account. + +## Related MSCs +The list can be found here: https://matrix.org/blog/2019/09/27/privacy-improvements-in-synapse-1-4-and-riot-1-4 + +## Steps and requirements + +- Only one identity server by account can be set. The user's choice is stored in account data with key `m.identity_server`. But every clients will managed its own token to log in to the identity server +```json +{ + "type": "m.identity_server", + "content": { + "base_url": "https://matrix.org" + } +} +``` +- The accepted terms are stored in the account data: +```json +{ + "type": "m.accepted_terms", + "content": { + "accepted": [ + "https://vector.im/identity-server-privacy-notice-1" + ] + } +} +``` + +- Default identity server URL, from Wellknown data is proposed to the user. +- Identity server can be set +- Identity server can be changed on another user's device, so when the change is detected (thanks to account data sync) RiotX should properly disconnect from a previous identity server (I think it was not the case in Riot-Android, where we keep the token forever) +- Registration to the identity server is managed with an openId token +- Terms of service can be accepted when configuring the identity server. +- Terms of service can be accepted after, if they change. +- Identity server can be modified +- Identity server can be disconnected with a warning dialog, with special content if there are current bound 3pid on this identity server. +- Email can be bound +- Email can be unbound +- Phone can be bound +- Phone can be unbound +- Look up can be performed, to get matrixIds from local contact book (phone and email): Android permission correctly handled (not done yet) +- Look up pepper can be updated if it is rotated on the identity server +- Invitation using 3PID can be done (See #548) (not done yet) +- Homeserver access-token will never be sent to an identity server +- When user sign-out: logout from the identity server if any. +- When user deactivate account: logout from the identity server if any. + +## Screens + +### Settings + +Identity server settings can be accessed from the internal setting of the application, both from "Discovery" section and from identity detail section. + +### Discovery screen + +This screen displays the identity server configuration and the binding of the user's ThreePid (email and msisdn). This is the main screen of the feature. + +### Set identity server screen + +This screen is a form to set a new identity server URL + +## Ref: +- https://matrix.org/blog/2019/09/27/privacy-improvements-in-synapse-1-4-and-riot-1-4 is a good summary of the role of an Identity server and the proper way to configure and use it in respect to the privacy and the consent of the user. +- API documentation: https://matrix.org/docs/spec/identity_service/latest +- vector.im TOS: https://vector.im/identity-server-privacy-notice 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 e92da1e424..a60e83ec96 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 @@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo import im.vector.matrix.android.api.session.group.GroupSummaryQueryParams import im.vector.matrix.android.api.session.group.model.GroupSummary +import im.vector.matrix.android.api.session.identity.ThreePid 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 @@ -94,6 +95,11 @@ class RxSession(private val session: Session) { return session.getPagedUsersLive(filter, excludedUserIds).asObservable() } + fun liveThreePIds(refreshData: Boolean): Observable> { + return session.getThreePidsLive(refreshData).asObservable() + .startWithCallable { session.getThreePids() } + } + fun createRoom(roomParams: CreateRoomParams): Single = singleBuilder { session.createRoom(roomParams, it) } diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 514d1accae..abc860d1ff 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -158,6 +158,9 @@ dependencies { // Bus implementation 'org.greenrobot:eventbus:3.1.1' + // Phone number https://github.com/google/libphonenumber + implementation 'com.googlecode.libphonenumber:libphonenumber:8.10.23' + debugImplementation 'com.airbnb.okreplay:okreplay:1.5.0' releaseImplementation 'com.airbnb.okreplay:noop:1.5.0' androidTestImplementation 'com.airbnb.okreplay:espresso:1.5.0' diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt index 5bc8653f3d..600bcf2983 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt @@ -28,10 +28,10 @@ import im.vector.matrix.android.api.auth.data.LoginFlowResult import im.vector.matrix.android.api.auth.registration.RegistrationResult import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.api.session.events.model.LocalEcho import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.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.TimelineSettings @@ -88,7 +88,8 @@ class CommonTestHelper(context: Context) { fun syncSession(session: Session) { val lock = CountDownLatch(1) - session.open() + GlobalScope.launch(Dispatchers.Main) { session.open() } + session.startSync(true) val syncLiveData = runBlocking(Dispatchers.Main) { @@ -116,7 +117,7 @@ class CommonTestHelper(context: Context) { */ fun sendTextMessage(room: Room, message: String, nbOfMessages: Int): List { val sentEvents = ArrayList(nbOfMessages) - val latch = CountDownLatch(nbOfMessages) + val latch = CountDownLatch(1) val timelineListener = object : Timeline.Listener { override fun onTimelineFailure(throwable: Throwable) { } @@ -127,7 +128,7 @@ class CommonTestHelper(context: Context) { override fun onTimelineUpdated(snapshot: List) { val newMessages = snapshot - .filter { LocalEcho.isLocalEchoId(it.eventId).not() } + .filter { it.root.sendState == SendState.SYNCED } .filter { it.root.getClearType() == EventType.MESSAGE } .filter { it.root.getClearContent().toModel()?.body?.startsWith(message) == true } @@ -143,7 +144,8 @@ class CommonTestHelper(context: Context) { for (i in 0 until nbOfMessages) { room.sendTextMessage(message + " #" + (i + 1)) } - await(latch) + // Wait 3 second more per message + await(latch, timeout = TestConstants.timeOutMillis + 3_000L * nbOfMessages) timeline.removeListener(timelineListener) timeline.dispose() @@ -291,6 +293,24 @@ class CommonTestHelper(context: Context) { return requestFailure!! } + fun createEventListener(latch: CountDownLatch, predicate: (List) -> Boolean): Timeline.Listener { + return object : Timeline.Listener { + override fun onTimelineFailure(throwable: Throwable) { + // noop + } + + override fun onNewTimelineEvents(eventIds: List) { + // noop + } + + override fun onTimelineUpdated(snapshot: List) { + if (predicate(snapshot)) { + latch.countDown() + } + } + } + } + /** * Await for a latch and ensure the result is true * @@ -349,3 +369,13 @@ class CommonTestHelper(context: Context) { session.close() } } + +fun List.checkSendOrder(baseTextMessage: String, numberOfMessages: Int, startIndex: Int): Boolean { + return drop(startIndex) + .take(numberOfMessages) + .foldRightIndexed(true) { index, timelineEvent, acc -> + val body = timelineEvent.root.content.toModel()?.body + val currentMessageSuffix = numberOfMessages - index + acc && (body == null || body.startsWith(baseTextMessage) && body.endsWith("#$currentMessageSuffix")) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt index 9278bed918..35ad8ff4e1 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt @@ -53,17 +53,19 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { /** * @return alice session */ - fun doE2ETestWithAliceInARoom(): CryptoTestData { + fun doE2ETestWithAliceInARoom(encryptedRoom: Boolean = true): CryptoTestData { val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams) val roomId = mTestHelper.doSync { aliceSession.createRoom(CreateRoomParams(name = "MyRoom"), it) } - val room = aliceSession.getRoom(roomId)!! + if (encryptedRoom) { + val room = aliceSession.getRoom(roomId)!! - mTestHelper.doSync { - room.enableEncryption(callback = it) + mTestHelper.doSync { + room.enableEncryption(callback = it) + } } return CryptoTestData(aliceSession, roomId) @@ -72,8 +74,8 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { /** * @return alice and bob sessions */ - fun doE2ETestWithAliceAndBobInARoom(): CryptoTestData { - val cryptoTestData = doE2ETestWithAliceInARoom() + fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true): CryptoTestData { + val cryptoTestData = doE2ETestWithAliceInARoom(encryptedRoom) val aliceSession = cryptoTestData.firstSession val aliceRoomId = cryptoTestData.roomId @@ -246,7 +248,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { assertNotNull(eventWireContent.get("session_id")) assertNotNull(eventWireContent.get("sender_key")) - assertEquals(senderSession.sessionParams.credentials.deviceId, eventWireContent.get("device_id")) + assertEquals(senderSession.sessionParams.deviceId, eventWireContent.get("device_id")) assertNotNull(event.eventId) assertEquals(roomId, event.roomId) diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/crosssigning/XSigningTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/crosssigning/XSigningTest.kt index f8d30a2679..da3bbdc23c 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/crosssigning/XSigningTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/crosssigning/XSigningTest.kt @@ -122,7 +122,7 @@ class XSigningTest : InstrumentedTest { // We will want to test that in alice POV, this new device would be trusted by cross signing val bobSession2 = mTestHelper.logIntoAccount(bobUserId, SessionTestParams(true)) - val bobSecondDeviceId = bobSession2.sessionParams.credentials.deviceId!! + val bobSecondDeviceId = bobSession2.sessionParams.deviceId!! // Check that bob first session sees the new login val data = mTestHelper.doSync> { diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt index bb6e020d89..57ab4aaf33 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt @@ -148,7 +148,7 @@ class KeyShareTests : InstrumentedTest { // Mark the device as trusted aliceSession.cryptoService().setDeviceVerification(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession.myUserId, - aliceSession2.sessionParams.credentials.deviceId ?: "") + aliceSession2.sessionParams.deviceId ?: "") // Re request aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root) @@ -253,12 +253,12 @@ class KeyShareTests : InstrumentedTest { }) val txId: String = "m.testVerif12" - aliceVerificationService2.beginKeyVerification(VerificationMethod.SAS, aliceSession1.myUserId, aliceSession1.sessionParams.credentials.deviceId + aliceVerificationService2.beginKeyVerification(VerificationMethod.SAS, aliceSession1.myUserId, aliceSession1.sessionParams.deviceId ?: "", txId) mTestHelper.waitWithLatch { latch -> mTestHelper.retryPeriodicallyWithLatch(latch) { - aliceSession1.cryptoService().getDeviceInfo(aliceSession1.myUserId, aliceSession2.sessionParams.credentials.deviceId ?: "")?.isVerified == true + aliceSession1.cryptoService().getDeviceInfo(aliceSession1.myUserId, aliceSession2.sessionParams.deviceId ?: "")?.isVerified == true } } diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt index 59ef24beec..2e698a929e 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt @@ -835,7 +835,7 @@ class KeysBackupTest : InstrumentedTest { assertTrue(signature.valid) assertNotNull(signature.device) assertEquals(cryptoTestData.firstSession.cryptoService().getMyDevice().deviceId, signature.deviceId) - assertEquals(signature.device!!.deviceId, cryptoTestData.firstSession.sessionParams.credentials.deviceId) + assertEquals(signature.device!!.deviceId, cryptoTestData.firstSession.sessionParams.deviceId) stateObserver.stopAndCheckStates(null) cryptoTestData.cleanUp(mTestHelper) @@ -997,7 +997,7 @@ class KeysBackupTest : InstrumentedTest { keysBackup.backupAllGroupSessions(null, it) } - val oldDeviceId = cryptoTestData.firstSession.sessionParams.credentials.deviceId!! + val oldDeviceId = cryptoTestData.firstSession.sessionParams.deviceId!! val oldKeyBackupVersion = keysBackup.currentBackupVersion val aliceUserId = cryptoTestData.firstSession.myUserId diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/SASTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/SASTest.kt index 1ac70d7f2b..460c411d43 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/SASTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/SASTest.kt @@ -468,14 +468,19 @@ class SASTest : InstrumentedTest { val aliceSASLatch = CountDownLatch(1) val aliceListener = object : VerificationService.Listener { + var matchOnce = true override fun transactionUpdated(tx: VerificationTransaction) { val uxState = (tx as OutgoingSasVerificationTransaction).uxState + Log.v("TEST", "== aliceState ${uxState.name}") when (uxState) { OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> { tx.userHasVerifiedShortCode() } OutgoingSasVerificationTransaction.UxState.VERIFIED -> { - aliceSASLatch.countDown() + if (matchOnce) { + matchOnce = false + aliceSASLatch.countDown() + } } else -> Unit } @@ -485,14 +490,23 @@ class SASTest : InstrumentedTest { val bobSASLatch = CountDownLatch(1) val bobListener = object : VerificationService.Listener { + var acceptOnce = true + var matchOnce = true override fun transactionUpdated(tx: VerificationTransaction) { val uxState = (tx as IncomingSasVerificationTransaction).uxState + Log.v("TEST", "== bobState ${uxState.name}") when (uxState) { IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> { - tx.performAccept() + if (acceptOnce) { + acceptOnce = false + tx.performAccept() + } } IncomingSasVerificationTransaction.UxState.SHOW_SAS -> { - tx.userHasVerifiedShortCode() + if (matchOnce) { + matchOnce = false + tx.userHasVerifiedShortCode() + } } IncomingSasVerificationTransaction.UxState.VERIFIED -> { bobSASLatch.countDown() @@ -579,7 +593,7 @@ class SASTest : InstrumentedTest { requestID!!, cryptoTestData.roomId, bobSession.myUserId, - bobSession.sessionParams.credentials.deviceId!!, + bobSession.sessionParams.deviceId!!, null) bobVerificationService.beginKeyVerificationInDMs( @@ -587,7 +601,7 @@ class SASTest : InstrumentedTest { requestID!!, cryptoTestData.roomId, aliceSession.myUserId, - aliceSession.sessionParams.credentials.deviceId!!, + aliceSession.sessionParams.deviceId!!, null) // we should reach SHOW SAS on both diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineBackToPreviousLastForwardTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineBackToPreviousLastForwardTest.kt new file mode 100644 index 0000000000..7c7de8170b --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineBackToPreviousLastForwardTest.kt @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.session.room.timeline + +import im.vector.matrix.android.InstrumentedTest +import im.vector.matrix.android.api.extensions.orFalse +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.timeline.Timeline +import im.vector.matrix.android.api.session.room.timeline.TimelineSettings +import im.vector.matrix.android.common.CommonTestHelper +import im.vector.matrix.android.common.CryptoTestHelper +import im.vector.matrix.android.common.checkSendOrder +import org.amshove.kluent.shouldBeFalse +import org.amshove.kluent.shouldBeTrue +import org.junit.Assert.assertTrue +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import timber.log.Timber +import java.util.concurrent.CountDownLatch + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class TimelineBackToPreviousLastForwardTest : InstrumentedTest { + + private val commonTestHelper = CommonTestHelper(context()) + private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + + /** + * This test ensure that if we have a chunk in the timeline which is due to a sync, and we click to permalink of an + * even contained in a previous lastForward chunk, we will be able to go back to the live + */ + @Test + fun backToPreviousLastForwardTest() { + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false) + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession!! + val aliceRoomId = cryptoTestData.roomId + + aliceSession.cryptoService().setWarnOnUnknownDevices(false) + bobSession.cryptoService().setWarnOnUnknownDevices(false) + + val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!! + val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!! + + val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(30)) + bobTimeline.start() + + var roomCreationEventId: String? = null + + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + roomCreationEventId = snapshot.lastOrNull()?.root?.eventId + // Ok, we have the 8 first messages of the initial sync (room creation and bob join event) + snapshot.size == 8 + } + + bobTimeline.addListener(eventsListener) + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + } + + // Bob stop to sync + bobSession.stopSync() + + val messageRoot = "First messages from Alice" + + // Alice sends 30 messages + commonTestHelper.sendTextMessage( + roomFromAlicePOV, + messageRoot, + 30) + + // Bob start to sync + bobSession.startSync(true) + + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // Ok, we have the 10 last messages from Alice. + snapshot.size == 10 + && snapshot.all { it.root.content.toModel()?.body?.startsWith(messageRoot).orFalse() } + } + + bobTimeline.addListener(eventsListener) + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue() + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + } + + // Bob navigate to the first event (room creation event), so inside the previous last forward chunk + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // The event is in db, so it is fetch and auto pagination occurs, half of the number of events we have for this chunk (?) + snapshot.size == 4 + } + + bobTimeline.addListener(eventsListener) + + // Restart the timeline to the first sent event, which is already in the database, so pagination should start automatically + assertTrue(roomFromBobPOV.getTimeLineEvent(roomCreationEventId!!) != null) + + bobTimeline.restartWithEventId(roomCreationEventId) + + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue() + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + } + + // Bob scroll to the future + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // Bob can see the first event of the room (so Back pagination has worked) + snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE + // 8 for room creation item, and 30 for the forward pagination + && snapshot.size == 38 + && snapshot.checkSendOrder(messageRoot, 30, 0) + } + + bobTimeline.addListener(eventsListener) + + bobTimeline.paginate(Timeline.Direction.FORWARDS, 50) + + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + } + bobTimeline.dispose() + + cryptoTestData.cleanUp(commonTestHelper) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineForwardPaginationTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineForwardPaginationTest.kt new file mode 100644 index 0000000000..adb5c81378 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineForwardPaginationTest.kt @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.session.room.timeline + +import im.vector.matrix.android.InstrumentedTest +import im.vector.matrix.android.api.extensions.orFalse +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.timeline.Timeline +import im.vector.matrix.android.api.session.room.timeline.TimelineSettings +import im.vector.matrix.android.common.CommonTestHelper +import im.vector.matrix.android.common.CryptoTestHelper +import im.vector.matrix.android.common.checkSendOrder +import org.amshove.kluent.shouldBeFalse +import org.amshove.kluent.shouldBeTrue +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import timber.log.Timber +import java.util.concurrent.CountDownLatch + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class TimelineForwardPaginationTest : InstrumentedTest { + + private val commonTestHelper = CommonTestHelper(context()) + private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + + /** + * This test ensure that if we click to permalink, we will be able to go back to the live + */ + @Test + fun forwardPaginationTest() { + val numberOfMessagesToSend = 90 + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false) + + val aliceSession = cryptoTestData.firstSession + val aliceRoomId = cryptoTestData.roomId + + aliceSession.cryptoService().setWarnOnUnknownDevices(false) + + val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!! + + // Alice sends X messages + val message = "Message from Alice" + val sentMessages = commonTestHelper.sendTextMessage( + roomFromAlicePOV, + message, + numberOfMessagesToSend) + + // Alice clear the cache + commonTestHelper.doSync { + aliceSession.clearCache(it) + } + + // And restarts the sync + aliceSession.startSync(true) + + val aliceTimeline = roomFromAlicePOV.createTimeline(null, TimelineSettings(30)) + aliceTimeline.start() + + // Alice sees the 10 last message of the room, and can only navigate BACKWARD + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Alice timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root.content}") + } + + // Ok, we have the 10 last messages of the initial sync + snapshot.size == 10 + && snapshot.all { it.root.content.toModel()?.body?.startsWith(message).orFalse() } + } + + // Open the timeline at last sent message + aliceTimeline.addListener(eventsListener) + commonTestHelper.await(lock) + aliceTimeline.removeAllListeners() + + aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue() + aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + } + + // Alice navigates to the first message of the room, which is not in its database. A GET /context is performed + // Then she can paginate BACKWARD and FORWARD + run { + val lock = CountDownLatch(1) + val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Alice timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root.content}") + } + + // The event is not in db, so it is fetch alone + snapshot.size == 1 + && snapshot.all { it.root.content.toModel()?.body?.startsWith("Message from Alice").orFalse() } + } + + aliceTimeline.addListener(aliceEventsListener) + + // Restart the timeline to the first sent event + aliceTimeline.restartWithEventId(sentMessages.last().eventId) + + commonTestHelper.await(lock) + aliceTimeline.removeAllListeners() + + aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue() + aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue() + } + + // Alice paginates BACKWARD and FORWARD of 50 events each + // Then she can only navigate FORWARD + run { + val lock = CountDownLatch(1) + val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Alice timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root.content}") + } + + // Alice can see the first event of the room (so Back pagination has worked) + snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE + // 6 for room creation item (backward pagination), 1 for the context, and 50 for the forward pagination + && snapshot.size == 6 + 1 + 50 + } + + aliceTimeline.addListener(aliceEventsListener) + + // Restart the timeline to the first sent event + // We ask to load event backward and forward + aliceTimeline.paginate(Timeline.Direction.BACKWARDS, 50) + aliceTimeline.paginate(Timeline.Direction.FORWARDS, 50) + + commonTestHelper.await(lock) + aliceTimeline.removeAllListeners() + + aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue() + aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + } + + // Alice paginates once again FORWARD for 50 events + // All the timeline is retrieved, she cannot paginate anymore in both direction + run { + val lock = CountDownLatch(1) + val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Alice timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root.content}") + } + // 6 for room creation item (backward pagination),and numberOfMessagesToSend (all the message of the room) + snapshot.size == 6 + numberOfMessagesToSend + && snapshot.checkSendOrder(message, numberOfMessagesToSend, 0) + } + + aliceTimeline.addListener(aliceEventsListener) + + // Ask for a forward pagination + aliceTimeline.paginate(Timeline.Direction.FORWARDS, 50) + + commonTestHelper.await(lock) + aliceTimeline.removeAllListeners() + + // The timeline is fully loaded + aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + } + + aliceTimeline.dispose() + + cryptoTestData.cleanUp(commonTestHelper) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelinePreviousLastForwardTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelinePreviousLastForwardTest.kt new file mode 100644 index 0000000000..3e673e4c08 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelinePreviousLastForwardTest.kt @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.session.room.timeline + +import im.vector.matrix.android.InstrumentedTest +import im.vector.matrix.android.api.extensions.orFalse +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.timeline.Timeline +import im.vector.matrix.android.api.session.room.timeline.TimelineSettings +import im.vector.matrix.android.common.CommonTestHelper +import im.vector.matrix.android.common.CryptoTestHelper +import im.vector.matrix.android.common.checkSendOrder +import org.amshove.kluent.shouldBeFalse +import org.amshove.kluent.shouldBeTrue +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import timber.log.Timber +import java.util.concurrent.CountDownLatch + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class TimelinePreviousLastForwardTest : InstrumentedTest { + + private val commonTestHelper = CommonTestHelper(context()) + private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + + /** + * This test ensure that if we have a chunk in the timeline which is due to a sync, and we click to permalink, we will be able to go back to the live + */ + @Test + fun previousLastForwardTest() { + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false) + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession!! + val aliceRoomId = cryptoTestData.roomId + + aliceSession.cryptoService().setWarnOnUnknownDevices(false) + bobSession.cryptoService().setWarnOnUnknownDevices(false) + + val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!! + val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!! + + val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(30)) + bobTimeline.start() + + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // Ok, we have the 8 first messages of the initial sync (room creation and bob invite and join events) + snapshot.size == 8 + } + + bobTimeline.addListener(eventsListener) + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + } + + // Bob stop to sync + bobSession.stopSync() + + val firstMessage = "First messages from Alice" + // Alice sends 30 messages + val firstMessageFromAliceId = commonTestHelper.sendTextMessage( + roomFromAlicePOV, + firstMessage, + 30) + .last() + .eventId + + // Bob start to sync + bobSession.startSync(true) + + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // Ok, we have the 10 last messages from Alice. This will be our future previous lastForward chunk + snapshot.size == 10 + && snapshot.all { it.root.content.toModel()?.body?.startsWith(firstMessage).orFalse() } + } + + bobTimeline.addListener(eventsListener) + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue() + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + } + + // Bob stop to sync + bobSession.stopSync() + + val secondMessage = "Second messages from Alice" + // Alice sends again 30 messages + commonTestHelper.sendTextMessage( + roomFromAlicePOV, + secondMessage, + 30) + + // Bob start to sync + bobSession.startSync(true) + + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // Ok, we have the 10 last messages from Alice. This will be our future previous lastForward chunk + snapshot.size == 10 + && snapshot.all { it.root.content.toModel()?.body?.startsWith(secondMessage).orFalse() } + } + + bobTimeline.addListener(eventsListener) + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue() + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + } + + // Bob navigate to the first message sent from Alice + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // The event is not in db, so it is fetch + snapshot.size == 1 + } + + bobTimeline.addListener(eventsListener) + + // Restart the timeline to the first sent event, and paginate in both direction + bobTimeline.restartWithEventId(firstMessageFromAliceId) + bobTimeline.paginate(Timeline.Direction.BACKWARDS, 50) + bobTimeline.paginate(Timeline.Direction.FORWARDS, 50) + + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue() + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue() + } + + // Paginate in both direction + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + snapshot.size == 8 + 1 + 35 + } + + bobTimeline.addListener(eventsListener) + + // Paginate in both direction + bobTimeline.paginate(Timeline.Direction.BACKWARDS, 50) + // Ensure the chunk in the middle is included in the next pagination + bobTimeline.paginate(Timeline.Direction.FORWARDS, 35) + + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue() + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + } + + // Bob scroll to the future, till the live + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // Bob can see the first event of the room (so Back pagination has worked) + snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE + // 8 for room creation item 60 message from Alice + && snapshot.size == 8 + 60 + && snapshot.checkSendOrder(secondMessage, 30, 0) + && snapshot.checkSendOrder(firstMessage, 30, 30) + } + + bobTimeline.addListener(eventsListener) + + bobTimeline.paginate(Timeline.Direction.FORWARDS, 50) + + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + } + + bobTimeline.dispose() + + cryptoTestData.cleanUp(commonTestHelper) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt index 4c6e3ea3bd..1a4c4aceee 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt @@ -23,7 +23,6 @@ import androidx.work.WorkManager import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.api.auth.AuthenticationService -import im.vector.matrix.android.api.crypto.MXCryptoConfig import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments @@ -32,20 +31,10 @@ import im.vector.matrix.android.internal.network.UserAgentHolder import im.vector.matrix.android.internal.util.BackgroundDetectionObserver import org.matrix.olm.OlmManager import java.io.InputStream -import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject -data class MatrixConfiguration( - val applicationFlavor: String = "Default-application-flavor", - val cryptoConfig: MXCryptoConfig = MXCryptoConfig() -) { - - interface Provider { - fun providesMatrixConfiguration(): MatrixConfiguration - } -} - /** * This is the main entry point to the matrix sdk. * To get the singleton instance, use getInstance static method. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixConfiguration.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixConfiguration.kt new file mode 100644 index 0000000000..d7c62f8bef --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixConfiguration.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api + +import im.vector.matrix.android.api.crypto.MXCryptoConfig +import java.net.Proxy + +data class MatrixConfiguration( + val applicationFlavor: String = "Default-application-flavor", + val cryptoConfig: MXCryptoConfig = MXCryptoConfig(), + /** + * Optional proxy to connect to the matrix servers + * You can create one using for instance Proxy(proxyType, InetSocketAddress(hostname, port) + */ + val proxy: Proxy? = null +) { + + /** + * Can be implemented by your Application class + */ + interface Provider { + fun providesMatrixConfiguration(): MatrixConfiguration + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/AuthenticationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/AuthenticationService.kt index 5150420de2..effeae596a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/AuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/AuthenticationService.kt @@ -20,7 +20,6 @@ import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.LoginFlowResult -import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.login.LoginWizard import im.vector.matrix.android.api.auth.registration.RegistrationWizard import im.vector.matrix.android.api.auth.wellknown.WellknownResult @@ -37,6 +36,11 @@ interface AuthenticationService { */ fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback): Cancelable + /** + * Request the supported login flows for the corresponding sessionId. + */ + fun getLoginFlowOfSession(sessionId: String, callback: MatrixCallback): Cancelable + /** * Return a LoginWizard, to login to the homeserver. The login flow has to be retrieved first. */ @@ -74,15 +78,6 @@ interface AuthenticationService { */ fun getLastAuthenticatedSession(): Session? - /** - * Get an authenticated session. You should at least call authenticate one time before. - * If you logout, this session will no longer be valid. - * - * @param sessionParams the sessionParams to open with. - * @return the associated session if any, or null - */ - fun getSession(sessionParams: SessionParams): Session? - /** * Create a session after a SSO successful login */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/SessionParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/SessionParams.kt index 2d65cac43d..1cbba50af7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/SessionParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/SessionParams.kt @@ -21,7 +21,48 @@ package im.vector.matrix.android.api.auth.data * You don't have to manually instantiate it. */ data class SessionParams( + /** + * Please consider using shortcuts instead + */ val credentials: Credentials, + + /** + * Please consider using shortcuts instead + */ val homeServerConnectionConfig: HomeServerConnectionConfig, + + /** + * Set to false if the current token is not valid anymore. Application should not have to use this info. + */ val isTokenValid: Boolean -) +) { + /* + * Shortcuts. Usually the application should only need to use these shortcuts + */ + + /** + * The userId of the session (Ex: "@user:domain.org") + */ + val userId = credentials.userId + + /** + * The deviceId of the session (Ex: "ABCDEFGH") + */ + val deviceId = credentials.deviceId + + /** + * The current homeserver Url. It can be different that the homeserver url entered + * during login phase, because a redirection may have occurred + */ + val homeServerUrl = homeServerConnectionConfig.homeServerUri.toString() + + /** + * The current homeserver host + */ + val homeServerHost = homeServerConnectionConfig.homeServerUri.host + + /** + * The default identity server url if any, returned by the homeserver during login phase + */ + val defaultIdentityServerUrl = homeServerConnectionConfig.identityServerUri?.toString() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/crypto/MXCryptoConfig.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/crypto/MXCryptoConfig.kt index a8d576bae9..fa8178334c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/crypto/MXCryptoConfig.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/crypto/MXCryptoConfig.kt @@ -19,11 +19,11 @@ package im.vector.matrix.android.api.crypto /** * Class to define the parameters used to customize or configure the end-to-end crypto. */ -data class MXCryptoConfig( +data class MXCryptoConfig constructor( // Tell whether the encryption of the event content is enabled for the invited members. // SDK clients can disable this by settings it to false. // Note that the encryption for the invited members will be blocked if the history visibility is "joined". - var enableEncryptionForInvitedMembers: Boolean = true, + val enableEncryptionForInvitedMembers: Boolean = true, /** * If set to true, the SDK will automatically ignore room key request (gossiping) @@ -31,6 +31,5 @@ data class MXCryptoConfig( * If set to false, the request will be forwarded to the application layer; in this * case the application can decide to prompt the user. */ - var discardRoomKeyRequestsFromUntrustedDevices : Boolean = true - + val discardRoomKeyRequestsFromUntrustedDevices: Boolean = true ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt index d7a6954fd5..7c9ace5d82 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt @@ -39,7 +39,10 @@ data class MatrixError( // For M_LIMIT_EXCEEDED @Json(name = "retry_after_ms") val retryAfterMillis: Long? = null, // For M_UNKNOWN_TOKEN - @Json(name = "soft_logout") val isSoftLogout: Boolean = false + @Json(name = "soft_logout") val isSoftLogout: Boolean = false, + // For M_INVALID_PEPPER + // {"error": "pepper does not match 'erZvr'", "lookup_pepper": "pQgMS", "algorithm": "sha256", "errcode": "M_INVALID_PEPPER"} + @Json(name = "lookup_pepper") val newLookupPepper: String? = null ) { companion object { @@ -129,6 +132,11 @@ data class MatrixError( /** (Not documented yet) */ const val M_WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION" + const val M_TERMS_NOT_SIGNED = "M_TERMS_NOT_SIGNED" + + // For identity service + const val M_INVALID_PEPPER = "M_INVALID_PEPPER" + // Possible value for "limit_type" const val LIMIT_TYPE_MAU = "monthly_active_user" } 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 1afeed922f..c86ca25faf 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 @@ -30,6 +30,7 @@ import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.group.GroupService import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService +import im.vector.matrix.android.api.session.identity.IdentityService import im.vector.matrix.android.api.session.profile.ProfileService import im.vector.matrix.android.api.session.pushers.PushersService import im.vector.matrix.android.api.session.room.RoomDirectoryService @@ -39,6 +40,7 @@ import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageSer import im.vector.matrix.android.api.session.signout.SignOutService import im.vector.matrix.android.api.session.sync.FilterService import im.vector.matrix.android.api.session.sync.SyncState +import im.vector.matrix.android.api.session.terms.TermsService import im.vector.matrix.android.api.session.user.UserService /** @@ -54,6 +56,7 @@ interface Session : SignOutService, FilterService, FileService, + TermsService, ProfileService, PushRuleService, PushersService, @@ -77,7 +80,7 @@ interface Session : * Useful shortcut to get access to the userId */ val myUserId: String - get() = sessionParams.credentials.userId + get() = sessionParams.userId /** * The sessionId @@ -145,6 +148,11 @@ interface Session : */ fun cryptoService(): CryptoService + /** + * Returns the identity service associated with the session + */ + fun identityService(): IdentityService + /** * Add a listener to the session. * @param listener the listener to add. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt index a60d0fd9ac..d3780ebe60 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt @@ -220,3 +220,11 @@ fun Event.isImageMessage(): Boolean { else -> false } } + +fun Event.isVideoMessage(): Boolean { + return getClearType() == EventType.MESSAGE + && when (getClearContent()?.toModel()?.msgType) { + MessageType.MSGTYPE_VIDEO -> true + else -> false + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilities.kt index c8526985e1..1c2b8de83b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilities.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilities.kt @@ -24,7 +24,15 @@ data class HomeServerCapabilities( /** * Max size of file which can be uploaded to the homeserver in bytes. [MAX_UPLOAD_FILE_SIZE_UNKNOWN] if unknown or not retrieved yet */ - val maxUploadFileSize: Long = MAX_UPLOAD_FILE_SIZE_UNKNOWN + val maxUploadFileSize: Long = MAX_UPLOAD_FILE_SIZE_UNKNOWN, + /** + * Last version identity server and binding supported + */ + val lastVersionIdentityServerSupported: Boolean = false, + /** + * Default identity server url, provided in Wellknown + */ + val defaultIdentityServerUrl: String? = null ) { companion object { const val MAX_UPLOAD_FILE_SIZE_UNKNOWN = -1L diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/FoundThreePid.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/FoundThreePid.kt new file mode 100644 index 0000000000..5817699636 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/FoundThreePid.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.identity + +data class FoundThreePid( + val threePid: ThreePid, + val matrixId: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityService.kt new file mode 100644 index 0000000000..2f2821d7a8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityService.kt @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.identity + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.util.Cancelable + +/** + * Provides access to the identity server configuration and services identity server can provide + */ +interface IdentityService { + /** + * Return the default identity server of the user, which may have been provided at login time by the homeserver, + * or by the Well-known setup of the homeserver + * It may be different from the current configured identity server + */ + fun getDefaultIdentityServer(): String? + + /** + * Return the current identity server URL used by this account. Returns null if no identity server is configured. + */ + fun getCurrentIdentityServerUrl(): String? + + /** + * Check if the identity server is valid + * See https://matrix.org/docs/spec/identity_service/latest#status-check + * RiotX SDK only supports identity server API v2 + */ + fun isValidIdentityServer(url: String, callback: MatrixCallback): Cancelable + + /** + * Update the identity server url. + * If successful, any previous identity server will be disconnected. + * In case of error, any previous identity server will remain configured. + * @param url the new url. + * @param callback will notify the user if change is successful. The String will be the final url of the identity server. + * The SDK can prepend "https://" for instance. + */ + fun setNewIdentityServer(url: String, callback: MatrixCallback): Cancelable + + /** + * Disconnect (logout) from the current identity server + */ + fun disconnect(callback: MatrixCallback): Cancelable + + /** + * This will ask the identity server to send an email or an SMS to let the user confirm he owns the ThreePid + */ + fun startBindThreePid(threePid: ThreePid, callback: MatrixCallback): Cancelable + + /** + * This will cancel a pending binding of threePid. + */ + fun cancelBindThreePid(threePid: ThreePid, callback: MatrixCallback): Cancelable + + /** + * This will ask the identity server to send an new email or a new SMS to let the user confirm he owns the ThreePid + */ + fun sendAgainValidationCode(threePid: ThreePid, callback: MatrixCallback): Cancelable + + /** + * Submit the code that the identity server has sent to the user (in email or SMS) + * Once successful, you will have to call [finalizeBindThreePid] + * @param code the code sent to the user + */ + fun submitValidationToken(threePid: ThreePid, code: String, callback: MatrixCallback): Cancelable + + /** + * This will perform the actual association of ThreePid and Matrix account + */ + fun finalizeBindThreePid(threePid: ThreePid, callback: MatrixCallback): Cancelable + + /** + * Unbind a threePid + * The request will actually be done on the homeserver + */ + fun unbindThreePid(threePid: ThreePid, callback: MatrixCallback): Cancelable + + /** + * Search MatrixId of users providing email and phone numbers + */ + fun lookUp(threePids: List, callback: MatrixCallback>): Cancelable + + /** + * Get the status of the current user's threePid + * A lookup will be performed, but also pending binding state will be restored + * + * @param threePids the list of threePid the user owns (retrieved form the homeserver) + * @param callback onSuccess will be called with a map of ThreePid -> SharedState + */ + fun getShareStatus(threePids: List, callback: MatrixCallback>): Cancelable + + fun addListener(listener: IdentityServiceListener) + fun removeListener(listener: IdentityServiceListener) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityServiceError.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityServiceError.kt new file mode 100644 index 0000000000..83fb949946 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityServiceError.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.identity + +sealed class IdentityServiceError : Throwable() { + object OutdatedIdentityServer : IdentityServiceError() + object OutdatedHomeServer : IdentityServiceError() + object NoIdentityServerConfigured : IdentityServiceError() + object TermsNotSignedException : IdentityServiceError() + object BulkLookupSha256NotSupported : IdentityServiceError() + object BindingError : IdentityServiceError() + object NoCurrentBindingError : IdentityServiceError() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityServiceListener.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityServiceListener.kt new file mode 100644 index 0000000000..13f622fe77 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityServiceListener.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.identity + +interface IdentityServiceListener { + fun onIdentityServerChange() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/SharedState.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/SharedState.kt new file mode 100644 index 0000000000..88cac776d6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/SharedState.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.identity + +enum class SharedState { + SHARED, + NOT_SHARED, + BINDING_IN_PROGRESS +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/ThreePid.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/ThreePid.kt new file mode 100644 index 0000000000..2a453ca1a6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/ThreePid.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.identity + +import com.google.i18n.phonenumbers.NumberParseException +import com.google.i18n.phonenumbers.PhoneNumberUtil +import im.vector.matrix.android.internal.session.profile.ThirdPartyIdentifier + +sealed class ThreePid(open val value: String) { + data class Email(val email: String) : ThreePid(email) + data class Msisdn(val msisdn: String) : ThreePid(msisdn) +} + +internal fun ThreePid.toMedium(): String { + return when (this) { + is ThreePid.Email -> ThirdPartyIdentifier.MEDIUM_EMAIL + is ThreePid.Msisdn -> ThirdPartyIdentifier.MEDIUM_MSISDN + } +} + +@Throws(NumberParseException::class) +internal fun ThreePid.Msisdn.getCountryCode(): String { + return with(PhoneNumberUtil.getInstance()) { + getRegionCodeForCountryCode(parse("+$msisdn", null).countryCode) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/profile/ProfileService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/profile/ProfileService.kt index c1dc9a8afa..92f9359e34 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/profile/ProfileService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/profile/ProfileService.kt @@ -17,7 +17,9 @@ package im.vector.matrix.android.api.session.profile +import androidx.lifecycle.LiveData import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.identity.ThreePid import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.Optional @@ -53,4 +55,15 @@ interface ProfileService { * */ fun getProfile(userId: String, matrixCallback: MatrixCallback): Cancelable + + /** + * Get the current user 3Pids + */ + fun getThreePids(): List + + /** + * Get the current user 3Pids Live + * @param refreshData set to true to fetch data from the homeserver + */ + fun getThreePidsLive(refreshData: Boolean): LiveData> } 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 0c3316e802..2fd7d84f04 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 @@ -29,6 +29,7 @@ import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.state.StateService import im.vector.matrix.android.api.session.room.timeline.TimelineService import im.vector.matrix.android.api.session.room.typing.TypingService +import im.vector.matrix.android.api.session.room.uploads.UploadsService import im.vector.matrix.android.api.util.Optional /** @@ -42,6 +43,7 @@ interface Room : TypingService, MembershipService, StateService, + UploadsService, ReportingService, RelationService, RoomCryptoService, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContentWithFormattedBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContentWithFormattedBody.kt new file mode 100644 index 0000000000..b51e3eb841 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContentWithFormattedBody.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.room.model.message + +interface MessageContentWithFormattedBody : MessageContent { + /** + * The format used in the formatted_body. Currently only "org.matrix.custom.html" is supported. + */ + val format: String? + + /** + * The formatted version of the body. This is required if format is specified. + */ + val formattedBody: String? + + /** + * Get the formattedBody, only if not blank and if the format is equal to "org.matrix.custom.html" + */ + val matrixFormattedBody: String? + get() = formattedBody?.takeIf { it.isNotBlank() && format == MessageFormat.FORMAT_MATRIX_HTML } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEmoteContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEmoteContent.kt index e7106a9755..7b63959f78 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEmoteContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEmoteContent.kt @@ -34,15 +34,15 @@ data class MessageEmoteContent( @Json(name = "body") override val body: String, /** - * The format used in the formatted_body. Currently only org.matrix.custom.html is supported. + * The format used in the formatted_body. Currently only "org.matrix.custom.html" is supported. */ - @Json(name = "format") val format: String? = null, + @Json(name = "format") override val format: String? = null, /** * The formatted version of the body. This is required if format is specified. */ - @Json(name = "formatted_body") val formattedBody: String? = null, + @Json(name = "formatted_body") override val formattedBody: String? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.new_content") override val newContent: Content? = null -) : MessageContent +) : MessageContentWithFormattedBody diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageNoticeContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageNoticeContent.kt index e08e07e9da..41e63bb457 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageNoticeContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageNoticeContent.kt @@ -34,15 +34,15 @@ data class MessageNoticeContent( @Json(name = "body") override val body: String, /** - * The format used in the formatted_body. Currently only org.matrix.custom.html is supported. + * The format used in the formatted_body. Currently only "org.matrix.custom.html" is supported. */ - @Json(name = "format") val format: String? = null, + @Json(name = "format") override val format: String? = null, /** * The formatted version of the body. This is required if format is specified. */ - @Json(name = "formatted_body") val formattedBody: String? = null, + @Json(name = "formatted_body") override val formattedBody: String? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.new_content") override val newContent: Content? = null -) : MessageContent +) : MessageContentWithFormattedBody diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageTextContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageTextContent.kt index cc5bb1f774..d6c54e3ff5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageTextContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageTextContent.kt @@ -34,15 +34,15 @@ data class MessageTextContent( @Json(name = "body") override val body: String, /** - * The format used in the formatted_body. Currently only org.matrix.custom.html is supported. + * The format used in the formatted_body. Currently only "org.matrix.custom.html" is supported. */ - @Json(name = "format") val format: String? = null, + @Json(name = "format") override val format: String? = null, /** * The formatted version of the body. This is required if format is specified. */ - @Json(name = "formatted_body") val formattedBody: String? = null, + @Json(name = "formatted_body") override val formattedBody: String? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.new_content") override val newContent: Content? = null -) : MessageContent +) : MessageContentWithFormattedBody diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/sender/SenderInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/sender/SenderInfo.kt new file mode 100644 index 0000000000..1a0908a6d3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/sender/SenderInfo.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.room.sender + +data class SenderInfo( + val userId: String, + /** + * Consider using [disambiguatedDisplayName] + */ + val displayName: String?, + val isUniqueDisplayName: Boolean, + val avatarUrl: String? +) { + val disambiguatedDisplayName: String + get() = when { + displayName.isNullOrBlank() -> userId + isUniqueDisplayName -> displayName + else -> "$displayName ($userId)" + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt index d7d6682046..19ff65dbe2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt @@ -58,7 +58,7 @@ interface Timeline { /** * Check if the timeline can be enriched by paginating. - * @param the direction to check in + * @param direction the direction to check in * @return true if timeline can be enriched */ fun hasMoreToLoad(direction: Direction): Boolean diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt index 0c8a04db36..273ea2366a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.api.session.room.timeline +import im.vector.matrix.android.BuildConfig 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.RelationType @@ -25,6 +26,7 @@ import im.vector.matrix.android.api.session.room.model.ReadReceipt import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageStickerContent import im.vector.matrix.android.api.session.room.model.message.isReply +import im.vector.matrix.android.api.session.room.sender.SenderInfo import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent @@ -38,13 +40,17 @@ data class TimelineEvent( val localId: Long, val eventId: String, val displayIndex: Int, - val senderName: String?, - val isUniqueDisplayName: Boolean, - val senderAvatar: String?, + val senderInfo: SenderInfo, val annotations: EventAnnotationsSummary? = null, val readReceipts: List = emptyList() ) { + init { + if (BuildConfig.DEBUG) { + assert(eventId == root.eventId) + } + } + val metadata = HashMap() /** @@ -62,14 +68,6 @@ data class TimelineEvent( } } - fun getDisambiguatedDisplayName(): String { - return when { - senderName.isNullOrBlank() -> root.senderId ?: "" - isUniqueDisplayName -> senderName - else -> "$senderName (${root.senderId})" - } - } - /** * Get the metadata associated with a key. * @param key the key to get the metadata diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt index 992cad41ca..154074b722 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt @@ -28,6 +28,10 @@ data class TimelineSettings( * A flag to filter edit events */ val filterEdits: Boolean = false, + /** + * A flag to filter redacted events + */ + val filterRedacted: Boolean = false, /** * A flag to filter by types. It should be used with [allowedTypes] field */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/GetUploadsResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/GetUploadsResult.kt new file mode 100644 index 0000000000..4c75d909aa --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/GetUploadsResult.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.room.uploads + +data class GetUploadsResult( + // List of fetched Events, most recent first + val uploadEvents: List, + // token to get more events + val nextToken: String, + // True if there are more event to load + val hasMore: Boolean +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/UploadEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/UploadEvent.kt new file mode 100644 index 0000000000..5df2b9c9e0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/UploadEvent.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.room.uploads + +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent +import im.vector.matrix.android.api.session.room.sender.SenderInfo + +/** + * Wrapper around on Event. + * Similar to [im.vector.matrix.android.api.session.room.timeline.TimelineEvent], contains an Event with extra useful data + */ +data class UploadEvent( + val root: Event, + val eventId: String, + val contentWithAttachmentContent: MessageWithAttachmentContent, + val senderInfo: SenderInfo +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/UploadsService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/UploadsService.kt new file mode 100644 index 0000000000..54a87cdcd9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/UploadsService.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.room.uploads + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.util.Cancelable + +/** + * This interface defines methods to get event with uploads (= attachments) sent to a room. It's implemented at the room level. + */ +interface UploadsService { + + /** + * Get a list of events containing URL sent to a room, from most recent to oldest one + * @param numberOfEvents the expected number of events to retrieve. The result can contain less events. + * @param since token to get next page, or null to get the first page + */ + fun getUploads(numberOfEvents: Int, + since: String?, + callback: MatrixCallback): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/terms/GetTermsResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/terms/GetTermsResponse.kt new file mode 100644 index 0000000000..29c6a7a921 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/terms/GetTermsResponse.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.terms + +import im.vector.matrix.android.internal.session.terms.TermsResponse + +data class GetTermsResponse( + val serverResponse: TermsResponse, + val alreadyAcceptedTermUrls: Set +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/terms/TermsService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/terms/TermsService.kt new file mode 100644 index 0000000000..36e6a411e3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/terms/TermsService.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.terms + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.util.Cancelable + +interface TermsService { + enum class ServiceType { + IntegrationManager, + IdentityService + } + + fun getTerms(serviceType: ServiceType, + baseUrl: String, + callback: MatrixCallback): Cancelable + + fun agreeToTerms(serviceType: ServiceType, + baseUrl: String, + agreedUrls: List, + token: String?, + callback: MatrixCallback): Cancelable +} 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 d5aa897c7d..f30494711b 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 @@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.room.model.RoomMemberSummary import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom +import im.vector.matrix.android.api.session.room.sender.SenderInfo import im.vector.matrix.android.api.session.user.model.User import java.util.Locale @@ -154,3 +155,5 @@ fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlia fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: getPrimaryAlias() ?: "", avatarUrl) fun RoomMemberSummary.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) + +fun SenderInfo.toMatrixItem() = MatrixItem.UserItem(userId, disambiguatedDisplayName, avatarUrl) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt index 232cb3f541..68f404cb71 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt @@ -25,16 +25,15 @@ import im.vector.matrix.android.internal.auth.db.AuthRealmMigration import im.vector.matrix.android.internal.auth.db.AuthRealmModule import im.vector.matrix.android.internal.auth.db.RealmPendingSessionStore import im.vector.matrix.android.internal.auth.db.RealmSessionParamsStore -import im.vector.matrix.android.internal.auth.wellknown.DefaultDirectLoginTask -import im.vector.matrix.android.internal.auth.wellknown.DefaultGetWellknownTask -import im.vector.matrix.android.internal.auth.wellknown.DirectLoginTask -import im.vector.matrix.android.internal.auth.wellknown.GetWellknownTask +import im.vector.matrix.android.internal.auth.login.DefaultDirectLoginTask +import im.vector.matrix.android.internal.auth.login.DirectLoginTask import im.vector.matrix.android.internal.database.RealmKeysUtils import im.vector.matrix.android.internal.di.AuthDatabase +import im.vector.matrix.android.internal.wellknown.WellknownModule import io.realm.RealmConfiguration import java.io.File -@Module +@Module(includes = [WellknownModule::class]) internal abstract class AuthModule { @Module @@ -74,9 +73,6 @@ internal abstract class AuthModule { @Binds abstract fun bindSessionCreator(creator: DefaultSessionCreator): SessionCreator - @Binds - abstract fun bindGetWellknownTask(task: DefaultGetWellknownTask): GetWellknownTask - @Binds abstract fun bindDirectLoginTask(task: DefaultDirectLoginTask): DirectLoginTask } 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 997cf70e5a..b543fa7507 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 @@ -23,7 +23,6 @@ import im.vector.matrix.android.api.auth.AuthenticationService import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.LoginFlowResult -import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.data.Versions import im.vector.matrix.android.api.auth.data.isLoginAndRegistrationSupportedBySdk import im.vector.matrix.android.api.auth.data.isSupportedBySdk @@ -33,14 +32,14 @@ import im.vector.matrix.android.api.auth.wellknown.WellknownResult import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.api.util.NoOpCancellable import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.auth.data.LoginFlowResponse import im.vector.matrix.android.internal.auth.data.RiotConfig import im.vector.matrix.android.internal.auth.db.PendingSessionData import im.vector.matrix.android.internal.auth.login.DefaultLoginWizard +import im.vector.matrix.android.internal.auth.login.DirectLoginTask import im.vector.matrix.android.internal.auth.registration.DefaultRegistrationWizard -import im.vector.matrix.android.internal.auth.wellknown.DirectLoginTask -import im.vector.matrix.android.internal.auth.wellknown.GetWellknownTask import im.vector.matrix.android.internal.di.Unauthenticated import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.network.executeRequest @@ -50,7 +49,7 @@ import im.vector.matrix.android.internal.task.launchToCallback import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.exhaustive import im.vector.matrix.android.internal.util.toCancelable -import kotlinx.coroutines.GlobalScope +import im.vector.matrix.android.internal.wellknown.GetWellknownTask import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okhttp3.OkHttpClient @@ -87,14 +86,21 @@ internal class DefaultAuthenticationService @Inject constructor( } } - override fun getSession(sessionParams: SessionParams): Session? { - return sessionManager.getOrCreateSession(sessionParams) + override fun getLoginFlowOfSession(sessionId: String, callback: MatrixCallback): Cancelable { + val homeServerConnectionConfig = sessionParamsStore.get(sessionId)?.homeServerConnectionConfig + + return if (homeServerConnectionConfig == null) { + callback.onFailure(IllegalStateException("Session not found")) + NoOpCancellable + } else { + getLoginFlow(homeServerConnectionConfig, callback) + } } override fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback): Cancelable { pendingSessionData = null - return GlobalScope.launch(coroutineDispatchers.main) { + return taskExecutor.executorScope.launch(coroutineDispatchers.main) { pendingSessionStore.delete() val result = runCatching { @@ -246,7 +252,8 @@ internal class DefaultAuthenticationService @Inject constructor( retrofitFactory, coroutineDispatchers, sessionCreator, - pendingSessionStore + pendingSessionStore, + taskExecutor.executorScope ).also { currentRegistrationWizard = it } @@ -266,7 +273,8 @@ internal class DefaultAuthenticationService @Inject constructor( retrofitFactory, coroutineDispatchers, sessionCreator, - pendingSessionStore + pendingSessionStore, + taskExecutor.executorScope ).also { currentLoginWizard = it } @@ -283,7 +291,7 @@ internal class DefaultAuthenticationService @Inject constructor( pendingSessionData = pendingSessionData?.homeServerConnectionConfig ?.let { PendingSessionData(it) } .also { - GlobalScope.launch(coroutineDispatchers.main) { + taskExecutor.executorScope.launch(coroutineDispatchers.main) { if (it == null) { // Should not happen pendingSessionStore.delete() @@ -300,7 +308,7 @@ internal class DefaultAuthenticationService @Inject constructor( pendingSessionData = null - GlobalScope.launch(coroutineDispatchers.main) { + taskExecutor.executorScope.launch(coroutineDispatchers.main) { pendingSessionStore.delete() } } @@ -308,7 +316,7 @@ internal class DefaultAuthenticationService @Inject constructor( override fun createSessionFromSso(homeServerConnectionConfig: HomeServerConnectionConfig, credentials: Credentials, callback: MatrixCallback): Cancelable { - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { createSessionFromSso(credentials, homeServerConnectionConfig) } } 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 ebd50a6924..a4db0e84f7 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 @@ -51,7 +51,7 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) { } return SessionParamsEntity( sessionParams.credentials.sessionId(), - sessionParams.credentials.userId, + sessionParams.userId, credentialsJson, homeServerConnectionConfigJson, sessionParams.isTokenValid) 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 4d98ddcf08..132073b340 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 @@ -38,7 +38,7 @@ import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.launchToCallback import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers -import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.withContext import okhttp3.OkHttpClient @@ -47,7 +47,8 @@ internal class DefaultLoginWizard( retrofitFactory: RetrofitFactory, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val sessionCreator: SessionCreator, - private val pendingSessionStore: PendingSessionStore + private val pendingSessionStore: PendingSessionStore, + private val coroutineScope: CoroutineScope ) : LoginWizard { private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here") @@ -59,7 +60,7 @@ internal class DefaultLoginWizard( password: String, deviceName: String, callback: MatrixCallback): Cancelable { - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { loginInternal(login, password, deviceName) } } @@ -80,7 +81,7 @@ internal class DefaultLoginWizard( } override fun resetPassword(email: String, newPassword: String, callback: MatrixCallback): Cancelable { - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { resetPasswordInternal(email, newPassword) } } @@ -108,7 +109,7 @@ internal class DefaultLoginWizard( callback.onFailure(IllegalStateException("developer error, no reset password in progress")) return NoOpCancellable } - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { resetPasswordMailConfirmedInternal(safeResetPasswordData) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/DirectLoginTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DirectLoginTask.kt similarity index 97% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/DirectLoginTask.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DirectLoginTask.kt index 01a3ab192d..90eddf2e14 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/DirectLoginTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DirectLoginTask.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.auth.wellknown +package im.vector.matrix.android.internal.auth.login import dagger.Lazy import im.vector.matrix.android.api.auth.data.Credentials diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt index 29970b6c0c..5a39de72ca 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt @@ -33,7 +33,7 @@ import im.vector.matrix.android.internal.auth.db.PendingSessionData import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.task.launchToCallback import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers -import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import okhttp3.OkHttpClient @@ -45,7 +45,8 @@ internal class DefaultRegistrationWizard( private val retrofitFactory: RetrofitFactory, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val sessionCreator: SessionCreator, - private val pendingSessionStore: PendingSessionStore + private val pendingSessionStore: PendingSessionStore, + private val coroutineScope: CoroutineScope ) : RegistrationWizard { private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here") @@ -72,7 +73,7 @@ internal class DefaultRegistrationWizard( override fun getRegistrationFlow(callback: MatrixCallback): Cancelable { val params = RegistrationParams() - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { performRegistrationRequest(params) } } @@ -86,7 +87,7 @@ internal class DefaultRegistrationWizard( password = password, initialDeviceDisplayName = initialDeviceDisplayName ) - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { performRegistrationRequest(params) .also { pendingSessionData = pendingSessionData.copy(isRegistrationStarted = true) @@ -101,7 +102,7 @@ internal class DefaultRegistrationWizard( return NoOpCancellable } val params = RegistrationParams(auth = AuthParams.createForCaptcha(safeSession, response)) - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { performRegistrationRequest(params) } } @@ -112,13 +113,13 @@ internal class DefaultRegistrationWizard( return NoOpCancellable } val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.TERMS, session = safeSession)) - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { performRegistrationRequest(params) } } override fun addThreePid(threePid: RegisterThreePid, callback: MatrixCallback): Cancelable { - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { pendingSessionData = pendingSessionData.copy(currentThreePidData = null) .also { pendingSessionStore.savePendingSessionData(it) } @@ -131,7 +132,7 @@ internal class DefaultRegistrationWizard( callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) return NoOpCancellable } - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { sendThreePid(safeCurrentThreePid) } } @@ -177,13 +178,13 @@ internal class DefaultRegistrationWizard( callback.onFailure(IllegalStateException("developer error, no pending three pid")) return NoOpCancellable } - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { performRegistrationRequest(safeParam, delayMillis) } } override fun handleValidateThreePid(code: String, callback: MatrixCallback): Cancelable { - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { validateThreePid(code) } } @@ -199,7 +200,7 @@ internal class DefaultRegistrationWizard( code = code ) val validationResponse = validateCodeTask.execute(ValidateCodeTask.Params(url, validationBody)) - if (validationResponse.success == true) { + if (validationResponse.isSuccess()) { // The entered code is correct // Same than validate email return performRegistrationRequest(registrationParams, 3_000) @@ -214,7 +215,7 @@ internal class DefaultRegistrationWizard( callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) return NoOpCancellable } - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.DUMMY, session = safeSession)) performRegistrationRequest(params) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt index 2cd52f702e..5bdc9579e0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.matrix.androidsdk.rest.model.login +package im.vector.matrix.android.internal.auth.registration import android.os.Parcelable import kotlinx.android.parcel.Parcelize diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt index 7512454052..dd5a553193 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt @@ -76,7 +76,7 @@ fun RegistrationFlowResponse.toFlowResult(): FlowResult { this.flows?.forEach { it.stages?.mapTo(allFlowTypes) { type -> type } } allFlowTypes.forEach { type -> - val isMandatory = flows?.all { type in it.stages ?: emptyList() } == true + val isMandatory = flows?.all { type in it.stages.orEmpty() } == true val stage = when (type) { LoginFlowTypes.RECAPTCHA -> Stage.ReCaptcha(isMandatory, ((params?.get(type) as? Map<*, *>)?.get("public_key") as? String) @@ -88,7 +88,7 @@ fun RegistrationFlowResponse.toFlowResult(): FlowResult { else -> Stage.Other(isMandatory, type, (params?.get(type) as? Map<*, *>)) } - if (type in completedStages ?: emptyList()) { + if (type in completedStages.orEmpty()) { completedStage.add(stage) } else { missingStage.add(stage) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/SuccessResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/SuccessResult.kt index 8bfa3dda1d..1d19d1a5e5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/SuccessResult.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/SuccessResult.kt @@ -18,9 +18,12 @@ package im.vector.matrix.android.internal.auth.registration import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.extensions.orFalse @JsonClass(generateAdapter = true) data class SuccessResult( @Json(name = "success") val success: Boolean? -) +) { + fun isSuccess() = success.orFalse() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt index d529cf4ae5..7badb26d8a 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt @@ -262,7 +262,7 @@ internal class DefaultCryptoService @Inject constructor( override fun onSuccess(data: DevicesListResponse) { // Save in local DB - cryptoStore.saveMyDevicesInfo(data.devices ?: emptyList()) + cryptoStore.saveMyDevicesInfo(data.devices.orEmpty()) callback.onSuccess(data) } } @@ -446,7 +446,7 @@ internal class DefaultCryptoService @Inject constructor( } override fun getCryptoDeviceInfo(userId: String): List { - return cryptoStore.getUserDeviceList(userId) ?: emptyList() + return cryptoStore.getUserDeviceList(userId).orEmpty() } override fun getLiveCryptoDeviceInfo(): LiveData> { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OneTimeKeysUploader.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OneTimeKeysUploader.kt index a0483335e5..89965e7da9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OneTimeKeysUploader.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OneTimeKeysUploader.kt @@ -137,7 +137,7 @@ internal class OneTimeKeysUploader @Inject constructor( private suspend fun uploadOneTimeKeys(oneTimeKeys: Map>?): KeysUploadResponse { val oneTimeJson = mutableMapOf() - val curve25519Map = oneTimeKeys?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY) ?: emptyMap() + val curve25519Map = oneTimeKeys?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY).orEmpty() curve25519Map.forEach { (key_id, value) -> val k = mutableMapOf() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt index 5766ee9980..1d452f4515 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt @@ -34,7 +34,7 @@ internal class EnsureOlmSessionsForUsersAction @Inject constructor(private val o suspend fun handle(users: List): MXUsersDevicesMap { Timber.v("## ensureOlmSessionsForUsers() : ensureOlmSessionsForUsers $users") val devicesByUser = users.associateWith { userId -> - val devices = cryptoStore.getUserDevices(userId)?.values ?: emptyList() + val devices = cryptoStore.getUserDevices(userId)?.values.orEmpty() devices.filter { // Don't bother setting up session to ourself diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt index 59ffa5f874..4c66f63d6a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt @@ -103,7 +103,7 @@ internal class MXMegolmDecryption(private val userId: String, senderCurve25519Key = olmDecryptionResult.senderKey, claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"), forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain - ?: emptyList() + .orEmpty() ) } else { throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmEncryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmEncryption.kt index a9b84a8e48..fc40331af2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmEncryption.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmEncryption.kt @@ -44,7 +44,7 @@ internal class MXOlmEncryption( ensureSession(userIds) val deviceInfos = ArrayList() for (userId in userIds) { - val devices = cryptoStore.getUserDevices(userId)?.values ?: emptyList() + val devices = cryptoStore.getUserDevices(userId)?.values.orEmpty() for (device in devices) { val key = device.identityKey() if (key == olmDevice.deviceCurve25519Key) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt index e83895709e..19243f1a23 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt @@ -199,7 +199,7 @@ internal object MXEncryptedAttachments { .replace('_', '/') } - private fun base64ToBase64Url(base64: String): String { + internal fun base64ToBase64Url(base64: String): String { return base64.replace("\n".toRegex(), "") .replace("\\+".toRegex(), "-") .replace('/', '_') diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoInfoMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoInfoMapper.kt index f3ddfb8faa..37aae2c47b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoInfoMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoInfoMapper.kt @@ -47,7 +47,7 @@ internal object CryptoInfoMapper { return CryptoCrossSigningKey( userId = keyInfo.userId, usages = keyInfo.usages, - keys = keyInfo.keys ?: emptyMap(), + keys = keyInfo.keys.orEmpty(), signatures = keyInfo.signatures, trustLevel = null ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt index 7064663995..6af96f886d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt @@ -450,7 +450,7 @@ internal class RealmCryptoStore @Inject constructor( } ) return Transformations.map(liveData) { - it.firstOrNull() ?: emptyList() + it.firstOrNull().orEmpty() } } @@ -480,7 +480,7 @@ internal class RealmCryptoStore @Inject constructor( } ) return Transformations.map(liveData) { - it.firstOrNull() ?: emptyList() + it.firstOrNull().orEmpty() } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt index 885abb776d..66ee0c58f5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt @@ -200,6 +200,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi } private fun migrateTo3(realm: DynamicRealm) { + Timber.d("Step 2 -> 3") Timber.d("Updating CryptoMetadataEntity table") realm.schema.get("CryptoMetadataEntity") ?.addField(CryptoMetadataEntityFields.KEY_BACKUP_RECOVERY_KEY, String::class.java) @@ -207,6 +208,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi } private fun migrateTo4(realm: DynamicRealm) { + Timber.d("Step 3 -> 4") Timber.d("Updating KeyInfoEntity table") val keyInfoEntities = realm.where("KeyInfoEntity").findAll() try { @@ -238,6 +240,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi } private fun migrateTo5(realm: DynamicRealm) { + Timber.d("Step 4 -> 5") realm.schema.create("MyDeviceLastSeenInfoEntity") .addField(MyDeviceLastSeenInfoEntityFields.DEVICE_ID, String::class.java) .addPrimaryKey(MyDeviceLastSeenInfoEntityFields.DEVICE_ID) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt index 19049c099c..21960ec9a0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt @@ -78,7 +78,7 @@ internal open class OutgoingGossipingRequestEntity( GossipRequestType.KEY -> { OutgoingRoomKeyRequest( requestBody = getRequestedKeyInfo(), - recipients = getRecipients() ?: emptyMap(), + recipients = getRecipients().orEmpty(), requestId = requestId ?: "", state = requestState ) @@ -86,7 +86,7 @@ internal open class OutgoingGossipingRequestEntity( GossipRequestType.SECRET -> { OutgoingSecretRequest( secretName = getRequestedSecretName(), - recipients = getRecipients() ?: emptyMap(), + recipients = getRecipients().orEmpty(), requestId = requestId ?: "", state = requestState ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tools/Tools.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tools/Tools.kt index c3d2c30079..5e406fdc4f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tools/Tools.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tools/Tools.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.crypto.tools import org.matrix.olm.OlmPkDecryption import org.matrix.olm.OlmPkEncryption import org.matrix.olm.OlmPkSigning +import org.matrix.olm.OlmUtility fun withOlmEncryption(block: (OlmPkEncryption) -> T): T { val olmPkEncryption = OlmPkEncryption() @@ -46,3 +47,12 @@ fun withOlmSigning(block: (OlmPkSigning) -> T): T { olmPkSigning.releaseSigning() } } + +fun withOlmUtility(block: (OlmUtility) -> T): T { + val olmUtility = OlmUtility() + try { + return block(olmUtility) + } finally { + olmUtility.releaseUtility() + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt index e3a765f95c..b1368b45b4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt @@ -198,18 +198,8 @@ internal class DefaultIncomingSASDefaultVerificationTransaction( // using the result as the shared secret. getSAS().setTheirPublicKey(otherKey) - // (Note: In all of the following HKDF is as defined in RFC 5869, and uses the previously agreed-on hash function as the hash function, - // the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of: - // - the string “MATRIX_KEY_VERIFICATION_SAS”, - // - the Matrix ID of the user who sent the m.key.verification.start message, - // - the device ID of the device that sent the m.key.verification.start message, - // - the Matrix ID of the user who sent the m.key.verification.accept message, - // - he device ID of the device that sent the m.key.verification.accept message - // - the transaction ID. - val sasInfo = "MATRIX_KEY_VERIFICATION_SAS$otherUserId$otherDeviceId$userId$deviceId$transactionId" - // decimal: generate five bytes by using HKDF. - // emoji: generate six bytes by using HKDF. - shortCodeBytes = getSAS().generateShortCode(sasInfo, 6) + + shortCodeBytes = calculateSASBytes() if (BuildConfig.LOG_PRIVATE_DATA) { Timber.v("************ BOB CODE ${getDecimalCodeRepresentation(shortCodeBytes!!)}") @@ -219,6 +209,35 @@ internal class DefaultIncomingSASDefaultVerificationTransaction( state = VerificationTxState.ShortCodeReady } + private fun calculateSASBytes(): ByteArray { + when (accepted?.keyAgreementProtocol) { + KEY_AGREEMENT_V1 -> { + // (Note: In all of the following HKDF is as defined in RFC 5869, and uses the previously agreed-on hash function as the hash function, + // the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of: + // - the string “MATRIX_KEY_VERIFICATION_SAS”, + // - the Matrix ID of the user who sent the m.key.verification.start message, + // - the device ID of the device that sent the m.key.verification.start message, + // - the Matrix ID of the user who sent the m.key.verification.accept message, + // - he device ID of the device that sent the m.key.verification.accept message + // - the transaction ID. + val sasInfo = "MATRIX_KEY_VERIFICATION_SAS$otherUserId$otherDeviceId$userId$deviceId$transactionId" + + // decimal: generate five bytes by using HKDF. + // emoji: generate six bytes by using HKDF. + return getSAS().generateShortCode(sasInfo, 6) + } + KEY_AGREEMENT_V2 -> { + // Adds the SAS public key, and separate by | + val sasInfo = "MATRIX_KEY_VERIFICATION_SAS|$otherUserId|$otherDeviceId|$otherKey|$userId|$deviceId|${getSAS().publicKey}|$transactionId" + return getSAS().generateShortCode(sasInfo, 6) + } + else -> { + // Protocol has been checked earlier + throw IllegalArgumentException() + } + } + } + override fun onKeyVerificationMac(vMac: ValidVerificationInfoMac) { Timber.v("## SAS I: received mac for request id:$transactionId") // Check for state? diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt index 689829b8e3..8081c07bcc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt @@ -121,7 +121,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction( // } // // val requestMessage = KeyVerificationRequest( -// fromDevice = session.sessionParams.credentials.deviceId ?: "", +// fromDevice = session.sessionParams.deviceId ?: "", // methods = listOf(KeyVerificationStart.VERIF_METHOD_SAS), // timestamp = System.currentTimeMillis().toInt(), // transactionId = transactionId @@ -193,18 +193,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction( if (accepted!!.commitment.equals(otherCommitment)) { getSAS().setTheirPublicKey(otherKey) - // (Note: In all of the following HKDF is as defined in RFC 5869, and uses the previously agreed-on hash function as the hash function, - // the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of: - // - the string “MATRIX_KEY_VERIFICATION_SAS”, - // - the Matrix ID of the user who sent the m.key.verification.start message, - // - the device ID of the device that sent the m.key.verification.start message, - // - the Matrix ID of the user who sent the m.key.verification.accept message, - // - he device ID of the device that sent the m.key.verification.accept message - // - the transaction ID. - val sasInfo = "MATRIX_KEY_VERIFICATION_SAS$userId$deviceId$otherUserId$otherDeviceId$transactionId" - // decimal: generate five bytes by using HKDF. - // emoji: generate six bytes by using HKDF. - shortCodeBytes = getSAS().generateShortCode(sasInfo, 6) + shortCodeBytes = calculateSASBytes() state = VerificationTxState.ShortCodeReady } else { // bad commitment @@ -212,14 +201,45 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction( } } + private fun calculateSASBytes(): ByteArray { + when (accepted?.keyAgreementProtocol) { + KEY_AGREEMENT_V1 -> { + // (Note: In all of the following HKDF is as defined in RFC 5869, and uses the previously agreed-on hash function as the hash function, + // the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of: + // - the string “MATRIX_KEY_VERIFICATION_SAS”, + // - the Matrix ID of the user who sent the m.key.verification.start message, + // - the device ID of the device that sent the m.key.verification.start message, + // - the Matrix ID of the user who sent the m.key.verification.accept message, + // - he device ID of the device that sent the m.key.verification.accept message + // - the transaction ID. + val sasInfo = "MATRIX_KEY_VERIFICATION_SAS$userId$deviceId$otherUserId$otherDeviceId$transactionId" + + // decimal: generate five bytes by using HKDF. + // emoji: generate six bytes by using HKDF. + return getSAS().generateShortCode(sasInfo, 6) + } + KEY_AGREEMENT_V2 -> { + // Adds the SAS public key, and separate by | + val sasInfo = "MATRIX_KEY_VERIFICATION_SAS|$userId|$deviceId|${getSAS().publicKey}|$otherUserId|$otherDeviceId|$otherKey|$transactionId" + return getSAS().generateShortCode(sasInfo, 6) + } + else -> { + // Protocol has been checked earlier + throw IllegalArgumentException() + } + } + } + override fun onKeyVerificationMac(vMac: ValidVerificationInfoMac) { Timber.v("## SAS O: onKeyVerificationMac id:$transactionId") + // There is starting to be a huge amount of state / race here :/ if (state != VerificationTxState.OnKeyReceived && state != VerificationTxState.ShortCodeReady && state != VerificationTxState.ShortCodeAccepted + && state != VerificationTxState.KeySent && state != VerificationTxState.SendingMac && state != VerificationTxState.MacSent) { - Timber.e("## SAS O: received key from invalid state $state") + Timber.e("## SAS O: received mac from invalid state $state") cancel(CancelCode.UnexpectedMessage) return } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt index 04a3560223..0f1666bd9d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt @@ -81,9 +81,9 @@ import im.vector.matrix.android.internal.crypto.verification.qrcode.generateShar import im.vector.matrix.android.internal.di.DeviceId import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import timber.log.Timber import java.util.UUID @@ -104,7 +104,8 @@ internal class DefaultVerificationService @Inject constructor( private val verificationTransportRoomMessageFactory: VerificationTransportRoomMessageFactory, private val verificationTransportToDeviceFactory: VerificationTransportToDeviceFactory, private val crossSigningService: CrossSigningService, - private val cryptoCoroutineScope: CoroutineScope + private val cryptoCoroutineScope: CoroutineScope, + private val taskExecutor: TaskExecutor ) : DefaultVerificationTransaction.Listener, VerificationService { private val uiHandler = Handler(Looper.getMainLooper()) @@ -161,7 +162,7 @@ internal class DefaultVerificationService @Inject constructor( } fun onRoomEvent(event: Event) { - GlobalScope.launch(coroutineDispatchers.crypto) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { when (event.getClearType()) { EventType.KEY_VERIFICATION_START -> { onRoomStartRequestReceived(event) @@ -301,7 +302,7 @@ internal class DefaultVerificationService @Inject constructor( // We don't want to block here val otherDeviceId = validRequestInfo.fromDevice - GlobalScope.launch { + cryptoCoroutineScope.launch { if (checkKeysAreDownloaded(senderId, otherDeviceId) == null) { Timber.e("## Verification device $otherDeviceId is not known") } @@ -340,7 +341,7 @@ internal class DefaultVerificationService @Inject constructor( } // We don't want to block here - GlobalScope.launch { + taskExecutor.executorScope.launch { if (checkKeysAreDownloaded(senderId, validRequestInfo.fromDevice) == null) { Timber.e("## SAS Verification device ${validRequestInfo.fromDevice} is not known") } @@ -610,7 +611,7 @@ internal class DefaultVerificationService @Inject constructor( if (validCancelReq == null) { // ignore - Timber.e("## SAS Received invalid key request") + Timber.e("## SAS Received invalid cancel request") // TODO should we cancel? return } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASDefaultVerificationTransaction.kt index 2a502730fa..7048d790a0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASDefaultVerificationTransaction.kt @@ -26,7 +26,6 @@ import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.internal.crypto.IncomingGossipingRequestManager import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction -import im.vector.matrix.android.internal.crypto.model.MXKey import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.extensions.toUnsignedInt import im.vector.matrix.android.internal.util.withoutPrefix @@ -66,8 +65,11 @@ internal abstract class SASDefaultVerificationTransaction( const val SAS_MAC_SHA256_LONGKDF = "hmac-sha256" const val SAS_MAC_SHA256 = "hkdf-hmac-sha256" + // Deprecated maybe removed later, use V2 + const val KEY_AGREEMENT_V1 = "curve25519" + const val KEY_AGREEMENT_V2 = "curve25519-hkdf-sha256" // ordered by preferred order - val KNOWN_AGREEMENT_PROTOCOLS = listOf(MXKey.KEY_CURVE_25519_TYPE) + val KNOWN_AGREEMENT_PROTOCOLS = listOf(KEY_AGREEMENT_V2, KEY_AGREEMENT_V1) // ordered by preferred order val KNOWN_HASHES = listOf("sha256") // ordered by preferred order diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt index 77234e82f4..e0bbffc23b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt @@ -48,10 +48,11 @@ import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.WorkManagerProvider import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory +import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.StringProvider import im.vector.matrix.android.internal.worker.WorkerParamsFactory +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import timber.log.Timber import java.util.UUID @@ -66,7 +67,8 @@ internal class VerificationTransportRoomMessage( private val userDeviceId: String?, private val roomId: String, private val localEchoEventFactory: LocalEchoEventFactory, - private val tx: DefaultVerificationTransaction? + private val tx: DefaultVerificationTransaction?, + private val coroutineScope: CoroutineScope ) : VerificationTransport { override fun sendToOther(type: String, @@ -131,7 +133,7 @@ internal class VerificationTransportRoomMessage( } // TODO listen to DB to get synced info - GlobalScope.launch(Dispatchers.Main) { + coroutineScope.launch(Dispatchers.Main) { workLiveData.observeForever(observer) } } @@ -212,7 +214,7 @@ internal class VerificationTransportRoomMessage( } // TODO listen to DB to get synced info - GlobalScope.launch(Dispatchers.Main) { + coroutineScope.launch(Dispatchers.Main) { workLiveData.observeForever(observer) } } @@ -265,7 +267,7 @@ internal class VerificationTransportRoomMessage( } // TODO listen to DB to get synced info - GlobalScope.launch(Dispatchers.Main) { + coroutineScope.launch(Dispatchers.Main) { workLiveData.observeForever(observer) } } @@ -384,9 +386,19 @@ internal class VerificationTransportRoomMessageFactory @Inject constructor( private val userId: String, @DeviceId private val deviceId: String?, - private val localEchoEventFactory: LocalEchoEventFactory) { + private val localEchoEventFactory: LocalEchoEventFactory, + private val taskExecutor: TaskExecutor +) { fun createTransport(roomId: String, tx: DefaultVerificationTransaction?): VerificationTransportRoomMessage { - return VerificationTransportRoomMessage(workManagerProvider, stringProvider, sessionId, userId, deviceId, roomId, localEchoEventFactory, tx) + return VerificationTransportRoomMessage(workManagerProvider, + stringProvider, + sessionId, + userId, + deviceId, + roomId, + localEchoEventFactory, + tx, + taskExecutor.executorScope) } } 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 80376fb6ee..d86151e694 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 @@ -60,10 +60,9 @@ internal fun ChunkEntity.merge(roomId: String, chunkToMerge: ChunkEntity, direct chunkToMerge.stateEvents.forEach { stateEvent -> addStateEvent(roomId, stateEvent, direction) } - return eventsToMerge - .forEach { - addTimelineEventFromMerge(localRealm, it, direction) - } + eventsToMerge.forEach { + addTimelineEventFromMerge(localRealm, it, direction) + } } internal fun ChunkEntity.addStateEvent(roomId: String, stateEvent: EventEntity, direction: PaginationDirection) { 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 0bf02aa92f..a46c49a706 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 @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.database.helper import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields +import im.vector.matrix.android.internal.extensions.assertIsManaged import io.realm.Realm internal fun TimelineEventEntity.Companion.nextId(realm: Realm): Long { @@ -28,3 +29,11 @@ internal fun TimelineEventEntity.Companion.nextId(realm: Realm): Long { currentIdNum.toLong() + 1 } } + +internal fun TimelineEventEntity.deleteOnCascade() { + assertIsManaged() + root?.deleteFromRealm() + annotations?.deleteFromRealm() + readReceipts?.deleteFromRealm() + deleteFromRealm() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/HomeServerCapabilitiesMapper.kt index a0d3662e03..a66f587cec 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/HomeServerCapabilitiesMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/HomeServerCapabilitiesMapper.kt @@ -20,21 +20,16 @@ import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities import im.vector.matrix.android.internal.database.model.HomeServerCapabilitiesEntity /** - * HomeServerCapabilitiesEntity <-> HomeSeverCapabilities + * HomeServerCapabilitiesEntity -> HomeSeverCapabilities */ internal object HomeServerCapabilitiesMapper { fun map(entity: HomeServerCapabilitiesEntity): HomeServerCapabilities { return HomeServerCapabilities( canChangePassword = entity.canChangePassword, - maxUploadFileSize = entity.maxUploadFileSize - ) - } - - fun map(domain: HomeServerCapabilities): HomeServerCapabilitiesEntity { - return HomeServerCapabilitiesEntity( - canChangePassword = domain.canChangePassword, - maxUploadFileSize = domain.maxUploadFileSize + maxUploadFileSize = entity.maxUploadFileSize, + lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported, + defaultIdentityServerUrl = entity.defaultIdentityServerUrl ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/PushRulesMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/PushRulesMapper.kt index a0f644a7cf..4425582a9b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/PushRulesMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/PushRulesMapper.kt @@ -45,7 +45,7 @@ internal object PushRulesMapper { private fun fromActionStr(actionsStr: String?): List { try { - return actionsStr?.let { moshiActionsAdapter.fromJson(it) } ?: emptyList() + return actionsStr?.let { moshiActionsAdapter.fromJson(it) }.orEmpty() } catch (e: Throwable) { Timber.e(e, "## failed to map push rule actions <$actionsStr>") return emptyList() 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 20651069b0..9d3da20b0e 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 @@ -49,7 +49,7 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa membership = roomSummaryEntity.membership, versioningState = roomSummaryEntity.versioningState, readMarkerId = roomSummaryEntity.readMarkerId, - userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList(), + userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) }.orEmpty(), canonicalAlias = roomSummaryEntity.canonicalAlias, aliases = roomSummaryEntity.aliases.toList(), isEncrypted = roomSummaryEntity.isEncrypted, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt index 4bd9b9855b..a90e62e15c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt @@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.database.mapper import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.model.ReadReceipt - +import im.vector.matrix.android.api.session.room.sender.SenderInfo import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.database.model.TimelineEventEntity import javax.inject.Inject @@ -41,15 +41,18 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS annotations = timelineEventEntity.annotations?.asDomain(), localId = timelineEventEntity.localId, displayIndex = timelineEventEntity.displayIndex, - senderName = timelineEventEntity.senderName, - isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName, - senderAvatar = timelineEventEntity.senderAvatar, + senderInfo = SenderInfo( + userId = timelineEventEntity.root?.sender ?: "", + displayName = timelineEventEntity.senderName, + isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName, + avatarUrl = timelineEventEntity.senderAvatar + ), readReceipts = readReceipts ?.distinctBy { it.user }?.sortedByDescending { it.originServerTs - } ?: emptyList() + }.orEmpty() ) } } 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 2d294e6783..19bf72970c 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 @@ -23,15 +23,20 @@ import io.realm.annotations.Index import io.realm.annotations.LinkingObjects internal open class ChunkEntity(@Index var prevToken: String? = null, + // Because of gaps we can have several chunks with nextToken == null @Index var nextToken: String? = null, var stateEvents: RealmList = RealmList(), var timelineEvents: RealmList = RealmList(), + // Only one chunk will have isLastForward == true @Index var isLastForward: Boolean = false, @Index var isLastBackward: Boolean = false ) : RealmObject() { fun identifier() = "${prevToken}_$nextToken" + // If true, then this chunk was previously a last forward chunk + fun hasBeenALastForwardChunk() = nextToken == null && !isLastForward + @LinkingObjects("chunks") val room: RealmResults? = null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/HomeServerCapabilitiesEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/HomeServerCapabilitiesEntity.kt index 5743597a61..a6b250b8fa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/HomeServerCapabilitiesEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/HomeServerCapabilitiesEntity.kt @@ -22,6 +22,8 @@ import io.realm.RealmObject internal open class HomeServerCapabilitiesEntity( var canChangePassword: Boolean = true, var maxUploadFileSize: Long = HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN, + var lastVersionIdentityServerSupported: Boolean = false, + var defaultIdentityServerUrl: String? = null, var lastUpdatedTimestamp: Long = 0L ) : RealmObject() { 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 081a6a5152..accac9ca97 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 @@ -37,6 +37,7 @@ import io.realm.annotations.RealmModule UserEntity::class, IgnoredUserEntity::class, BreadcrumbsEntity::class, + UserThreePidEntity::class, EventAnnotationsSummaryEntity::class, ReactionAggregatedSummaryEntity::class, EditAggregatedSummaryEntity::class, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/UserThreePidEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/UserThreePidEntity.kt new file mode 100644 index 0000000000..f41ac1baa7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/UserThreePidEntity.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.database.model + +import io.realm.RealmObject + +internal open class UserThreePidEntity( + var medium: String = "", + var address: String = "", + var validatedAt: Long = 0, + var addedAt: Long = 0 +) : 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 009ee4b7fe..5efb84a105 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 @@ -41,7 +41,7 @@ internal fun ChunkEntity.Companion.find(realm: Realm, roomId: String, prevToken: return query.findFirst() } -internal fun ChunkEntity.Companion.findLastLiveChunkFromRoom(realm: Realm, roomId: String): ChunkEntity? { +internal fun ChunkEntity.Companion.findLastForwardChunkOfRoom(realm: Realm, roomId: String): ChunkEntity? { return where(realm, roomId) .equalTo(ChunkEntityFields.IS_LAST_FORWARD, true) .findFirst() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterEntityQueries.kt index 6902d39a82..42a84113ca 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterEntityQueries.kt @@ -35,7 +35,7 @@ internal fun FilterEntity.Companion.get(realm: Realm): FilterEntity? { internal fun FilterEntity.Companion.getOrCreate(realm: Realm): FilterEntity { return get(realm) ?: realm.createObject() .apply { - filterBodyJson = FilterFactory.createDefaultFilterBody().toJSONString() + filterBodyJson = FilterFactory.createDefaultFilter().toJSONString() roomEventFilterJson = FilterFactory.createDefaultRoomFilter().toJSONString() filterId = "" } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt index 1b83577a8c..9c73dff1dd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt @@ -36,7 +36,7 @@ internal fun isEventRead(monarchy: Monarchy, var isEventRead = false monarchy.doWithRealm { realm -> - val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) ?: return@doWithRealm + val liveChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) ?: return@doWithRealm val eventToCheck = liveChunk.timelineEvents.find(eventId) isEventRead = if (eventToCheck == null || eventToCheck.root?.sender == userId) { true 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 5168d0728e..fb1cc8136a 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 @@ -59,11 +59,11 @@ internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm, filterTypes: List = emptyList()): TimelineEventEntity? { val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: return null val sendingTimelineEvents = roomEntity.sendingTimelineEvents.where().filterTypes(filterTypes) - val liveEvents = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)?.timelineEvents?.where()?.filterTypes(filterTypes) + val liveEvents = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)?.timelineEvents?.where()?.filterTypes(filterTypes) if (filterContentRelation) { liveEvents - ?.not()?.like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.EDIT_TYPE) - ?.not()?.like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.RESPONSE_TYPE) + ?.not()?.like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT) + ?.not()?.like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE) } val query = if (includesSending && sendingTimelineEvents.findAll().isNotEmpty()) { sendingTimelineEvents diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventFilter.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventFilter.kt new file mode 100644 index 0000000000..14560ead85 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventFilter.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.database.query + +/** + * Query strings used to filter the timeline events regarding the Json raw string of the Event + */ +internal object TimelineEventFilter { + /** + * To apply to Event.content + */ + internal object Content { + internal const val EDIT = """{*"m.relates_to"*"rel_type":*"m.replace"*}""" + internal const val RESPONSE = """{*"m.relates_to"*"rel_type":*"m.response"*}""" + } + + /** + * To apply to Event.decryptionResultJson + */ + internal object DecryptedContent { + internal const val URL = """{*"file":*"url":*}""" + } + + /** + * To apply to Event.unsigned + */ + internal object Unsigned { + internal const val REDACTED = """{*"redacted_because":*}""" + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/AuthQualifiers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/AuthQualifiers.kt index 8ee27b3375..0ceb94caa7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/AuthQualifiers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/AuthQualifiers.kt @@ -20,8 +20,12 @@ import javax.inject.Qualifier @Qualifier @Retention(AnnotationRetention.RUNTIME) -annotation class Authenticated +internal annotation class Authenticated @Qualifier @Retention(AnnotationRetention.RUNTIME) -annotation class Unauthenticated +internal annotation class AuthenticatedIdentity + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class Unauthenticated diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/DbQualifiers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/DbQualifiers.kt index 3fdeb7eacc..4501ae5746 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/DbQualifiers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/DbQualifiers.kt @@ -20,12 +20,16 @@ import javax.inject.Qualifier @Qualifier @Retention(AnnotationRetention.RUNTIME) -annotation class AuthDatabase +internal annotation class AuthDatabase @Qualifier @Retention(AnnotationRetention.RUNTIME) -annotation class SessionDatabase +internal annotation class SessionDatabase @Qualifier @Retention(AnnotationRetention.RUNTIME) -annotation class CryptoDatabase +internal annotation class CryptoDatabase + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class IdentityDatabase diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/FileQualifiers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/FileQualifiers.kt index aa39fc6fe8..5dfc04539a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/FileQualifiers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/FileQualifiers.kt @@ -20,16 +20,16 @@ import javax.inject.Qualifier @Qualifier @Retention(AnnotationRetention.RUNTIME) -annotation class SessionFilesDirectory +internal annotation class SessionFilesDirectory @Qualifier @Retention(AnnotationRetention.RUNTIME) -annotation class SessionCacheDirectory +internal annotation class SessionCacheDirectory @Qualifier @Retention(AnnotationRetention.RUNTIME) -annotation class CacheDirectory +internal annotation class CacheDirectory @Qualifier @Retention(AnnotationRetention.RUNTIME) -annotation class ExternalFilesDirectory +internal annotation class ExternalFilesDirectory diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NetworkModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NetworkModule.kt index b7ab257363..ddde4fc7d6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NetworkModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NetworkModule.kt @@ -21,6 +21,7 @@ import com.squareup.moshi.Moshi import dagger.Module import dagger.Provides import im.vector.matrix.android.BuildConfig +import im.vector.matrix.android.api.MatrixConfiguration import im.vector.matrix.android.internal.network.TimeOutInterceptor import im.vector.matrix.android.internal.network.UserAgentInterceptor import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor @@ -64,7 +65,8 @@ internal object NetworkModule { @Provides @JvmStatic @Unauthenticated - fun providesOkHttpClient(stethoInterceptor: StethoInterceptor, + fun providesOkHttpClient(matrixConfiguration: MatrixConfiguration, + stethoInterceptor: StethoInterceptor, timeoutInterceptor: TimeOutInterceptor, userAgentInterceptor: UserAgentInterceptor, httpLoggingInterceptor: HttpLoggingInterceptor, @@ -82,6 +84,9 @@ internal object NetworkModule { if (BuildConfig.LOG_PRIVATE_DATA) { addInterceptor(curlLoggingInterceptor) } + matrixConfiguration.proxy?.let { + proxy(it) + } } .addInterceptor(okReplayInterceptor) .build() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/extensions/Strings.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/extensions/Strings.kt new file mode 100644 index 0000000000..09913b9f04 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/extensions/Strings.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.extensions + +/** + * Ex: "abcdef".subStringBetween("a", "f") -> "bcde" + * Ex: "abcdefff".subStringBetween("a", "f") -> "bcdeff" + * Ex: "aaabcdef".subStringBetween("a", "f") -> "aabcde" + */ +internal fun String.subStringBetween(prefix: String, suffix: String) = substringAfter(prefix).substringBeforeLast(suffix) 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 c802d4b63a..a15f660790 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 @@ -16,20 +16,16 @@ package im.vector.matrix.android.internal.network -import im.vector.matrix.android.internal.auth.SessionParamsStore -import im.vector.matrix.android.internal.di.SessionId +import im.vector.matrix.android.internal.network.token.AccessTokenProvider import okhttp3.Interceptor import okhttp3.Response -import javax.inject.Inject -internal class AccessTokenInterceptor @Inject constructor( - @SessionId private val sessionId: String, - private val sessionParamsStore: SessionParamsStore) : Interceptor { +internal class AccessTokenInterceptor(private val accessTokenProvider: AccessTokenProvider) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { var request = chain.request() - accessToken?.let { + accessTokenProvider.getToken()?.let { val newRequestBuilder = request.newBuilder() // Add the access token to all requests if it is set newRequestBuilder.addHeader(HttpHeaders.Authorization, "Bearer $it") @@ -38,7 +34,4 @@ internal class AccessTokenInterceptor @Inject constructor( return chain.proceed(request) } - - private val accessToken - get() = sessionParamsStore.get(sessionId)?.credentials?.accessToken } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt index ab6745148f..56e6ee0953 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt @@ -28,8 +28,9 @@ internal object NetworkConstants { const val URI_API_MEDIA_PREFIX_PATH_R0 = "$URI_API_MEDIA_PREFIX_PATH/r0/" // Identity server - const val URI_IDENTITY_PATH = "_matrix/identity/api/v1/" - const val URI_IDENTITY_PATH_V2 = "_matrix/identity/v2/" + const val URI_IDENTITY_PREFIX_PATH = "_matrix/identity/v2" + const val URI_IDENTITY_PATH_V2 = "$URI_IDENTITY_PREFIX_PATH/" - const val URI_API_PREFIX_IDENTITY = "_matrix/identity/api/v1" + // TODO Ganfra, use correct value + const val URI_INTEGRATION_MANAGER_PATH = "TODO/" } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/httpclient/OkHttpClientUtil.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/httpclient/OkHttpClientUtil.kt new file mode 100644 index 0000000000..8ffa0553e9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/httpclient/OkHttpClientUtil.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.network.httpclient + +import im.vector.matrix.android.internal.network.AccessTokenInterceptor +import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor +import im.vector.matrix.android.internal.network.token.AccessTokenProvider +import okhttp3.OkHttpClient + +internal fun OkHttpClient.addAccessTokenInterceptor(accessTokenProvider: AccessTokenProvider): OkHttpClient { + return newBuilder() + .apply { + // Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor + val existingCurlInterceptors = interceptors().filterIsInstance() + interceptors().removeAll(existingCurlInterceptors) + + addInterceptor(AccessTokenInterceptor(accessTokenProvider)) + + // Re add eventually the curl logging interceptors + existingCurlInterceptors.forEach { + addInterceptor(it) + } + } + .build() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/token/AccessTokenProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/token/AccessTokenProvider.kt new file mode 100644 index 0000000000..08176392fa --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/token/AccessTokenProvider.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.network.token + +internal interface AccessTokenProvider { + fun getToken(): String? +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/token/HomeserverAccessTokenProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/token/HomeserverAccessTokenProvider.kt new file mode 100644 index 0000000000..b570cb362e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/token/HomeserverAccessTokenProvider.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.network.token + +import im.vector.matrix.android.internal.auth.SessionParamsStore +import im.vector.matrix.android.internal.di.SessionId +import javax.inject.Inject + +internal class HomeserverAccessTokenProvider @Inject constructor( + @SessionId private val sessionId: String, + private val sessionParamsStore: SessionParamsStore +) : AccessTokenProvider { + override fun getToken() = sessionParamsStore.get(sessionId)?.credentials?.accessToken +} 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 cfb0d23f2b..0cdd39f117 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 @@ -28,10 +28,10 @@ import im.vector.matrix.android.internal.di.ExternalFilesDirectory import im.vector.matrix.android.internal.di.SessionCacheDirectory import im.vector.matrix.android.internal.di.Unauthenticated import im.vector.matrix.android.internal.extensions.foldToCallback +import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.toCancelable import im.vector.matrix.android.internal.util.writeToFile -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okhttp3.OkHttpClient @@ -51,7 +51,9 @@ internal class DefaultFileService @Inject constructor( private val contentUrlResolver: ContentUrlResolver, @Unauthenticated private val okHttpClient: OkHttpClient, - private val coroutineDispatchers: MatrixCoroutineDispatchers) : FileService { + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val taskExecutor: TaskExecutor +) : FileService { /** * Download file in the cache folder, and eventually decrypt it @@ -63,7 +65,7 @@ internal class DefaultFileService @Inject constructor( url: String?, elementToDecrypt: ElementToDecrypt?, callback: MatrixCallback): Cancelable { - return GlobalScope.launch(coroutineDispatchers.main) { + return taskExecutor.executorScope.launch(coroutineDispatchers.main) { withContext(coroutineDispatchers.io) { Try { val folder = File(sessionCacheDirectory, "MF") 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 b30c29a719..2a96784451 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 @@ -43,6 +43,7 @@ import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageSer import im.vector.matrix.android.api.session.signout.SignOutService import im.vector.matrix.android.api.session.sync.FilterService import im.vector.matrix.android.api.session.sync.SyncState +import im.vector.matrix.android.api.session.terms.TermsService 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 @@ -50,12 +51,14 @@ import im.vector.matrix.android.internal.crypto.crosssigning.ShieldTrustUpdater import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.di.WorkManagerProvider +import im.vector.matrix.android.internal.session.identity.DefaultIdentityService import im.vector.matrix.android.internal.session.room.timeline.TimelineEventDecryptor import im.vector.matrix.android.internal.session.sync.SyncTokenStore import im.vector.matrix.android.internal.session.sync.job.SyncThread import im.vector.matrix.android.internal.session.sync.job.SyncWorker +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe @@ -82,6 +85,7 @@ internal class DefaultSession @Inject constructor( private val signOutService: Lazy, private val pushRuleService: Lazy, private val pushersService: Lazy, + private val termsService: Lazy, private val cryptoService: Lazy, private val fileService: Lazy, private val secureStorageService: Lazy, @@ -97,8 +101,11 @@ internal class DefaultSession @Inject constructor( private val _sharedSecretStorageService: Lazy, private val accountService: Lazy, private val timelineEventDecryptor: TimelineEventDecryptor, - private val shieldTrustUpdater: ShieldTrustUpdater) - : Session, + private val shieldTrustUpdater: ShieldTrustUpdater, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val defaultIdentityService: DefaultIdentityService, + private val taskExecutor: TaskExecutor +) : Session, RoomService by roomService.get(), RoomDirectoryService by roomDirectoryService.get(), GroupService by groupService.get(), @@ -108,6 +115,7 @@ internal class DefaultSession @Inject constructor( PushRuleService by pushRuleService.get(), PushersService by pushersService.get(), FileService by fileService.get(), + TermsService by termsService.get(), InitialSyncProgressService by initialSyncProgressService.get(), SecureStorageService by secureStorageService.get(), HomeServerCapabilitiesService by homeServerCapabilitiesService.get(), @@ -133,6 +141,7 @@ internal class DefaultSession @Inject constructor( eventBus.register(this) timelineEventDecryptor.start() shieldTrustUpdater.start() + defaultIdentityService.start() } override fun requireBackgroundSync() { @@ -175,6 +184,10 @@ internal class DefaultSession @Inject constructor( isOpen = false eventBus.unregister(this) shieldTrustUpdater.stop() + taskExecutor.executorScope.launch(coroutineDispatchers.main) { + // This has to be done on main thread + defaultIdentityService.stop() + } } override fun getSyncStateLive(): LiveData { @@ -204,7 +217,7 @@ internal class DefaultSession @Inject constructor( if (globalError is GlobalError.InvalidToken && globalError.softLogout) { // Mark the token has invalid - GlobalScope.launch(Dispatchers.IO) { + taskExecutor.executorScope.launch(Dispatchers.IO) { sessionParamsStore.setTokenInvalid(sessionId) } } @@ -218,6 +231,8 @@ internal class DefaultSession @Inject constructor( override fun cryptoService(): CryptoService = cryptoService.get() + override fun identityService() = defaultIdentityService + override fun addListener(listener: Session.Listener) { sessionListeners.addListener(listener) } @@ -228,6 +243,6 @@ internal class DefaultSession @Inject constructor( // For easy debugging override fun toString(): String { - return "$myUserId - ${sessionParams.credentials.deviceId}" + return "$myUserId - ${sessionParams.deviceId}" } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt index 0ebfc1c4c5..ca8ab42ab8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt @@ -36,6 +36,7 @@ import im.vector.matrix.android.internal.session.filter.FilterModule import im.vector.matrix.android.internal.session.group.GetGroupDataWorker import im.vector.matrix.android.internal.session.group.GroupModule import im.vector.matrix.android.internal.session.homeserver.HomeServerCapabilitiesModule +import im.vector.matrix.android.internal.session.identity.IdentityModule import im.vector.matrix.android.internal.session.openid.OpenIdModule import im.vector.matrix.android.internal.session.profile.ProfileModule import im.vector.matrix.android.internal.session.pushers.AddHttpPusherWorker @@ -51,6 +52,7 @@ import im.vector.matrix.android.internal.session.sync.SyncModule import im.vector.matrix.android.internal.session.sync.SyncTask import im.vector.matrix.android.internal.session.sync.SyncTokenStore import im.vector.matrix.android.internal.session.sync.job.SyncWorker +import im.vector.matrix.android.internal.session.terms.TermsModule import im.vector.matrix.android.internal.session.user.UserModule import im.vector.matrix.android.internal.session.user.accountdata.AccountDataModule import im.vector.matrix.android.internal.task.TaskExecutor @@ -72,6 +74,8 @@ import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers CryptoModule::class, PushersModule::class, OpenIdModule::class, + IdentityModule::class, + TermsModule::class, AccountDataModule::class, ProfileModule::class, SessionAssistedInjectModule::class, 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 8bdfff062f..c7afcc1d47 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 @@ -50,14 +50,15 @@ import im.vector.matrix.android.internal.di.Unauthenticated import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.UserMd5 import im.vector.matrix.android.internal.eventbus.EventBusTimberLogger -import im.vector.matrix.android.internal.network.AccessTokenInterceptor import im.vector.matrix.android.internal.network.DefaultNetworkConnectivityChecker import im.vector.matrix.android.internal.network.FallbackNetworkCallbackStrategy import im.vector.matrix.android.internal.network.NetworkCallbackStrategy import im.vector.matrix.android.internal.network.NetworkConnectivityChecker import im.vector.matrix.android.internal.network.PreferredNetworkCallbackStrategy import im.vector.matrix.android.internal.network.RetrofitFactory -import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor +import im.vector.matrix.android.internal.network.httpclient.addAccessTokenInterceptor +import im.vector.matrix.android.internal.network.token.AccessTokenProvider +import im.vector.matrix.android.internal.network.token.HomeserverAccessTokenProvider import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater import im.vector.matrix.android.internal.session.homeserver.DefaultHomeServerCapabilitiesService import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater @@ -175,21 +176,8 @@ internal abstract class SessionModule { @SessionScope @Authenticated fun providesOkHttpClient(@Unauthenticated okHttpClient: OkHttpClient, - accessTokenInterceptor: AccessTokenInterceptor): OkHttpClient { - return okHttpClient.newBuilder() - .apply { - // Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor - val existingCurlInterceptors = interceptors().filterIsInstance() - interceptors().removeAll(existingCurlInterceptors) - - addInterceptor(accessTokenInterceptor) - - // Re add eventually the curl logging interceptors - existingCurlInterceptors.forEach { - addInterceptor(it) - } - } - .build() + @Authenticated accessTokenProvider: AccessTokenProvider): OkHttpClient { + return okHttpClient.addAccessTokenInterceptor(accessTokenProvider) } @JvmStatic @@ -233,44 +221,48 @@ internal abstract class SessionModule { } } + @Binds + @Authenticated + abstract fun bindAccessTokenProvider(provider: HomeserverAccessTokenProvider): AccessTokenProvider + @Binds abstract fun bindSession(session: DefaultSession): Session @Binds - abstract fun bindNetworkConnectivityChecker(networkConnectivityChecker: DefaultNetworkConnectivityChecker): NetworkConnectivityChecker + abstract fun bindNetworkConnectivityChecker(checker: DefaultNetworkConnectivityChecker): NetworkConnectivityChecker @Binds @IntoSet - abstract fun bindGroupSummaryUpdater(groupSummaryUpdater: GroupSummaryUpdater): LiveEntityObserver + abstract fun bindGroupSummaryUpdater(updater: GroupSummaryUpdater): LiveEntityObserver @Binds @IntoSet - abstract fun bindEventsPruner(eventsPruner: EventsPruner): LiveEntityObserver + abstract fun bindEventsPruner(pruner: EventsPruner): LiveEntityObserver @Binds @IntoSet - abstract fun bindEventRelationsAggregationUpdater(eventRelationsAggregationUpdater: EventRelationsAggregationUpdater): LiveEntityObserver + abstract fun bindEventRelationsAggregationUpdater(updater: EventRelationsAggregationUpdater): LiveEntityObserver @Binds @IntoSet - abstract fun bindRoomTombstoneEventLiveObserver(roomTombstoneEventLiveObserver: RoomTombstoneEventLiveObserver): LiveEntityObserver + abstract fun bindRoomTombstoneEventLiveObserver(observer: RoomTombstoneEventLiveObserver): LiveEntityObserver @Binds @IntoSet - abstract fun bindRoomCreateEventLiveObserver(roomCreateEventLiveObserver: RoomCreateEventLiveObserver): LiveEntityObserver + abstract fun bindRoomCreateEventLiveObserver(observer: RoomCreateEventLiveObserver): LiveEntityObserver @Binds @IntoSet - abstract fun bindVerificationMessageLiveObserver(verificationMessageLiveObserver: VerificationMessageLiveObserver): LiveEntityObserver + abstract fun bindVerificationMessageLiveObserver(observer: VerificationMessageLiveObserver): LiveEntityObserver @Binds - abstract fun bindInitialSyncProgressService(initialSyncProgressService: DefaultInitialSyncProgressService): InitialSyncProgressService + abstract fun bindInitialSyncProgressService(service: DefaultInitialSyncProgressService): InitialSyncProgressService @Binds - abstract fun bindSecureStorageService(secureStorageService: DefaultSecureStorageService): SecureStorageService + abstract fun bindSecureStorageService(service: DefaultSecureStorageService): SecureStorageService @Binds - abstract fun bindHomeServerCapabilitiesService(homeServerCapabilitiesService: DefaultHomeServerCapabilitiesService): HomeServerCapabilitiesService + abstract fun bindHomeServerCapabilitiesService(service: DefaultHomeServerCapabilitiesService): HomeServerCapabilitiesService @Binds abstract fun bindAccountDataService(service: DefaultAccountDataService): AccountDataService diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DeactivateAccountTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DeactivateAccountTask.kt index f5b105cfee..b993534ba1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DeactivateAccountTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DeactivateAccountTask.kt @@ -19,8 +19,10 @@ package im.vector.matrix.android.internal.session.account import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.cleanup.CleanupSession +import im.vector.matrix.android.internal.session.identity.IdentityDisconnectTask import im.vector.matrix.android.internal.task.Task import org.greenrobot.eventbus.EventBus +import timber.log.Timber import javax.inject.Inject internal interface DeactivateAccountTask : Task { @@ -34,6 +36,7 @@ internal class DefaultDeactivateAccountTask @Inject constructor( private val accountAPI: AccountAPI, private val eventBus: EventBus, @UserId private val userId: String, + private val identityDisconnectTask: IdentityDisconnectTask, private val cleanupSession: CleanupSession ) : DeactivateAccountTask { @@ -44,6 +47,10 @@ internal class DefaultDeactivateAccountTask @Inject constructor( apiCall = accountAPI.deactivate(deactivateAccountParams) } + // Logout from identity server if any, ignoring errors + runCatching { identityDisconnectTask.execute(Unit) } + .onFailure { Timber.w(it, "Unable to disconnect identity server") } + cleanupSession.handle() } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentModule.kt index 1faf489dc4..577626c8ac 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentModule.kt @@ -25,8 +25,8 @@ import im.vector.matrix.android.api.session.content.ContentUrlResolver internal abstract class ContentModule { @Binds - abstract fun bindContentUploadStateTracker(contentUploadStateTracker: DefaultContentUploadStateTracker): ContentUploadStateTracker + abstract fun bindContentUploadStateTracker(tracker: DefaultContentUploadStateTracker): ContentUploadStateTracker @Binds - abstract fun bindContentUrlResolver(contentUrlResolver: DefaultContentUrlResolver): ContentUrlResolver + abstract fun bindContentUrlResolver(resolver: DefaultContentUrlResolver): ContentUrlResolver } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultFilterRepository.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultFilterRepository.kt index f7df8c512e..95291de4b6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultFilterRepository.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultFilterRepository.kt @@ -28,25 +28,25 @@ import javax.inject.Inject internal class DefaultFilterRepository @Inject constructor(private val monarchy: Monarchy) : FilterRepository { - override suspend fun storeFilter(filterBody: FilterBody, roomEventFilter: RoomEventFilter): Boolean { + override suspend fun storeFilter(filter: Filter, roomEventFilter: RoomEventFilter): Boolean { return Realm.getInstance(monarchy.realmConfiguration).use { realm -> - val filter = FilterEntity.get(realm) + val filterEntity = FilterEntity.get(realm) // Filter has changed, or no filter Id yet - filter == null - || filter.filterBodyJson != filterBody.toJSONString() - || filter.filterId.isBlank() + filterEntity == null + || filterEntity.filterBodyJson != filter.toJSONString() + || filterEntity.filterId.isBlank() }.also { hasChanged -> if (hasChanged) { // Filter is new or has changed, store it and reset the filter Id. // This has to be done outside of the Realm.use(), because awaitTransaction change the current thread monarchy.awaitTransaction { realm -> // We manage only one filter for now - val filterBodyJson = filterBody.toJSONString() + val filterJson = filter.toJSONString() val roomEventFilterJson = roomEventFilter.toJSONString() val filterEntity = FilterEntity.getOrCreate(realm) - filterEntity.filterBodyJson = filterBodyJson + filterEntity.filterBodyJson = filterJson filterEntity.roomEventFilterJson = roomEventFilterJson // Reset filterId filterEntity.filterId = "" @@ -55,14 +55,14 @@ internal class DefaultFilterRepository @Inject constructor(private val monarchy: } } - override suspend fun storeFilterId(filterBody: FilterBody, filterId: String) { + override suspend fun storeFilterId(filter: Filter, filterId: String) { monarchy.awaitTransaction { // We manage only one filter for now - val filterBodyJson = filterBody.toJSONString() + val filterJson = filter.toJSONString() // Update the filter id, only if the filter body matches it.where() - .equalTo(FilterEntityFields.FILTER_BODY_JSON, filterBodyJson) + .equalTo(FilterEntityFields.FILTER_BODY_JSON, filterJson) ?.findFirst() ?.filterId = filterId } 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 47c5e4a08a..f396e01e86 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 @@ -43,10 +43,10 @@ internal class DefaultSaveFilterTask @Inject constructor( override suspend fun execute(params: SaveFilterTask.Params) { val filterBody = when (params.filterPreset) { FilterService.FilterPreset.RiotFilter -> { - FilterFactory.createRiotFilterBody() + FilterFactory.createRiotFilter() } FilterService.FilterPreset.NoFilter -> { - FilterFactory.createDefaultFilterBody() + FilterFactory.createDefaultFilter() } } val roomFilter = when (params.filterPreset) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/EventFilter.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/EventFilter.kt new file mode 100644 index 0000000000..f5d159588b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/EventFilter.kt @@ -0,0 +1,59 @@ +/* + * 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.filter + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Represents "Filter" as mentioned in the SPEC + * https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-user-userid-filter + */ +@JsonClass(generateAdapter = true) +data class EventFilter( + /** + * The maximum number of events to return. + */ + @Json(name = "limit") val limit: Int? = null, + /** + * A list of senders IDs to include. If this list is absent then all senders are included. + */ + @Json(name = "senders") val senders: List? = null, + /** + * A list of sender IDs to exclude. If this list is absent then no senders are excluded. + * A matching sender will be excluded even if it is listed in the 'senders' filter. + */ + @Json(name = "not_senders") val notSenders: List? = null, + /** + * A list of event types to include. If this list is absent then all event types are included. + * A '*' can be used as a wildcard to match any sequence of characters. + */ + @Json(name = "types") val types: List? = null, + /** + * A list of event types to exclude. If this list is absent then no event types are excluded. + * A matching type will be excluded even if it is listed in the 'types' filter. + * A '*' can be used as a wildcard to match any sequence of characters. + */ + @Json(name = "not_types") val notTypes: List? = null +) { + fun hasData(): Boolean { + return limit != null + || senders != null + || notSenders != null + || types != null + || notTypes != null + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/Filter.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/Filter.kt index fc0472e32f..4b826a00f8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/Filter.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/Filter.kt @@ -17,28 +17,42 @@ package im.vector.matrix.android.internal.session.filter import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.di.MoshiProvider /** - * Represents "Filter" as mentioned in the SPEC + * Class which can be parsed to a filter json string. Used for POST and GET + * Have a look here for further information: * https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-user-userid-filter */ @JsonClass(generateAdapter = true) -data class Filter( - @Json(name = "limit") val limit: Int? = null, - @Json(name = "senders") val senders: List? = null, - @Json(name = "not_senders") val notSenders: List? = null, - @Json(name = "types") val types: List? = null, - @Json(name = "not_types") val notTypes: List? = null, - @Json(name = "rooms") val rooms: List? = null, - @Json(name = "not_rooms") val notRooms: List? = null +internal data class Filter( + /** + * List of event fields to include. If this list is absent then all fields are included. The entries may + * include '.' characters to indicate sub-fields. So ['content.body'] will include the 'body' field of the + * 'content' object. A literal '.' character in a field name may be escaped using a '\'. A server may + * include more fields than were requested. + */ + @Json(name = "event_fields") val eventFields: List? = null, + /** + * The format to use for events. 'client' will return the events in a format suitable for clients. + * 'federation' will return the raw event as received over federation. The default is 'client'. One of: ["client", "federation"] + */ + @Json(name = "event_format") val eventFormat: String? = null, + /** + * The presence updates to include. + */ + @Json(name = "presence") val presence: EventFilter? = null, + /** + * The user account data that isn't associated with rooms to include. + */ + @Json(name = "account_data") val accountData: EventFilter? = null, + /** + * Filters to be applied to room data. + */ + @Json(name = "room") val room: RoomFilter? = null ) { - fun hasData(): Boolean { - return (limit != null - || senders != null - || notSenders != null - || types != null - || notTypes != null - || rooms != null - || notRooms != null) + + fun toJSONString(): String { + return MoshiProvider.providesMoshi().adapter(Filter::class.java).toJson(this) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterApi.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterApi.kt index 092038ee5d..deae2d5b3a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterApi.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterApi.kt @@ -32,7 +32,7 @@ internal interface FilterApi { * @param body the Json representation of a FilterBody object */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/filter") - fun uploadFilter(@Path("userId") userId: String, @Body body: FilterBody): Call + fun uploadFilter(@Path("userId") userId: String, @Body body: Filter): Call /** * Gets a filter with a given filterId from the homeserver @@ -42,6 +42,5 @@ internal interface FilterApi { * @return Filter */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/filter/{filterId}") - fun getFilterById(@Path("userId") userId: String, @Path("filterId") - filterId: String): Call + fun getFilterById(@Path("userId") userId: String, @Path("filterId") filterId: String): Call } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterBody.kt deleted file mode 100644 index 535c66f637..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterBody.kt +++ /dev/null @@ -1,39 +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.matrix.android.internal.session.filter - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import im.vector.matrix.android.internal.di.MoshiProvider - -/** - * Class which can be parsed to a filter json string. Used for POST and GET - * Have a look here for further information: - * https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-user-userid-filter - */ -@JsonClass(generateAdapter = true) -internal data class FilterBody( - @Json(name = "event_fields") val eventFields: List? = null, - @Json(name = "event_format") val eventFormat: String? = null, - @Json(name = "presence") val presence: Filter? = null, - @Json(name = "account_data") val accountData: Filter? = null, - @Json(name = "room") val room: RoomFilter? = null -) { - - fun toJSONString(): String { - return MoshiProvider.providesMoshi().adapter(FilterBody::class.java).toJson(this) - } -} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterFactory.kt index a070759de9..15c57ab1c8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterFactory.kt @@ -20,12 +20,21 @@ import im.vector.matrix.android.api.session.events.model.EventType internal object FilterFactory { - fun createDefaultFilterBody(): FilterBody { - return FilterUtil.enableLazyLoading(FilterBody(), true) + fun createUploadsFilter(numberOfEvents: Int): RoomEventFilter { + return RoomEventFilter( + limit = numberOfEvents, + containsUrl = true, + types = listOf(EventType.MESSAGE), + lazyLoadMembers = true + ) } - fun createRiotFilterBody(): FilterBody { - return FilterBody( + fun createDefaultFilter(): Filter { + return FilterUtil.enableLazyLoading(Filter(), true) + } + + fun createRiotFilter(): Filter { + return Filter( room = RoomFilter( timeline = createRiotTimelineFilter(), state = createRiotStateFilter() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterRepository.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterRepository.kt index d205ea8a87..c558732f44 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterRepository.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterRepository.kt @@ -21,12 +21,12 @@ internal interface FilterRepository { /** * Return true if the filterBody has changed, or need to be sent to the server */ - suspend fun storeFilter(filterBody: FilterBody, roomEventFilter: RoomEventFilter): Boolean + suspend fun storeFilter(filter: Filter, roomEventFilter: RoomEventFilter): Boolean /** * Set the filterId of this filter */ - suspend fun storeFilterId(filterBody: FilterBody, filterId: String) + suspend fun storeFilterId(filter: Filter, filterId: String) /** * Return filter json or filter id diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterResponse.kt index 75e2c23da9..a9bfb70d5e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterResponse.kt @@ -24,5 +24,10 @@ import com.squareup.moshi.JsonClass */ @JsonClass(generateAdapter = true) data class FilterResponse( + /** + * Required. The ID of the filter that was created. Cannot start with a { as this character + * is used to determine if the filter provided is inline JSON or a previously declared + * filter by homeservers on some APIs. + */ @Json(name = "filter_id") val filterId: String ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterUtil.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterUtil.kt index 3f4e61e6b5..53ede5ad45 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterUtil.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterUtil.kt @@ -81,30 +81,30 @@ internal object FilterUtil { } */ /** - * Compute a new filterBody to enable or disable the lazy loading + * Compute a new filter to enable or disable the lazy loading * * - * If lazy loading is on, the filterBody will looks like + * If lazy loading is on, the filter will looks like * {"room":{"state":{"lazy_load_members":true})} * - * @param filterBody filterBody to patch + * @param filter filter to patch * @param useLazyLoading true to enable lazy loading */ - fun enableLazyLoading(filterBody: FilterBody, useLazyLoading: Boolean): FilterBody { + fun enableLazyLoading(filter: Filter, useLazyLoading: Boolean): Filter { if (useLazyLoading) { // Enable lazy loading - return filterBody.copy( - room = filterBody.room?.copy( - state = filterBody.room.state?.copy(lazyLoadMembers = true) + return filter.copy( + room = filter.room?.copy( + state = filter.room.state?.copy(lazyLoadMembers = true) ?: RoomEventFilter(lazyLoadMembers = true) ) ?: RoomFilter(state = RoomEventFilter(lazyLoadMembers = true)) ) } else { - val newRoomEventFilter = filterBody.room?.state?.copy(lazyLoadMembers = null)?.takeIf { it.hasData() } - val newRoomFilter = filterBody.room?.copy(state = newRoomEventFilter)?.takeIf { it.hasData() } + val newRoomEventFilter = filter.room?.state?.copy(lazyLoadMembers = null)?.takeIf { it.hasData() } + val newRoomFilter = filter.room?.copy(state = newRoomEventFilter)?.takeIf { it.hasData() } - return filterBody.copy( + return filter.copy( room = newRoomFilter ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/RoomEventFilter.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/RoomEventFilter.kt index 9cdccc5c8b..81e7b672b4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/RoomEventFilter.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/RoomEventFilter.kt @@ -25,14 +25,46 @@ import im.vector.matrix.android.internal.di.MoshiProvider */ @JsonClass(generateAdapter = true) data class RoomEventFilter( - @Json(name = "limit") var limit: Int? = null, + /** + * The maximum number of events to return. + */ + @Json(name = "limit") val limit: Int? = null, + /** + * A list of sender IDs to exclude. If this list is absent then no senders are excluded. A matching sender will + * be excluded even if it is listed in the 'senders' filter. + */ @Json(name = "not_senders") val notSenders: List? = null, + /** + * A list of event types to exclude. If this list is absent then no event types are excluded. A matching type will + * be excluded even if it is listed in the 'types' filter. A '*' can be used as a wildcard to match any sequence of characters. + */ @Json(name = "not_types") val notTypes: List? = null, + /** + * A list of senders IDs to include. If this list is absent then all senders are included. + */ @Json(name = "senders") val senders: List? = null, + /** + * A list of event types to include. If this list is absent then all event types are included. A '*' can be used as + * a wildcard to match any sequence of characters. + */ @Json(name = "types") val types: List? = null, + /** + * A list of room IDs to include. If this list is absent then all rooms are included. + */ @Json(name = "rooms") val rooms: List? = null, + /** + * A list of room IDs to exclude. If this list is absent then no rooms are excluded. A matching room will be excluded + * even if it is listed in the 'rooms' filter. + */ @Json(name = "not_rooms") val notRooms: List? = null, + /** + * If true, includes only events with a url key in their content. If false, excludes those events. If omitted, url + * key is not considered for filtering. + */ @Json(name = "contains_url") val containsUrl: Boolean? = null, + /** + * If true, enables lazy-loading of membership events. See Lazy-loading room members for more information. Defaults to false. + */ @Json(name = "lazy_load_members") val lazyLoadMembers: Boolean? = null ) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/RoomFilter.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/RoomFilter.kt index 3109763570..e79a0a624e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/RoomFilter.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/RoomFilter.kt @@ -24,12 +24,37 @@ import com.squareup.moshi.JsonClass */ @JsonClass(generateAdapter = true) data class RoomFilter( + /** + * A list of room IDs to exclude. If this list is absent then no rooms are excluded. + * A matching room will be excluded even if it is listed in the 'rooms' filter. + * This filter is applied before the filters in ephemeral, state, timeline or account_data + */ @Json(name = "not_rooms") val notRooms: List? = null, + /** + * A list of room IDs to include. If this list is absent then all rooms are included. + * This filter is applied before the filters in ephemeral, state, timeline or account_data + */ @Json(name = "rooms") val rooms: List? = null, + /** + * The events that aren't recorded in the room history, e.g. typing and receipts, to include for rooms. + */ @Json(name = "ephemeral") val ephemeral: RoomEventFilter? = null, + /** + * Include rooms that the user has left in the sync, default false + */ @Json(name = "include_leave") val includeLeave: Boolean? = null, + /** + * The state events to include for rooms. + * Developer remark: StateFilter is exactly the same than RoomEventFilter + */ @Json(name = "state") val state: RoomEventFilter? = null, + /** + * The message and state update events to include for rooms. + */ @Json(name = "timeline") val timeline: RoomEventFilter? = null, + /** + * The per user account data to include for rooms. + */ @Json(name = "account_data") val accountData: RoomEventFilter? = null ) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/CapabilitiesAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/CapabilitiesAPI.kt index f37bbfe798..880a8fbc31 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/CapabilitiesAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/CapabilitiesAPI.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.internal.session.homeserver +import im.vector.matrix.android.api.auth.data.Versions import im.vector.matrix.android.internal.network.NetworkConstants import retrofit2.Call import retrofit2.http.GET @@ -38,5 +39,11 @@ internal interface CapabilitiesAPI { * Request the versions */ @GET(NetworkConstants.URI_API_PREFIX_PATH_ + "versions") - fun getVersions(): Call + fun getVersions(): Call + + /** + * Ping the homeserver. We do not care about the returned data, so there is no use to parse them + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_ + "versions") + fun ping(): Call } 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 9f068381f0..be5b0d3949 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 @@ -17,9 +17,14 @@ package im.vector.matrix.android.internal.session.homeserver import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.auth.data.Versions +import im.vector.matrix.android.api.auth.data.isLoginAndRegistrationSupportedBySdk +import im.vector.matrix.android.api.auth.wellknown.WellknownResult import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities +import im.vector.matrix.android.internal.wellknown.GetWellknownTask import im.vector.matrix.android.internal.database.model.HomeServerCapabilitiesEntity import im.vector.matrix.android.internal.database.query.getOrCreate +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 im.vector.matrix.android.internal.util.awaitTransaction @@ -32,7 +37,10 @@ internal interface GetHomeServerCapabilitiesTask : Task internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( private val capabilitiesAPI: CapabilitiesAPI, private val monarchy: Monarchy, - private val eventBus: EventBus + private val eventBus: EventBus, + private val getWellknownTask: GetWellknownTask, + @UserId + private val userId: String ) : GetHomeServerCapabilitiesTask { override suspend fun execute(params: Unit) { @@ -47,29 +55,54 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( return } - val uploadCapabilities = executeRequest(eventBus) { - apiCall = capabilitiesAPI.getUploadCapabilities() - } - val capabilities = runCatching { executeRequest(eventBus) { apiCall = capabilitiesAPI.getCapabilities() } }.getOrNull() - // TODO Add other call here (get version, etc.) + val uploadCapabilities = runCatching { + executeRequest(eventBus) { + apiCall = capabilitiesAPI.getUploadCapabilities() + } + }.getOrNull() - insertInDb(capabilities, uploadCapabilities) + val versions = runCatching { + executeRequest(null) { + apiCall = capabilitiesAPI.getVersions() + } + }.getOrNull() + + val wellknownResult = runCatching { + getWellknownTask.execute(GetWellknownTask.Params(userId)) + }.getOrNull() + + insertInDb(capabilities, uploadCapabilities, versions, wellknownResult) } - private suspend fun insertInDb(getCapabilitiesResult: GetCapabilitiesResult?, getUploadCapabilitiesResult: GetUploadCapabilitiesResult) { + private suspend fun insertInDb(getCapabilitiesResult: GetCapabilitiesResult?, + getUploadCapabilitiesResult: GetUploadCapabilitiesResult?, + getVersionResult: Versions?, + getWellknownResult: WellknownResult?) { monarchy.awaitTransaction { realm -> val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity.getOrCreate(realm) - homeServerCapabilitiesEntity.canChangePassword = getCapabilitiesResult.canChangePassword() + if (getCapabilitiesResult != null) { + homeServerCapabilitiesEntity.canChangePassword = getCapabilitiesResult.canChangePassword() + } - homeServerCapabilitiesEntity.maxUploadFileSize = getUploadCapabilitiesResult.maxUploadSize - ?: HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN + if (getUploadCapabilitiesResult != null) { + homeServerCapabilitiesEntity.maxUploadFileSize = getUploadCapabilitiesResult.maxUploadSize + ?: HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN + } + + if (getVersionResult != null) { + homeServerCapabilitiesEntity.lastVersionIdentityServerSupported = getVersionResult.isLoginAndRegistrationSupportedBySdk() + } + + if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) { + homeServerCapabilitiesEntity.defaultIdentityServerUrl = getWellknownResult.identityServerUrl + } homeServerCapabilitiesEntity.lastUpdatedTimestamp = Date().time } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/GetCapabilitiesResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/GetCapabilitiesResult.kt index ffaa998789..9a625571a8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/GetCapabilitiesResult.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/GetCapabilitiesResult.kt @@ -53,6 +53,6 @@ internal data class ChangePassword( ) // The spec says: If not present, the client should assume that password changes are possible via the API -internal fun GetCapabilitiesResult?.canChangePassword(): Boolean { - return this?.capabilities?.changePassword?.enabled.orTrue() +internal fun GetCapabilitiesResult.canChangePassword(): Boolean { + return capabilities?.changePassword?.enabled.orTrue() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeServerCapabilitiesModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeServerCapabilitiesModule.kt index f2b249ab0c..91ec3fe305 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeServerCapabilitiesModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeServerCapabilitiesModule.kt @@ -20,9 +20,10 @@ import dagger.Binds import dagger.Module import dagger.Provides import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.wellknown.WellknownModule import retrofit2.Retrofit -@Module +@Module(includes = [WellknownModule::class]) internal abstract class HomeServerCapabilitiesModule { @Module diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeServerPinger.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeServerPinger.kt index e220c0064d..705deb4e57 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeServerPinger.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeServerPinger.kt @@ -35,7 +35,7 @@ internal class HomeServerPinger @Inject constructor(private val taskExecutor: Ta suspend fun canReachHomeServer(): Boolean { return try { executeRequest(null) { - apiCall = capabilitiesAPI.getVersions() + apiCall = capabilitiesAPI.ping() } true } catch (throwable: Throwable) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/DefaultIdentityService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/DefaultIdentityService.kt new file mode 100644 index 0000000000..1a271e659e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/DefaultIdentityService.kt @@ -0,0 +1,341 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import dagger.Lazy +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.auth.data.SessionParams +import im.vector.matrix.android.api.extensions.tryThis +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.failure.MatrixError +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService +import im.vector.matrix.android.api.session.identity.FoundThreePid +import im.vector.matrix.android.api.session.identity.IdentityService +import im.vector.matrix.android.api.session.identity.IdentityServiceError +import im.vector.matrix.android.api.session.identity.IdentityServiceListener +import im.vector.matrix.android.api.session.identity.SharedState +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.api.util.NoOpCancellable +import im.vector.matrix.android.internal.di.AuthenticatedIdentity +import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.network.RetrofitFactory +import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.session.identity.data.IdentityStore +import im.vector.matrix.android.internal.session.identity.todelete.AccountDataDataSource +import im.vector.matrix.android.internal.session.identity.todelete.observeNotNull +import im.vector.matrix.android.internal.session.openid.GetOpenIdTokenTask +import im.vector.matrix.android.internal.session.profile.BindThreePidsTask +import im.vector.matrix.android.internal.session.profile.UnbindThreePidsTask +import im.vector.matrix.android.internal.session.sync.model.accountdata.IdentityServerContent +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData +import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.launchToCallback +import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers +import im.vector.matrix.android.internal.util.ensureProtocol +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import timber.log.Timber +import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection + +@SessionScope +internal class DefaultIdentityService @Inject constructor( + private val identityStore: IdentityStore, + private val getOpenIdTokenTask: GetOpenIdTokenTask, + private val identityBulkLookupTask: IdentityBulkLookupTask, + private val identityRegisterTask: IdentityRegisterTask, + private val identityPingTask: IdentityPingTask, + private val identityDisconnectTask: IdentityDisconnectTask, + private val identityRequestTokenForBindingTask: IdentityRequestTokenForBindingTask, + @Unauthenticated + private val unauthenticatedOkHttpClient: Lazy, + @AuthenticatedIdentity + private val okHttpClient: Lazy, + private val retrofitFactory: RetrofitFactory, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val updateUserAccountDataTask: UpdateUserAccountDataTask, + private val bindThreePidsTask: BindThreePidsTask, + private val submitTokenForBindingTask: IdentitySubmitTokenForBindingTask, + private val unbindThreePidsTask: UnbindThreePidsTask, + private val identityApiProvider: IdentityApiProvider, + private val accountDataDataSource: AccountDataDataSource, + private val homeServerCapabilitiesService: HomeServerCapabilitiesService, + private val sessionParams: SessionParams, + private val taskExecutor: TaskExecutor +) : IdentityService { + + private val lifecycleOwner: LifecycleOwner = LifecycleOwner { lifecycleRegistry } + private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(lifecycleOwner) + + private val listeners = mutableSetOf() + + fun start() { + lifecycleRegistry.currentState = Lifecycle.State.STARTED + // Observe the account data change + accountDataDataSource + .getLiveAccountDataEvent(UserAccountData.TYPE_IDENTITY_SERVER) + .observeNotNull(lifecycleOwner) { + notifyIdentityServerUrlChange(it.getOrNull()?.content?.toModel()?.baseUrl) + } + + // Init identityApi + updateIdentityAPI(identityStore.getIdentityData()?.identityServerUrl) + } + + private fun notifyIdentityServerUrlChange(baseUrl: String?) { + // This is maybe not a real change (echo of account data we are just setting) + if (identityStore.getIdentityData()?.identityServerUrl == baseUrl) { + Timber.d("Echo of local identity server url change, or no change") + } else { + // Url has changed, we have to reset our store, update internal configuration and notify listeners + identityStore.setUrl(baseUrl) + updateIdentityAPI(baseUrl) + listeners.toList().forEach { tryThis { it.onIdentityServerChange() } } + } + } + + fun stop() { + lifecycleRegistry.currentState = Lifecycle.State.DESTROYED + } + + /** + * First return the identity server provided during login phase. + * If null, provide the one in wellknown configuration of the homeserver + * Else return null + */ + override fun getDefaultIdentityServer(): String? { + return sessionParams.defaultIdentityServerUrl + ?.takeIf { it.isNotEmpty() } + ?: homeServerCapabilitiesService.getHomeServerCapabilities().defaultIdentityServerUrl + } + + override fun getCurrentIdentityServerUrl(): String? { + return identityStore.getIdentityData()?.identityServerUrl + } + + override fun startBindThreePid(threePid: ThreePid, callback: MatrixCallback): Cancelable { + if (homeServerCapabilitiesService.getHomeServerCapabilities().lastVersionIdentityServerSupported.not()) { + callback.onFailure(IdentityServiceError.OutdatedHomeServer) + return NoOpCancellable + } + + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + identityRequestTokenForBindingTask.execute(IdentityRequestTokenForBindingTask.Params(threePid, false)) + } + } + + override fun cancelBindThreePid(threePid: ThreePid, callback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + identityStore.deletePendingBinding(threePid) + } + } + + override fun sendAgainValidationCode(threePid: ThreePid, callback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + identityRequestTokenForBindingTask.execute(IdentityRequestTokenForBindingTask.Params(threePid, true)) + } + } + + override fun finalizeBindThreePid(threePid: ThreePid, callback: MatrixCallback): Cancelable { + if (homeServerCapabilitiesService.getHomeServerCapabilities().lastVersionIdentityServerSupported.not()) { + callback.onFailure(IdentityServiceError.OutdatedHomeServer) + return NoOpCancellable + } + + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + bindThreePidsTask.execute(BindThreePidsTask.Params(threePid)) + } + } + + override fun submitValidationToken(threePid: ThreePid, code: String, callback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + submitTokenForBindingTask.execute(IdentitySubmitTokenForBindingTask.Params(threePid, code)) + } + } + + override fun unbindThreePid(threePid: ThreePid, callback: MatrixCallback): Cancelable { + if (homeServerCapabilitiesService.getHomeServerCapabilities().lastVersionIdentityServerSupported.not()) { + callback.onFailure(IdentityServiceError.OutdatedHomeServer) + return NoOpCancellable + } + + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + unbindThreePidsTask.execute(UnbindThreePidsTask.Params(threePid)) + } + } + + override fun isValidIdentityServer(url: String, callback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java) + + identityPingTask.execute(IdentityPingTask.Params(api)) + } + } + + override fun disconnect(callback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + identityDisconnectTask.execute(Unit) + + identityStore.setUrl(null) + updateIdentityAPI(null) + updateAccountData(null) + } + } + + override fun setNewIdentityServer(url: String, callback: MatrixCallback): Cancelable { + val urlCandidate = url.ensureProtocol() + + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + val current = getCurrentIdentityServerUrl() + if (urlCandidate == current) { + // Nothing to do + Timber.d("Same URL, nothing to do") + } else { + // Disconnect previous one if any, first, because the token will change. + // In case of error when configuring the new identity server, this is not a big deal, + // we will ask for a new token on the previous Identity server + runCatching { identityDisconnectTask.execute(Unit) } + .onFailure { Timber.w(it, "Unable to disconnect identity server") } + + // Try to get a token + val token = getNewIdentityServerToken(urlCandidate) + + identityStore.setUrl(urlCandidate) + identityStore.setToken(token) + updateIdentityAPI(urlCandidate) + + updateAccountData(urlCandidate) + } + urlCandidate + } + } + + private suspend fun updateAccountData(url: String?) { + // Also notify the listener + withContext(coroutineDispatchers.main) { + listeners.toList().forEach { tryThis { it.onIdentityServerChange() } } + } + + updateUserAccountDataTask.execute(UpdateUserAccountDataTask.IdentityParams( + identityContent = IdentityServerContent(baseUrl = url) + )) + } + + override fun lookUp(threePids: List, callback: MatrixCallback>): Cancelable { + if (threePids.isEmpty()) { + callback.onSuccess(emptyList()) + return NoOpCancellable + } + + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + lookUpInternal(true, threePids) + } + } + + override fun getShareStatus(threePids: List, callback: MatrixCallback>): Cancelable { + if (threePids.isEmpty()) { + callback.onSuccess(emptyMap()) + return NoOpCancellable + } + + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + val lookupResult = lookUpInternal(true, threePids) + + threePids.associateWith { threePid -> + // If not in lookup result, check if there is a pending binding + if (lookupResult.firstOrNull { it.threePid == threePid } == null) { + if (identityStore.getPendingBinding(threePid) == null) { + SharedState.NOT_SHARED + } else { + SharedState.BINDING_IN_PROGRESS + } + } else { + SharedState.SHARED + } + } + } + } + + private suspend fun lookUpInternal(canRetry: Boolean, threePids: List): List { + ensureToken() + + return try { + identityBulkLookupTask.execute(IdentityBulkLookupTask.Params(threePids)) + } catch (throwable: Throwable) { + // Refresh token? + when { + throwable.isInvalidToken() && canRetry -> { + identityStore.setToken(null) + lookUpInternal(false, threePids) + } + throwable.isTermsNotSigned() -> throw IdentityServiceError.TermsNotSignedException + else -> throw throwable + } + } + } + + private suspend fun ensureToken() { + val identityData = identityStore.getIdentityData() ?: throw IdentityServiceError.NoIdentityServerConfigured + val url = identityData.identityServerUrl ?: throw IdentityServiceError.NoIdentityServerConfigured + + if (identityData.token == null) { + // Try to get a token + val token = getNewIdentityServerToken(url) + identityStore.setToken(token) + } + } + + private suspend fun getNewIdentityServerToken(url: String): String { + val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java) + + val openIdToken = getOpenIdTokenTask.execute(Unit) + val token = identityRegisterTask.execute(IdentityRegisterTask.Params(api, openIdToken)) + + return token.token + } + + override fun addListener(listener: IdentityServiceListener) { + listeners.add(listener) + } + + override fun removeListener(listener: IdentityServiceListener) { + listeners.remove(listener) + } + + private fun updateIdentityAPI(url: String?) { + identityApiProvider.identityApi = url + ?.let { retrofitFactory.create(okHttpClient, it) } + ?.create(IdentityAPI::class.java) + } +} + +private fun Throwable.isInvalidToken(): Boolean { + return this is Failure.ServerError + && httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */ +} + +private fun Throwable.isTermsNotSigned(): Boolean { + return this is Failure.ServerError + && httpCode == HttpsURLConnection.HTTP_FORBIDDEN /* 403 */ + && error.code == MatrixError.M_TERMS_NOT_SIGNED +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAPI.kt new file mode 100644 index 0000000000..ca361b4265 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAPI.kt @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity + +import im.vector.matrix.android.internal.auth.registration.SuccessResult +import im.vector.matrix.android.internal.network.NetworkConstants +import im.vector.matrix.android.internal.session.identity.model.IdentityAccountResponse +import im.vector.matrix.android.internal.session.identity.model.IdentityHashDetailResponse +import im.vector.matrix.android.internal.session.identity.model.IdentityLookUpParams +import im.vector.matrix.android.internal.session.identity.model.IdentityLookUpResponse +import im.vector.matrix.android.internal.session.identity.model.IdentityRequestOwnershipParams +import im.vector.matrix.android.internal.session.identity.model.IdentityRequestTokenForEmailBody +import im.vector.matrix.android.internal.session.identity.model.IdentityRequestTokenForMsisdnBody +import im.vector.matrix.android.internal.session.identity.model.IdentityRequestTokenResponse +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path + +/** + * Ref: https://matrix.org/docs/spec/identity_service/latest + * This contain the requests which need an identity server token + */ +internal interface IdentityAPI { + /** + * Gets information about what user owns the access token used in the request. + * Will return a 403 for when terms are not signed + * Ref: https://matrix.org/docs/spec/identity_service/latest#get-matrix-identity-v2-account + */ + @GET(NetworkConstants.URI_IDENTITY_PATH_V2 + "account") + fun getAccount(): Call + + /** + * Logs out the access token, preventing it from being used to authenticate future requests to the server. + */ + @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "account/logout") + fun logout(): Call + + /** + * Request the hash detail to request a bunch of 3PIDs + * Ref: https://matrix.org/docs/spec/identity_service/latest#get-matrix-identity-v2-hash-details + */ + @GET(NetworkConstants.URI_IDENTITY_PATH_V2 + "hash_details") + fun hashDetails(): Call + + /** + * Request a bunch of 3PIDs + * Ref: https://matrix.org/docs/spec/identity_service/latest#post-matrix-identity-v2-lookup + * + * @param body the body request + */ + @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "lookup") + fun lookup(@Body body: IdentityLookUpParams): Call + + /** + * Create a session to change the bind status of an email to an identity server + * The identity server will also send an email + * + * @param body + * @return the sid + */ + @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "validate/email/requestToken") + fun requestTokenToBindEmail(@Body body: IdentityRequestTokenForEmailBody): Call + + /** + * Create a session to change the bind status of an phone number to an identity server + * The identity server will also send an SMS on the ThreePid provided + * + * @param body + * @return the sid + */ + @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "validate/msisdn/requestToken") + fun requestTokenToBindMsisdn(@Body body: IdentityRequestTokenForMsisdnBody): Call + + /** + * Validate ownership of an email address, or a phone number. + * Ref: + * - https://matrix.org/docs/spec/identity_service/latest#post-matrix-identity-v2-validate-msisdn-submittoken + * - https://matrix.org/docs/spec/identity_service/latest#post-matrix-identity-v2-validate-email-submittoken + */ + @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "validate/{medium}/submitToken") + fun submitToken(@Path("medium") medium: String, @Body body: IdentityRequestOwnershipParams): Call +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAccessTokenProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAccessTokenProvider.kt new file mode 100644 index 0000000000..ee2f18c767 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAccessTokenProvider.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity + +import im.vector.matrix.android.internal.network.token.AccessTokenProvider +import im.vector.matrix.android.internal.session.identity.data.IdentityStore +import javax.inject.Inject + +internal class IdentityAccessTokenProvider @Inject constructor( + private val identityStore: IdentityStore +) : AccessTokenProvider { + override fun getToken() = identityStore.getIdentityData()?.token +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityApiProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityApiProvider.kt new file mode 100644 index 0000000000..3262a56398 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityApiProvider.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity + +import im.vector.matrix.android.internal.session.SessionScope +import javax.inject.Inject + +@SessionScope +internal class IdentityApiProvider @Inject constructor() { + + var identityApi: IdentityAPI? = null +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAuthAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAuthAPI.kt new file mode 100644 index 0000000000..04abf5fe6c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAuthAPI.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity + +import im.vector.matrix.android.internal.network.NetworkConstants +import im.vector.matrix.android.internal.session.identity.model.IdentityRegisterResponse +import im.vector.matrix.android.internal.session.openid.RequestOpenIdTokenResponse +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST + +/** + * Ref: https://matrix.org/docs/spec/identity_service/latest + * This contain the requests which do not need an identity server token + */ +internal interface IdentityAuthAPI { + + /** + * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery + * Simple ping call to check if server exists and is alive + * + * Ref: https://matrix.org/docs/spec/identity_service/unstable#status-check + * https://matrix.org/docs/spec/identity_service/latest#get-matrix-identity-v2 + * + * @return 200 in case of success + */ + @GET(NetworkConstants.URI_IDENTITY_PREFIX_PATH) + fun ping(): Call + + /** + * Ping v1 will be used to check outdated Identity server + */ + @GET("_matrix/identity/api/v1") + fun pingV1(): Call + + /** + * Exchanges an OpenID token from the homeserver for an access token to access the identity server. + * The request body is the same as the values returned by /openid/request_token in the Client-Server API. + */ + @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "account/register") + fun register(@Body openIdToken: RequestOpenIdTokenResponse): Call +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityBulkLookupTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityBulkLookupTask.kt new file mode 100644 index 0000000000..9f1579af60 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityBulkLookupTask.kt @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity + +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.failure.MatrixError +import im.vector.matrix.android.api.session.identity.FoundThreePid +import im.vector.matrix.android.api.session.identity.IdentityServiceError +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.matrix.android.api.session.identity.toMedium +import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments.base64ToBase64Url +import im.vector.matrix.android.internal.crypto.tools.withOlmUtility +import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.identity.data.IdentityStore +import im.vector.matrix.android.internal.session.identity.model.IdentityHashDetailResponse +import im.vector.matrix.android.internal.session.identity.model.IdentityLookUpParams +import im.vector.matrix.android.internal.session.identity.model.IdentityLookUpResponse +import im.vector.matrix.android.internal.task.Task +import java.util.Locale +import javax.inject.Inject + +internal interface IdentityBulkLookupTask : Task> { + data class Params( + val threePids: List + ) +} + +internal class DefaultIdentityBulkLookupTask @Inject constructor( + private val identityApiProvider: IdentityApiProvider, + private val identityStore: IdentityStore, + @UserId private val userId: String +) : IdentityBulkLookupTask { + + override suspend fun execute(params: IdentityBulkLookupTask.Params): List { + val identityAPI = getIdentityApiAndEnsureTerms(identityApiProvider, userId) + val identityData = identityStore.getIdentityData() ?: throw IdentityServiceError.NoIdentityServerConfigured + val pepper = identityData.hashLookupPepper + val hashDetailResponse = if (pepper == null) { + // We need to fetch the hash details first + fetchAndStoreHashDetails(identityAPI) + } else { + IdentityHashDetailResponse(pepper, identityData.hashLookupAlgorithm) + } + + if (hashDetailResponse.algorithms.contains("sha256").not()) { + // TODO We should ask the user if he is ok to send their 3Pid in clear, but for the moment we do not do it + // Also, what we have in cache could be outdated, the identity server maybe now supports sha256 + throw IdentityServiceError.BulkLookupSha256NotSupported + } + + val hashedAddresses = withOlmUtility { olmUtility -> + params.threePids.map { threePid -> + base64ToBase64Url( + olmUtility.sha256(threePid.value.toLowerCase(Locale.ROOT) + + " " + threePid.toMedium() + " " + hashDetailResponse.pepper) + ) + } + } + + val identityLookUpV2Response = lookUpInternal(identityAPI, hashedAddresses, hashDetailResponse, true) + + // Convert back to List + return handleSuccess(params.threePids, hashedAddresses, identityLookUpV2Response) + } + + private suspend fun lookUpInternal(identityAPI: IdentityAPI, + hashedAddresses: List, + hashDetailResponse: IdentityHashDetailResponse, + canRetry: Boolean): IdentityLookUpResponse { + return try { + executeRequest(null) { + apiCall = identityAPI.lookup(IdentityLookUpParams( + hashedAddresses, + IdentityHashDetailResponse.ALGORITHM_SHA256, + hashDetailResponse.pepper + )) + } + } catch (failure: Throwable) { + // Catch invalid hash pepper and retry + if (canRetry && failure is Failure.ServerError && failure.error.code == MatrixError.M_INVALID_PEPPER) { + // This is not documented, by the error can contain the new pepper! + if (!failure.error.newLookupPepper.isNullOrEmpty()) { + // Store it and use it right now + hashDetailResponse.copy(pepper = failure.error.newLookupPepper) + .also { identityStore.setHashDetails(it) } + .let { lookUpInternal(identityAPI, hashedAddresses, it, false /* Avoid infinite loop */) } + } else { + // Retrieve the new hash details + val newHashDetailResponse = fetchAndStoreHashDetails(identityAPI) + + if (hashDetailResponse.algorithms.contains(IdentityHashDetailResponse.ALGORITHM_SHA256).not()) { + // TODO We should ask the user if he is ok to send their 3Pid in clear, but for the moment we do not do it + // Also, what we have in cache is maybe outdated, the identity server maybe now support sha256 + throw IdentityServiceError.BulkLookupSha256NotSupported + } + + lookUpInternal(identityAPI, hashedAddresses, newHashDetailResponse, false /* Avoid infinite loop */) + } + } else { + // Other error + throw failure + } + } + } + + private suspend fun fetchAndStoreHashDetails(identityAPI: IdentityAPI): IdentityHashDetailResponse { + return executeRequest(null) { + apiCall = identityAPI.hashDetails() + } + .also { identityStore.setHashDetails(it) } + } + + private fun handleSuccess(threePids: List, hashedAddresses: List, identityLookUpResponse: IdentityLookUpResponse): List { + return identityLookUpResponse.mappings.keys.map { hashedAddress -> + FoundThreePid(threePids[hashedAddresses.indexOf(hashedAddress)], identityLookUpResponse.mappings[hashedAddress] ?: error("")) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityDisconnectTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityDisconnectTask.kt new file mode 100644 index 0000000000..abed3962bc --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityDisconnectTask.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity + +import im.vector.matrix.android.api.session.identity.IdentityServiceError +import im.vector.matrix.android.internal.di.AuthenticatedIdentity +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.network.token.AccessTokenProvider +import im.vector.matrix.android.internal.task.Task +import timber.log.Timber +import javax.inject.Inject + +internal interface IdentityDisconnectTask : Task + +internal class DefaultIdentityDisconnectTask @Inject constructor( + private val identityApiProvider: IdentityApiProvider, + @AuthenticatedIdentity + private val accessTokenProvider: AccessTokenProvider +) : IdentityDisconnectTask { + + override suspend fun execute(params: Unit) { + val identityAPI = identityApiProvider.identityApi ?: throw IdentityServiceError.NoIdentityServerConfigured + + // Ensure we have a token. + // We can have an identity server configured, but no token yet. + if (accessTokenProvider.getToken() == null) { + Timber.d("No token to disconnect identity server.") + return + } + + executeRequest(null) { + apiCall = identityAPI.logout() + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityModule.kt new file mode 100644 index 0000000000..7a5790788b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityModule.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity + +import dagger.Binds +import dagger.Module +import dagger.Provides +import im.vector.matrix.android.internal.database.RealmKeysUtils +import im.vector.matrix.android.internal.di.AuthenticatedIdentity +import im.vector.matrix.android.internal.di.IdentityDatabase +import im.vector.matrix.android.internal.di.SessionFilesDirectory +import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.di.UserMd5 +import im.vector.matrix.android.internal.network.httpclient.addAccessTokenInterceptor +import im.vector.matrix.android.internal.network.token.AccessTokenProvider +import im.vector.matrix.android.internal.session.SessionModule +import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.session.identity.data.IdentityStore +import im.vector.matrix.android.internal.session.identity.db.IdentityRealmModule +import im.vector.matrix.android.internal.session.identity.db.RealmIdentityStore +import io.realm.RealmConfiguration +import okhttp3.OkHttpClient +import java.io.File + +@Module +internal abstract class IdentityModule { + + @Module + companion object { + @JvmStatic + @Provides + @SessionScope + @AuthenticatedIdentity + fun providesOkHttpClient(@Unauthenticated okHttpClient: OkHttpClient, + @AuthenticatedIdentity accessTokenProvider: AccessTokenProvider): OkHttpClient { + return okHttpClient.addAccessTokenInterceptor(accessTokenProvider) + } + + @JvmStatic + @Provides + @IdentityDatabase + @SessionScope + fun providesIdentityRealmConfiguration(realmKeysUtils: RealmKeysUtils, + @SessionFilesDirectory directory: File, + @UserMd5 userMd5: String): RealmConfiguration { + return RealmConfiguration.Builder() + .directory(directory) + .name("matrix-sdk-identity.realm") + .apply { + realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5)) + } + .modules(IdentityRealmModule()) + .build() + } + } + + @Binds + @AuthenticatedIdentity + abstract fun bindAccessTokenProvider(provider: IdentityAccessTokenProvider): AccessTokenProvider + + @Binds + abstract fun bindIdentityStore(store: RealmIdentityStore): IdentityStore + + @Binds + abstract fun bindIdentityPingTask(task: DefaultIdentityPingTask): IdentityPingTask + + @Binds + abstract fun bindIdentityRegisterTask(task: DefaultIdentityRegisterTask): IdentityRegisterTask + + @Binds + abstract fun bindIdentityRequestTokenForBindingTask(task: DefaultIdentityRequestTokenForBindingTask): IdentityRequestTokenForBindingTask + + @Binds + abstract fun bindIdentitySubmitTokenForBindingTask(task: DefaultIdentitySubmitTokenForBindingTask): IdentitySubmitTokenForBindingTask + + @Binds + abstract fun bindIdentityBulkLookupTask(task: DefaultIdentityBulkLookupTask): IdentityBulkLookupTask + + @Binds + abstract fun bindIdentityDisconnectTask(task: DefaultIdentityDisconnectTask): IdentityDisconnectTask +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityPingTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityPingTask.kt new file mode 100644 index 0000000000..50a36097a1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityPingTask.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity + +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.session.identity.IdentityServiceError +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task +import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection + +internal interface IdentityPingTask : Task { + data class Params( + val identityAuthAPI: IdentityAuthAPI + ) +} + +internal class DefaultIdentityPingTask @Inject constructor() : IdentityPingTask { + + override suspend fun execute(params: IdentityPingTask.Params) { + try { + executeRequest(null) { + apiCall = params.identityAuthAPI.ping() + } + } catch (throwable: Throwable) { + if (throwable is Failure.ServerError && throwable.httpCode == HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) { + // Check if API v1 is available + executeRequest(null) { + apiCall = params.identityAuthAPI.pingV1() + } + // API V1 is responding, but not V2 -> Outdated + throw IdentityServiceError.OutdatedIdentityServer + } else { + throw throwable + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityRegisterTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityRegisterTask.kt new file mode 100644 index 0000000000..c72e364ef8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityRegisterTask.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity + +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.identity.model.IdentityRegisterResponse +import im.vector.matrix.android.internal.session.openid.RequestOpenIdTokenResponse +import im.vector.matrix.android.internal.task.Task +import javax.inject.Inject + +internal interface IdentityRegisterTask : Task { + data class Params( + val identityAuthAPI: IdentityAuthAPI, + val openIdTokenResponse: RequestOpenIdTokenResponse + ) +} + +internal class DefaultIdentityRegisterTask @Inject constructor() : IdentityRegisterTask { + + override suspend fun execute(params: IdentityRegisterTask.Params): IdentityRegisterResponse { + return executeRequest(null) { + apiCall = params.identityAuthAPI.register(params.openIdTokenResponse) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityRequestTokenForBindingTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityRequestTokenForBindingTask.kt new file mode 100644 index 0000000000..313f5f6662 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityRequestTokenForBindingTask.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity + +import im.vector.matrix.android.api.session.identity.IdentityServiceError +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.matrix.android.api.session.identity.getCountryCode +import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.identity.data.IdentityPendingBinding +import im.vector.matrix.android.internal.session.identity.data.IdentityStore +import im.vector.matrix.android.internal.session.identity.model.IdentityRequestTokenForEmailBody +import im.vector.matrix.android.internal.session.identity.model.IdentityRequestTokenForMsisdnBody +import im.vector.matrix.android.internal.session.identity.model.IdentityRequestTokenResponse +import im.vector.matrix.android.internal.task.Task +import java.util.UUID +import javax.inject.Inject + +internal interface IdentityRequestTokenForBindingTask : Task { + data class Params( + val threePid: ThreePid, + // True to request the identity server to send again the email or the SMS + val sendAgain: Boolean + ) +} + +internal class DefaultIdentityRequestTokenForBindingTask @Inject constructor( + private val identityApiProvider: IdentityApiProvider, + private val identityStore: IdentityStore, + @UserId private val userId: String +) : IdentityRequestTokenForBindingTask { + + override suspend fun execute(params: IdentityRequestTokenForBindingTask.Params) { + val identityAPI = getIdentityApiAndEnsureTerms(identityApiProvider, userId) + + val identityPendingBinding = identityStore.getPendingBinding(params.threePid) + + if (params.sendAgain && identityPendingBinding == null) { + throw IdentityServiceError.NoCurrentBindingError + } + + val clientSecret = identityPendingBinding?.clientSecret ?: UUID.randomUUID().toString() + val sendAttempt = identityPendingBinding?.sendAttempt?.inc() ?: 1 + + val tokenResponse = executeRequest(null) { + apiCall = when (params.threePid) { + is ThreePid.Email -> identityAPI.requestTokenToBindEmail(IdentityRequestTokenForEmailBody( + clientSecret = clientSecret, + sendAttempt = sendAttempt, + email = params.threePid.email + )) + is ThreePid.Msisdn -> { + identityAPI.requestTokenToBindMsisdn(IdentityRequestTokenForMsisdnBody( + clientSecret = clientSecret, + sendAttempt = sendAttempt, + phoneNumber = params.threePid.msisdn, + countryCode = params.threePid.getCountryCode() + )) + } + } + } + + // Store client secret, send attempt and sid + identityStore.storePendingBinding( + params.threePid, + IdentityPendingBinding( + clientSecret = clientSecret, + sendAttempt = sendAttempt, + sid = tokenResponse.sid + ) + ) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentitySubmitTokenForBindingTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentitySubmitTokenForBindingTask.kt new file mode 100644 index 0000000000..fae1dd1eba --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentitySubmitTokenForBindingTask.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity + +import im.vector.matrix.android.api.session.identity.IdentityServiceError +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.matrix.android.api.session.identity.toMedium +import im.vector.matrix.android.internal.auth.registration.SuccessResult +import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.identity.data.IdentityStore +import im.vector.matrix.android.internal.session.identity.model.IdentityRequestOwnershipParams +import im.vector.matrix.android.internal.task.Task +import javax.inject.Inject + +internal interface IdentitySubmitTokenForBindingTask : Task { + data class Params( + val threePid: ThreePid, + val token: String + ) +} + +internal class DefaultIdentitySubmitTokenForBindingTask @Inject constructor( + private val identityApiProvider: IdentityApiProvider, + private val identityStore: IdentityStore, + @UserId private val userId: String +) : IdentitySubmitTokenForBindingTask { + + override suspend fun execute(params: IdentitySubmitTokenForBindingTask.Params) { + val identityAPI = getIdentityApiAndEnsureTerms(identityApiProvider, userId) + val identityPendingBinding = identityStore.getPendingBinding(params.threePid) ?: throw IdentityServiceError.NoCurrentBindingError + + val tokenResponse = executeRequest(null) { + apiCall = identityAPI.submitToken( + params.threePid.toMedium(), + IdentityRequestOwnershipParams( + clientSecret = identityPendingBinding.clientSecret, + sid = identityPendingBinding.sid, + token = params.token + )) + } + + if (!tokenResponse.isSuccess()) { + throw IdentityServiceError.BindingError + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityTaskHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityTaskHelper.kt new file mode 100644 index 0000000000..bd97a0af2b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityTaskHelper.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity + +import im.vector.matrix.android.api.session.identity.IdentityServiceError +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.identity.model.IdentityAccountResponse + +internal suspend fun getIdentityApiAndEnsureTerms(identityApiProvider: IdentityApiProvider, userId: String): IdentityAPI { + val identityAPI = identityApiProvider.identityApi ?: throw IdentityServiceError.NoIdentityServerConfigured + + // Always check that we have access to the service (regarding terms) + val identityAccountResponse = executeRequest(null) { + apiCall = identityAPI.getAccount() + } + + assert(userId == identityAccountResponse.userId) + + return identityAPI +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/data/IdentityData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/data/IdentityData.kt new file mode 100644 index 0000000000..f1e57e1ed5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/data/IdentityData.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.data + +internal data class IdentityData( + val identityServerUrl: String?, + val token: String?, + val hashLookupPepper: String?, + val hashLookupAlgorithm: List +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/data/IdentityPendingBinding.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/data/IdentityPendingBinding.kt new file mode 100644 index 0000000000..b7f405cb0a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/data/IdentityPendingBinding.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.data + +internal data class IdentityPendingBinding( + /* Managed by Riot */ + val clientSecret: String, + /* Managed by Riot */ + val sendAttempt: Int, + /* Provided by the identity server */ + val sid: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/data/IdentityStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/data/IdentityStore.kt new file mode 100644 index 0000000000..d5cd3277ec --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/data/IdentityStore.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.data + +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.matrix.android.internal.session.identity.model.IdentityHashDetailResponse + +internal interface IdentityStore { + + fun getIdentityData(): IdentityData? + + fun setUrl(url: String?) + + fun setToken(token: String?) + + fun setHashDetails(hashDetailResponse: IdentityHashDetailResponse) + + /** + * Store details about a current binding + */ + fun storePendingBinding(threePid: ThreePid, data: IdentityPendingBinding) + + fun getPendingBinding(threePid: ThreePid): IdentityPendingBinding? + + fun deletePendingBinding(threePid: ThreePid) +} + +internal fun IdentityStore.getIdentityServerUrlWithoutProtocol(): String? { + return getIdentityData()?.identityServerUrl?.substringAfter("://") +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityDataEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityDataEntity.kt new file mode 100644 index 0000000000..76e480bdc9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityDataEntity.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.db + +import io.realm.RealmList +import io.realm.RealmObject + +internal open class IdentityDataEntity( + var identityServerUrl: String? = null, + var token: String? = null, + var hashLookupPepper: String? = null, + var hashLookupAlgorithm: RealmList = RealmList() +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityDataEntityQuery.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityDataEntityQuery.kt new file mode 100644 index 0000000000..0a07359642 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityDataEntityQuery.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.db + +import io.realm.Realm +import io.realm.RealmList +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +/** + * Only one object can be stored at a time + */ +internal fun IdentityDataEntity.Companion.get(realm: Realm): IdentityDataEntity? { + return realm.where().findFirst() +} + +private fun IdentityDataEntity.Companion.getOrCreate(realm: Realm): IdentityDataEntity { + return get(realm) ?: realm.createObject() +} + +internal fun IdentityDataEntity.Companion.setUrl(realm: Realm, + url: String?) { + realm.where().findAll().deleteAllFromRealm() + // Delete all pending binding if any + IdentityPendingBindingEntity.deleteAll(realm) + + if (url != null) { + getOrCreate(realm).apply { + identityServerUrl = url + } + } +} + +internal fun IdentityDataEntity.Companion.setToken(realm: Realm, + newToken: String?) { + get(realm)?.apply { + token = newToken + } +} + +internal fun IdentityDataEntity.Companion.setHashDetails(realm: Realm, + pepper: String, + algorithms: List) { + get(realm)?.apply { + hashLookupPepper = pepper + hashLookupAlgorithm = RealmList().apply { addAll(algorithms) } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityMapper.kt new file mode 100644 index 0000000000..1335e38565 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityMapper.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.db + +import im.vector.matrix.android.internal.session.identity.data.IdentityData +import im.vector.matrix.android.internal.session.identity.data.IdentityPendingBinding + +internal object IdentityMapper { + + fun map(entity: IdentityDataEntity): IdentityData { + return IdentityData( + identityServerUrl = entity.identityServerUrl, + token = entity.token, + hashLookupPepper = entity.hashLookupPepper, + hashLookupAlgorithm = entity.hashLookupAlgorithm.toList() + ) + } + + fun map(entity: IdentityPendingBindingEntity): IdentityPendingBinding { + return IdentityPendingBinding( + clientSecret = entity.clientSecret, + sendAttempt = entity.sendAttempt, + sid = entity.sid + ) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityPendingBindingEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityPendingBindingEntity.kt new file mode 100644 index 0000000000..51b195a7fc --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityPendingBindingEntity.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.db + +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.matrix.android.api.session.identity.toMedium +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class IdentityPendingBindingEntity( + @PrimaryKey var threePid: String = "", + /* Managed by Riot */ + var clientSecret: String = "", + /* Managed by Riot */ + var sendAttempt: Int = 0, + /* Provided by the identity server */ + var sid: String = "" +) : RealmObject() { + + companion object { + fun ThreePid.toPrimaryKey() = "${toMedium()}_$value" + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityPendingBindingEntityQuery.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityPendingBindingEntityQuery.kt new file mode 100644 index 0000000000..e358be6bbb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityPendingBindingEntityQuery.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.db + +import im.vector.matrix.android.api.session.identity.ThreePid +import io.realm.Realm +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +internal fun IdentityPendingBindingEntity.Companion.get(realm: Realm, threePid: ThreePid): IdentityPendingBindingEntity? { + return realm.where() + .equalTo(IdentityPendingBindingEntityFields.THREE_PID, threePid.toPrimaryKey()) + .findFirst() +} + +internal fun IdentityPendingBindingEntity.Companion.getOrCreate(realm: Realm, threePid: ThreePid): IdentityPendingBindingEntity { + return get(realm, threePid) ?: realm.createObject(threePid.toPrimaryKey()) +} + +internal fun IdentityPendingBindingEntity.Companion.delete(realm: Realm, threePid: ThreePid) { + get(realm, threePid)?.deleteFromRealm() +} + +internal fun IdentityPendingBindingEntity.Companion.deleteAll(realm: Realm) { + realm.where() + .findAll() + .deleteAllFromRealm() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityRealmModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityRealmModule.kt new file mode 100644 index 0000000000..19bd90ee1f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityRealmModule.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.db + +import io.realm.annotations.RealmModule + +/** + * Realm module for identity server classes + */ +@RealmModule(library = true, + classes = [ + IdentityDataEntity::class, + IdentityPendingBindingEntity::class + ]) +internal class IdentityRealmModule diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/RealmIdentityStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/RealmIdentityStore.kt new file mode 100644 index 0000000000..c294fbbf4b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/RealmIdentityStore.kt @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.db + +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.matrix.android.internal.di.IdentityDatabase +import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.session.identity.data.IdentityPendingBinding +import im.vector.matrix.android.internal.session.identity.data.IdentityData +import im.vector.matrix.android.internal.session.identity.data.IdentityStore +import im.vector.matrix.android.internal.session.identity.model.IdentityHashDetailResponse +import io.realm.Realm +import io.realm.RealmConfiguration +import javax.inject.Inject + +@SessionScope +internal class RealmIdentityStore @Inject constructor( + @IdentityDatabase + private val realmConfiguration: RealmConfiguration +) : IdentityStore { + + override fun getIdentityData(): IdentityData? { + return Realm.getInstance(realmConfiguration).use { realm -> + IdentityDataEntity.get(realm)?.let { IdentityMapper.map(it) } + } + } + + override fun setUrl(url: String?) { + Realm.getInstance(realmConfiguration).use { + it.executeTransaction { realm -> + IdentityDataEntity.setUrl(realm, url) + } + } + } + + override fun setToken(token: String?) { + Realm.getInstance(realmConfiguration).use { + it.executeTransaction { realm -> + IdentityDataEntity.setToken(realm, token) + } + } + } + + override fun setHashDetails(hashDetailResponse: IdentityHashDetailResponse) { + Realm.getInstance(realmConfiguration).use { + it.executeTransaction { realm -> + IdentityDataEntity.setHashDetails(realm, hashDetailResponse.pepper, hashDetailResponse.algorithms) + } + } + } + + override fun storePendingBinding(threePid: ThreePid, data: IdentityPendingBinding) { + Realm.getInstance(realmConfiguration).use { + it.executeTransaction { realm -> + IdentityPendingBindingEntity.getOrCreate(realm, threePid).let { entity -> + entity.clientSecret = data.clientSecret + entity.sendAttempt = data.sendAttempt + entity.sid = data.sid + } + } + } + } + + override fun getPendingBinding(threePid: ThreePid): IdentityPendingBinding? { + return Realm.getInstance(realmConfiguration).use { realm -> + IdentityPendingBindingEntity.get(realm, threePid)?.let { IdentityMapper.map(it) } + } + } + + override fun deletePendingBinding(threePid: ThreePid) { + Realm.getInstance(realmConfiguration).use { + it.executeTransaction { realm -> + IdentityPendingBindingEntity.delete(realm, threePid) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityAccountResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityAccountResponse.kt new file mode 100644 index 0000000000..a72eb75537 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityAccountResponse.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class IdentityAccountResponse( + /** + * Required. The user ID which registered the token. + */ + @Json(name = "user_id") + val userId: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityHashDetailResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityHashDetailResponse.kt new file mode 100644 index 0000000000..16a4e1fc71 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityHashDetailResponse.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Ref: https://github.com/matrix-org/matrix-doc/blob/hs/hash-identity/proposals/2134-identity-hash-lookup.md + */ +@JsonClass(generateAdapter = true) +internal data class IdentityHashDetailResponse( + /** + * Required. The pepper the client MUST use in hashing identifiers, and MUST supply to the /lookup endpoint when performing lookups. + * Servers SHOULD rotate this string often. + */ + @Json(name = "lookup_pepper") + val pepper: String, + + /** + * Required. The algorithms the server supports. Must contain at least "sha256". + * "none" can be another possible value. + */ + @Json(name = "algorithms") + val algorithms: List +) { + companion object { + const val ALGORITHM_SHA256 = "sha256" + const val ALGORITHM_NONE = "none" + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityLookUpParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityLookUpParams.kt new file mode 100644 index 0000000000..f87d14e1fe --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityLookUpParams.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Ref: https://github.com/matrix-org/matrix-doc/blob/hs/hash-identity/proposals/2134-identity-hash-lookup.md + */ +@JsonClass(generateAdapter = true) +internal data class IdentityLookUpParams( + /** + * Required. The addresses to look up. The format of the entries here depend on the algorithm used. + * Note that queries which have been incorrectly hashed or formatted will lead to no matches. + */ + @Json(name = "addresses") + val hashedAddresses: List, + + /** + * Required. The algorithm the client is using to encode the addresses. This should be one of the available options from /hash_details. + */ + @Json(name = "algorithm") + val algorithm: String, + + /** + * Required. The pepper from /hash_details. This is required even when the algorithm does not make use of it. + */ + @Json(name = "pepper") + val pepper: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityLookUpResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityLookUpResponse.kt new file mode 100644 index 0000000000..a71e2f7366 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityLookUpResponse.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Ref: https://github.com/matrix-org/matrix-doc/blob/hs/hash-identity/proposals/2134-identity-hash-lookup.md + */ +@JsonClass(generateAdapter = true) +internal data class IdentityLookUpResponse( + /** + * Required. Any applicable mappings of addresses to Matrix User IDs. Addresses which do not have associations will + * not be included, which can make this property be an empty object. + */ + @Json(name = "mappings") + val mappings: Map +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRegisterResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRegisterResponse.kt new file mode 100644 index 0000000000..86999d570d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRegisterResponse.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class IdentityRegisterResponse( + /** + * Required. An opaque string representing the token to authenticate future requests to the identity server with. + */ + @Json(name = "token") + val token: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRequestOwnershipParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRequestOwnershipParams.kt new file mode 100644 index 0000000000..9da86cbc48 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRequestOwnershipParams.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class IdentityRequestOwnershipParams( + /** + * Required. The client secret that was supplied to the requestToken call. + */ + @Json(name = "client_secret") + val clientSecret: String, + + /** + * Required. The session ID, generated by the requestToken call. + */ + @Json(name = "sid") + val sid: String, + + /** + * Required. The token generated by the requestToken call and sent to the user. + */ + @Json(name = "token") + val token: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRequestTokenBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRequestTokenBody.kt new file mode 100644 index 0000000000..3e92ebb1d2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRequestTokenBody.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +// Just to consider common parameters +private interface IdentityRequestTokenBody { + /** + * Required. A unique string generated by the client, and used to identify the validation attempt. + * It must be a string consisting of the characters [0-9a-zA-Z.=_-]. + * Its length must not exceed 255 characters and it must not be empty. + */ + val clientSecret: String + + val sendAttempt: Int +} + +@JsonClass(generateAdapter = true) +internal data class IdentityRequestTokenForEmailBody( + @Json(name = "client_secret") + override val clientSecret: String, + + /** + * Required. The server will only send an email if the send_attempt is a number greater than the most + * recent one which it has seen, scoped to that email + client_secret pair. This is to avoid repeatedly + * sending the same email in the case of request retries between the POSTing user and the identity server. + * The client should increment this value if they desire a new email (e.g. a reminder) to be sent. + * If they do not, the server should respond with success but not resend the email. + */ + @Json(name = "send_attempt") + override val sendAttempt: Int, + + /** + * Required. The email address to validate. + */ + @Json(name = "email") + val email: String +) : IdentityRequestTokenBody + +@JsonClass(generateAdapter = true) +internal data class IdentityRequestTokenForMsisdnBody( + @Json(name = "client_secret") + override val clientSecret: String, + + /** + * Required. The server will only send an SMS if the send_attempt is a number greater than the most recent one + * which it has seen, scoped to that country + phone_number + client_secret triple. This is to avoid repeatedly + * sending the same SMS in the case of request retries between the POSTing user and the identity server. + * The client should increment this value if they desire a new SMS (e.g. a reminder) to be sent. + */ + @Json(name = "send_attempt") + override val sendAttempt: Int, + + /** + * Required. The phone number to validate. + */ + @Json(name = "phone_number") + val phoneNumber: String, + + /** + * Required. The two-letter uppercase ISO-3166-1 alpha-2 country code that the number in phone_number + * should be parsed as if it were dialled from. + */ + @Json(name = "country") + val countryCode: String +) : IdentityRequestTokenBody diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRequestTokenResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRequestTokenResponse.kt new file mode 100644 index 0000000000..cb3c257ddb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRequestTokenResponse.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class IdentityRequestTokenResponse( + /** + * Required. The session ID. Session IDs are opaque strings generated by the identity server. + * They must consist entirely of the characters [0-9a-zA-Z.=_-]. + * Their length must not exceed 255 characters and they must not be empty. + */ + @Json(name = "sid") + val sid: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/AccountDataDataSource.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/AccountDataDataSource.kt new file mode 100644 index 0000000000..37b0da9101 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/AccountDataDataSource.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.todelete + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.util.Optional +import im.vector.matrix.android.api.util.toOptional +import im.vector.matrix.android.internal.database.model.UserAccountDataEntity +import im.vector.matrix.android.internal.database.model.UserAccountDataEntityFields +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent +import io.realm.Realm +import io.realm.RealmQuery +import javax.inject.Inject + +// There will be a duplicated class when Integration manager will be merged, so delete this one +internal class AccountDataDataSource @Inject constructor(private val monarchy: Monarchy, + private val accountDataMapper: AccountDataMapper) { + + fun getAccountDataEvent(type: String): UserAccountDataEvent? { + return getAccountDataEvents(setOf(type)).firstOrNull() + } + + fun getLiveAccountDataEvent(type: String): LiveData> { + return Transformations.map(getLiveAccountDataEvents(setOf(type))) { + it.firstOrNull()?.toOptional() + } + } + + fun getAccountDataEvents(types: Set): List { + return monarchy.fetchAllMappedSync( + { accountDataEventsQuery(it, types) }, + accountDataMapper::map + ) + } + + fun getLiveAccountDataEvents(types: Set): LiveData> { + return monarchy.findAllMappedWithChanges( + { accountDataEventsQuery(it, types) }, + accountDataMapper::map + ) + } + + private fun accountDataEventsQuery(realm: Realm, types: Set): RealmQuery { + val query = realm.where(UserAccountDataEntity::class.java) + if (types.isNotEmpty()) { + query.`in`(UserAccountDataEntityFields.TYPE, types.toTypedArray()) + } + return query + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/AccountDataMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/AccountDataMapper.kt new file mode 100644 index 0000000000..3a736681e2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/AccountDataMapper.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.todelete + +import com.squareup.moshi.Moshi +import im.vector.matrix.android.api.util.JSON_DICT_PARAMETERIZED_TYPE +import im.vector.matrix.android.internal.database.model.UserAccountDataEntity +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent +import javax.inject.Inject + +// There will be a duplicated class when Integration manager will be merged, so delete this one +internal class AccountDataMapper @Inject constructor(moshi: Moshi) { + + private val adapter = moshi.adapter>(JSON_DICT_PARAMETERIZED_TYPE) + + fun map(entity: UserAccountDataEntity): UserAccountDataEvent { + return UserAccountDataEvent( + type = entity.type ?: "", + content = entity.contentStr?.let { adapter.fromJson(it) }.orEmpty() + ) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/LiveData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/LiveData.kt new file mode 100644 index 0000000000..f84756fa86 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/LiveData.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.todelete + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer + +// There will be a duplicated class when Integration manager will be merged, so delete this one +inline fun LiveData.observeK(owner: LifecycleOwner, crossinline observer: (T?) -> Unit) { + this.observe(owner, Observer { observer(it) }) +} + +inline fun LiveData.observeNotNull(owner: LifecycleOwner, crossinline observer: (T) -> Unit) { + this.observe(owner, Observer { it?.run(observer) }) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/AccountThreePidsResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/AccountThreePidsResponse.kt new file mode 100644 index 0000000000..17f12113dd --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/AccountThreePidsResponse.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.session.profile + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the ThreePids response + */ +@JsonClass(generateAdapter = true) +internal data class AccountThreePidsResponse( + @Json(name = "threepids") + val threePids: List? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/BindThreePidBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/BindThreePidBody.kt new file mode 100644 index 0000000000..6ba3ddda4a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/BindThreePidBody.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.session.profile + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class BindThreePidBody( + /** + * Required. The client secret used in the session with the identity server. + */ + @Json(name = "client_secret") + val clientSecret: String, + + /** + * Required. The identity server to use. (without "https://") + */ + @Json(name = "id_server") + var identityServerUrlWithoutProtocol: String, + + /** + * Required. An access token previously registered with the identity server. + */ + @Json(name = "id_access_token") + var identityServerAccessToken: String, + + /** + * Required. The session identifier given by the identity server. + */ + @Json(name = "sid") + var sid: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/BindThreePidsTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/BindThreePidsTask.kt new file mode 100644 index 0000000000..0e1987dd5f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/BindThreePidsTask.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.profile + +import im.vector.matrix.android.api.session.identity.IdentityServiceError +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.matrix.android.internal.di.AuthenticatedIdentity +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.network.token.AccessTokenProvider +import im.vector.matrix.android.internal.session.identity.data.IdentityStore +import im.vector.matrix.android.internal.session.identity.data.getIdentityServerUrlWithoutProtocol +import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal abstract class BindThreePidsTask : Task { + data class Params( + val threePid: ThreePid + ) +} + +internal class DefaultBindThreePidsTask @Inject constructor(private val profileAPI: ProfileAPI, + private val identityStore: IdentityStore, + @AuthenticatedIdentity + private val accessTokenProvider: AccessTokenProvider, + private val eventBus: EventBus) : BindThreePidsTask() { + override suspend fun execute(params: Params) { + val identityServerUrlWithoutProtocol = identityStore.getIdentityServerUrlWithoutProtocol() ?: throw IdentityServiceError.NoIdentityServerConfigured + val identityServerAccessToken = accessTokenProvider.getToken() ?: throw IdentityServiceError.NoIdentityServerConfigured + val identityPendingBinding = identityStore.getPendingBinding(params.threePid) ?: throw IdentityServiceError.NoCurrentBindingError + + executeRequest(eventBus) { + apiCall = profileAPI.bindThreePid( + BindThreePidBody( + clientSecret = identityPendingBinding.clientSecret, + identityServerUrlWithoutProtocol = identityServerUrlWithoutProtocol, + identityServerAccessToken = identityServerAccessToken, + sid = identityPendingBinding.sid + )) + } + + // Binding is over, cleanup the store + identityStore.deletePendingBinding(params.threePid) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/DefaultProfileService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/DefaultProfileService.kt index e2c18e41d6..a981e8e930 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/DefaultProfileService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/DefaultProfileService.kt @@ -17,16 +17,23 @@ package im.vector.matrix.android.internal.session.profile +import androidx.lifecycle.LiveData +import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.identity.ThreePid import im.vector.matrix.android.api.session.profile.ProfileService import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.Optional +import im.vector.matrix.android.internal.database.model.UserThreePidEntity import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith +import io.realm.kotlin.where import javax.inject.Inject internal class DefaultProfileService @Inject constructor(private val taskExecutor: TaskExecutor, + private val monarchy: Monarchy, + private val refreshUserThreePidsTask: RefreshUserThreePidsTask, private val getProfileInfoTask: GetProfileInfoTask) : ProfileService { override fun getDisplayName(userId: String, matrixCallback: MatrixCallback>): Cancelable { @@ -73,4 +80,33 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto } .executeBy(taskExecutor) } + + override fun getThreePids(): List { + return monarchy.fetchAllMappedSync( + { it.where() }, + { it.asDomain() } + ) + } + + override fun getThreePidsLive(refreshData: Boolean): LiveData> { + if (refreshData) { + // Force a refresh of the values + refreshUserThreePidsTask + .configureWith() + .executeBy(taskExecutor) + } + + return monarchy.findAllMappedWithChanges( + { it.where() }, + { it.asDomain() } + ) + } +} + +private fun UserThreePidEntity.asDomain(): ThreePid { + return when (medium) { + ThirdPartyIdentifier.MEDIUM_EMAIL -> ThreePid.Email(address) + ThirdPartyIdentifier.MEDIUM_MSISDN -> ThreePid.Msisdn(address) + else -> error("Invalid medium type") + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileAPI.kt index 197d85f879..717497e582 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileAPI.kt @@ -20,10 +20,12 @@ package im.vector.matrix.android.internal.session.profile import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.internal.network.NetworkConstants import retrofit2.Call +import retrofit2.http.Body import retrofit2.http.GET +import retrofit2.http.POST import retrofit2.http.Path -interface ProfileAPI { +internal interface ProfileAPI { /** * Get the combined profile information for this user. @@ -33,4 +35,24 @@ interface ProfileAPI { */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}") fun getProfile(@Path("userId") userId: String): Call + + /** + * List all 3PIDs linked to the Matrix user account. + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid") + fun getThreePIDs(): Call + + /** + * Bind a threePid + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-account-3pid-bind + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "account/3pid/bind") + fun bindThreePid(@Body body: BindThreePidBody): Call + + /** + * Unbind a threePid + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-account-3pid-unbind + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "account/3pid/unbind") + fun unbindThreePid(@Body body: UnbindThreePidBody): Call } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileModule.kt index 7005a5341f..0d7ebe5b62 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileModule.kt @@ -42,4 +42,13 @@ internal abstract class ProfileModule { @Binds abstract fun bindGetProfileTask(task: DefaultGetProfileInfoTask): GetProfileInfoTask + + @Binds + abstract fun bindRefreshUserThreePidsTask(task: DefaultRefreshUserThreePidsTask): RefreshUserThreePidsTask + + @Binds + abstract fun bindBindThreePidsTask(task: DefaultBindThreePidsTask): BindThreePidsTask + + @Binds + abstract fun bindUnbindThreePidsTask(task: DefaultUnbindThreePidsTask): UnbindThreePidsTask } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/RefreshUserThreePidsTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/RefreshUserThreePidsTask.kt new file mode 100644 index 0000000000..9e4d683b8e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/RefreshUserThreePidsTask.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.profile + +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.internal.database.model.UserThreePidEntity +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal abstract class RefreshUserThreePidsTask : Task + +internal class DefaultRefreshUserThreePidsTask @Inject constructor(private val profileAPI: ProfileAPI, + private val monarchy: Monarchy, + private val eventBus: EventBus) : RefreshUserThreePidsTask() { + + override suspend fun execute(params: Unit) { + val accountThreePidsResponse = executeRequest(eventBus) { + apiCall = profileAPI.getThreePIDs() + } + + Timber.d("Get ${accountThreePidsResponse.threePids?.size} threePids") + // Store the list in DB + monarchy.writeAsync { realm -> + realm.where(UserThreePidEntity::class.java).findAll().deleteAllFromRealm() + accountThreePidsResponse.threePids?.forEach { + val entity = UserThreePidEntity( + it.medium?.takeIf { med -> med in ThirdPartyIdentifier.SUPPORTED_MEDIUM } ?: return@forEach, + it.address ?: return@forEach, + it.validatedAt.toLong(), + it.addedAt.toLong()) + realm.insertOrUpdate(entity) + } + } + } +} + +private fun Any?.toLong(): Long { + return when (this) { + null -> 0L + is Long -> this + is Double -> this.toLong() + else -> 0L + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ThirdPartyIdentifier.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ThirdPartyIdentifier.kt new file mode 100755 index 0000000000..76fa3bd80f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ThirdPartyIdentifier.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.session.profile + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class ThirdPartyIdentifier( + /** + * Required. The medium of the third party identifier. One of: ["email", "msisdn"] + */ + @Json(name = "medium") + val medium: String? = null, + + /** + * Required. The third party identifier address. + */ + @Json(name = "address") + val address: String? = null, + + /** + * Required. The timestamp in milliseconds when this 3PID has been validated. + * Define as Object because it should be Long and it is a Double. + * So, it might change. + */ + @Json(name = "validated_at") + val validatedAt: Any? = null, + + /** + * Required. The timestamp in milliseconds when this 3PID has been added to the user account. + * Define as Object because it should be Long and it is a Double. + * So, it might change. + */ + @Json(name = "added_at") + val addedAt: Any? = null +) { + companion object { + const val MEDIUM_EMAIL = "email" + const val MEDIUM_MSISDN = "msisdn" + + val SUPPORTED_MEDIUM = listOf(MEDIUM_EMAIL, MEDIUM_MSISDN) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/UnbindThreePidBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/UnbindThreePidBody.kt new file mode 100644 index 0000000000..705569ba87 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/UnbindThreePidBody.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.session.profile + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class UnbindThreePidBody( + /** + * The identity server to unbind from. If not provided, the homeserver MUST use the id_server the identifier was added through. + * If the homeserver does not know the original id_server, it MUST return a id_server_unbind_result of no-support. + */ + @Json(name = "id_server") + val identityServerUrlWithoutProtocol: String?, + + /** + * Required. The medium of the third party identifier being removed. One of: ["email", "msisdn"] + */ + @Json(name = "medium") + val medium: String, + + /** + * Required. The third party address being removed. + */ + @Json(name = "address") + val address: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/UnbindThreePidResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/UnbindThreePidResponse.kt new file mode 100644 index 0000000000..51467ad201 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/UnbindThreePidResponse.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.session.profile + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class UnbindThreePidResponse( + @Json(name = "id_server_unbind_result") + val idServerUnbindResult: String? +) { + fun isSuccess() = idServerUnbindResult == "success" +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/UnbindThreePidsTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/UnbindThreePidsTask.kt new file mode 100644 index 0000000000..5206ea9bda --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/UnbindThreePidsTask.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.profile + +import im.vector.matrix.android.api.session.identity.IdentityServiceError +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.matrix.android.api.session.identity.toMedium +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.identity.data.IdentityStore +import im.vector.matrix.android.internal.session.identity.data.getIdentityServerUrlWithoutProtocol +import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal abstract class UnbindThreePidsTask : Task { + data class Params( + val threePid: ThreePid + ) +} + +internal class DefaultUnbindThreePidsTask @Inject constructor(private val profileAPI: ProfileAPI, + private val identityStore: IdentityStore, + private val eventBus: EventBus) : UnbindThreePidsTask() { + override suspend fun execute(params: Params): Boolean { + val identityServerUrlWithoutProtocol = identityStore.getIdentityServerUrlWithoutProtocol() + ?: throw IdentityServiceError.NoIdentityServerConfigured + + return executeRequest(eventBus) { + apiCall = profileAPI.unbindThreePid( + UnbindThreePidBody( + identityServerUrlWithoutProtocol, + params.threePid.toMedium(), + params.threePid.value + )) + }.isSuccess() + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt index 833469909f..40d0500a48 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt @@ -34,6 +34,7 @@ import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.state.StateService import im.vector.matrix.android.api.session.room.timeline.TimelineService import im.vector.matrix.android.api.session.room.typing.TypingService +import im.vector.matrix.android.api.session.room.uploads.UploadsService import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.toOptional import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM @@ -54,6 +55,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, private val sendService: SendService, private val draftService: DraftService, private val stateService: StateService, + private val uploadsService: UploadsService, private val reportingService: ReportingService, private val readService: ReadService, private val typingService: TypingService, @@ -68,6 +70,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, SendService by sendService, DraftService by draftService, StateService by stateService, + UploadsService by uploadsService, ReportingService by reportingService, ReadService by readService, TypingService by typingService, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt index 7c544d64cf..974c30dba9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt @@ -32,6 +32,7 @@ import im.vector.matrix.android.internal.session.room.state.DefaultStateService import im.vector.matrix.android.internal.session.room.state.SendStateTask import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService import im.vector.matrix.android.internal.session.room.typing.DefaultTypingService +import im.vector.matrix.android.internal.session.room.uploads.DefaultUploadsService import im.vector.matrix.android.internal.task.TaskExecutor import javax.inject.Inject @@ -47,6 +48,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona private val sendServiceFactory: DefaultSendService.Factory, private val draftServiceFactory: DefaultDraftService.Factory, private val stateServiceFactory: DefaultStateService.Factory, + private val uploadsServiceFactory: DefaultUploadsService.Factory, private val reportingServiceFactory: DefaultReportingService.Factory, private val readServiceFactory: DefaultReadService.Factory, private val typingServiceFactory: DefaultTypingService.Factory, @@ -66,6 +68,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona sendService = sendServiceFactory.create(roomId), draftService = draftServiceFactory.create(roomId), stateService = stateServiceFactory.create(roomId), + uploadsService = uploadsServiceFactory.create(roomId), reportingService = reportingServiceFactory.create(roomId), readService = readServiceFactory.create(roomId), typingService = typingServiceFactory.create(roomId), diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index 6b003b5ba2..001ce120c8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -56,12 +56,16 @@ import im.vector.matrix.android.internal.session.room.reporting.DefaultReportCon import im.vector.matrix.android.internal.session.room.reporting.ReportContentTask import im.vector.matrix.android.internal.session.room.state.DefaultSendStateTask import im.vector.matrix.android.internal.session.room.state.SendStateTask +import im.vector.matrix.android.internal.session.room.timeline.DefaultFetchNextTokenAndPaginateTask import im.vector.matrix.android.internal.session.room.timeline.DefaultGetContextOfEventTask import im.vector.matrix.android.internal.session.room.timeline.DefaultPaginationTask +import im.vector.matrix.android.internal.session.room.timeline.FetchNextTokenAndPaginateTask import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask import im.vector.matrix.android.internal.session.room.timeline.PaginationTask import im.vector.matrix.android.internal.session.room.typing.DefaultSendTypingTask import im.vector.matrix.android.internal.session.room.typing.SendTypingTask +import im.vector.matrix.android.internal.session.room.uploads.DefaultGetUploadsTask +import im.vector.matrix.android.internal.session.room.uploads.GetUploadsTask import retrofit2.Retrofit @Module @@ -143,6 +147,9 @@ internal abstract class RoomModule { @Binds abstract fun bindPaginationTask(task: DefaultPaginationTask): PaginationTask + @Binds + abstract fun bindFetchNextTokenAndPaginateTask(task: DefaultFetchNextTokenAndPaginateTask): FetchNextTokenAndPaginateTask + @Binds abstract fun bindFetchEditHistoryTask(task: DefaultFetchEditHistoryTask): FetchEditHistoryTask @@ -151,4 +158,7 @@ internal abstract class RoomModule { @Binds abstract fun bindSendTypingTask(task: DefaultSendTypingTask): SendTypingTask + + @Binds + abstract fun bindGetUploadsTask(task: DefaultGetUploadsTask): GetUploadsTask } 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 6e0adccfb9..d6c1a3ada8 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 @@ -131,7 +131,7 @@ internal class RoomSummaryUpdater @Inject constructor( ?.canonicalAlias val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel()?.aliases - ?: emptyList() + .orEmpty() roomSummaryEntity.aliases.clear() roomSummaryEntity.aliases.addAll(roomAliases) roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|") diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DraftRepository.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DraftRepository.kt index b00bf2aadb..8740567fc8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DraftRepository.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DraftRepository.kt @@ -143,7 +143,7 @@ class DraftRepository @Inject constructor(private val monarchy: Monarchy) { } ) return Transformations.map(liveData) { - it.firstOrNull() ?: emptyList() + it.firstOrNull().orEmpty() } } 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 feb05a3275..3c1df50b75 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 @@ -126,6 +126,7 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: return name ?: roomId } + /** See [im.vector.matrix.android.api.session.room.sender.SenderInfo.disambiguatedDisplayName] */ private fun resolveRoomMemberName(roomMemberSummary: RoomMemberSummaryEntity?, roomMemberHelper: RoomMemberHelper): String? { if (roomMemberSummary == null) return null 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 59a7dc4b9c..252c8a31fd 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 @@ -111,7 +111,7 @@ internal class DefaultReadService @AssistedInject constructor( { readReceiptsSummaryMapper.map(it) } ) return Transformations.map(liveRealmData) { - it.firstOrNull() ?: emptyList() + it.firstOrNull().orEmpty() } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/UpdateQuickReactionTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/UpdateQuickReactionTask.kt index 6ec316e9a4..9684161c4e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/UpdateQuickReactionTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/UpdateQuickReactionTask.kt @@ -48,7 +48,7 @@ internal class DefaultUpdateQuickReactionTask @Inject constructor(private val mo monarchy.doWithRealm { realm -> res = updateQuickReaction(realm, params.reaction, params.oppositeReaction, params.eventId) } - return UpdateQuickReactionTask.Result(res?.first, res?.second ?: emptyList()) + return UpdateQuickReactionTask.Result(res?.first, res?.second.orEmpty()) } private fun updateQuickReaction(realm: Realm, reaction: String, oppositeReaction: String, eventId: String): Pair?> { 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 1037b7c79c..9c8723af05 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 @@ -68,31 +68,40 @@ internal class DefaultSendService @AssistedInject constructor( private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor() override fun sendTextMessage(text: CharSequence, msgType: String, autoMarkdown: Boolean): Cancelable { - val event = localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown).also { - createLocalEcho(it) + return localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown) + .also { createLocalEcho(it) } + .let { sendEvent(it) } + } + + // For test only + private fun sendTextMessages(text: CharSequence, msgType: String, autoMarkdown: Boolean, times: Int): Cancelable { + return CancelableBag().apply { + // Send the event several times + repeat(times) { i -> + localEchoEventFactory.createTextEvent(roomId, msgType, "$text - $i", autoMarkdown) + .also { createLocalEcho(it) } + .let { sendEvent(it) } + .also { add(it) } + } } - return sendEvent(event) } override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String): Cancelable { - val event = localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType).also { - createLocalEcho(it) - } - return sendEvent(event) + return localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType) + .also { createLocalEcho(it) } + .let { sendEvent(it) } } override fun sendPoll(question: String, options: List): Cancelable { - val event = localEchoEventFactory.createPollEvent(roomId, question, options).also { - createLocalEcho(it) - } - return sendEvent(event) + return localEchoEventFactory.createPollEvent(roomId, question, options) + .also { createLocalEcho(it) } + .let { sendEvent(it) } } override fun sendOptionsReply(pollEventId: String, optionIndex: Int, optionValue: String): Cancelable { - val event = localEchoEventFactory.createOptionsReplyEvent(roomId, pollEventId, optionIndex, optionValue).also { - createLocalEcho(it) - } - return sendEvent(event) + return localEchoEventFactory.createOptionsReplyEvent(roomId, pollEventId, optionIndex, optionValue) + .also { createLocalEcho(it) } + .let { sendEvent(it) } } private fun sendEvent(event: Event): Cancelable { @@ -119,8 +128,8 @@ internal class DefaultSendService @AssistedInject constructor( override fun redactEvent(event: Event, reason: String?): Cancelable { // TODO manage media/attachements? - val redactWork = createRedactEventWork(event, reason) - return timelineSendEventWorkCommon.postWork(roomId, redactWork) + return createRedactEventWork(event, reason) + .let { timelineSendEventWorkCommon.postWork(roomId, it) } } override fun resendTextMessage(localEcho: TimelineEvent): Cancelable? { @@ -263,31 +272,30 @@ internal class DefaultSendService @AssistedInject constructor( private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { // Same parameter - val params = EncryptEventWorker.Params(sessionId, event) - val sendWorkData = WorkerParamsFactory.toData(params) - - return workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerProvider.workConstraints) - .setInputData(sendWorkData) - .startChain(startChain) - .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS) - .build() + return EncryptEventWorker.Params(sessionId, event) + .let { WorkerParamsFactory.toData(it) } + .let { + workManagerProvider.matrixOneTimeWorkRequestBuilder() + .setConstraints(WorkManagerProvider.workConstraints) + .setInputData(it) + .startChain(startChain) + .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() + } } private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { - val sendContentWorkerParams = SendEventWorker.Params(sessionId, event) - val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) - - return timelineSendEventWorkCommon.createWork(sendWorkData, startChain) + return SendEventWorker.Params(sessionId, event) + .let { WorkerParamsFactory.toData(it) } + .let { timelineSendEventWorkCommon.createWork(it, startChain) } } private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest { - val redactEvent = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason).also { - createLocalEcho(it) - } - val sendContentWorkerParams = RedactEventWorker.Params(sessionId, redactEvent.eventId!!, roomId, event.eventId, reason) - val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) - return timelineSendEventWorkCommon.createWork(redactWorkData, true) + return localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason) + .also { createLocalEcho(it) } + .let { RedactEventWorker.Params(sessionId, it.eventId!!, roomId, event.eventId, reason) } + .let { WorkerParamsFactory.toData(it) } + .let { timelineSendEventWorkCommon.createWork(it, true) } } private fun createUploadMediaWork(allLocalEchos: List, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 5f0515e669..82d393e79a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -35,6 +35,7 @@ import im.vector.matrix.android.api.session.room.model.message.FileInfo import im.vector.matrix.android.api.session.room.model.message.ImageInfo import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageContentWithFormattedBody import im.vector.matrix.android.api.session.room.model.message.MessageFileContent import im.vector.matrix.android.api.session.room.model.message.MessageFormat import im.vector.matrix.android.api.session.room.model.message.MessageImageContent @@ -56,6 +57,7 @@ import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent 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.internal.di.UserId +import im.vector.matrix.android.internal.extensions.subStringBetween import im.vector.matrix.android.internal.session.content.ThumbnailExtractor import im.vector.matrix.android.internal.session.room.send.pills.TextPillsUtils import im.vector.matrix.android.internal.task.TaskExecutor @@ -84,6 +86,7 @@ internal class LocalEchoEventFactory @Inject constructor( ) { // TODO Inject private val parser = Parser.builder().build() + // TODO Inject private val renderer = HtmlRenderer.builder().build() @@ -102,8 +105,15 @@ internal class LocalEchoEventFactory @Inject constructor( val document = parser.parse(source) val htmlText = renderer.render(document) - if (isFormattedTextPertinent(source, htmlText)) { - return TextContent(text.toString(), htmlText) + // Cleanup extra paragraph + val cleanHtmlText = if (htmlText.startsWith("

") && htmlText.endsWith("

\n")) { + htmlText.subStringBetween("

", "

\n") + } else { + htmlText + } + + if (isFormattedTextPertinent(source, cleanHtmlText)) { + return TextContent(text.toString(), cleanHtmlText) } } else { // Try to detect pills @@ -192,7 +202,7 @@ internal class LocalEchoEventFactory @Inject constructor( permalink, stringProvider.getString(R.string.message_reply_to_prefix), userLink, - originalEvent.getDisambiguatedDisplayName(), + originalEvent.senderInfo.disambiguatedDisplayName, body.takeFormatted(), createTextContent(newBodyText, newBodyAutoMarkdown).takeFormatted() ) @@ -433,10 +443,8 @@ internal class LocalEchoEventFactory @Inject constructor( MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_NOTICE -> { var formattedText: String? = null - if (content is MessageTextContent) { - if (content.format == MessageFormat.FORMAT_MATRIX_HTML) { - formattedText = content.formattedBody - } + if (content is MessageContentWithFormattedBody) { + formattedText = content.matrixFormattedBody } val isReply = content.isReply() || originalContent.isReply() return if (isReply) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendResponse.kt index 947edee1ed..6d4784d225 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendResponse.kt @@ -21,5 +21,8 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) internal data class SendResponse( + /** + * A unique identifier for the event. + */ @Json(name = "event_id") val eventId: String ) 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 f2bee734ce..95a8581c2b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.session.room.timeline import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.extensions.orFalse import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.RelationType import im.vector.matrix.android.api.session.events.model.toModel @@ -35,7 +36,7 @@ import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryE 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.query.FilterContent +import im.vector.matrix.android.internal.database.query.TimelineEventFilter import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.whereInRoom @@ -71,6 +72,7 @@ internal class DefaultTimeline( private val realmConfiguration: RealmConfiguration, private val taskExecutor: TaskExecutor, private val contextOfEventTask: GetContextOfEventTask, + private val fetchNextTokenAndPaginateTask: FetchNextTokenAndPaginateTask, private val paginationTask: PaginationTask, private val timelineEventMapper: TimelineEventMapper, private val settings: TimelineSettings, @@ -383,7 +385,7 @@ internal class DefaultTimeline( } /** - * This has to be called on TimelineThread as it access realm live results + * This has to be called on TimelineThread as it accesses realm live results * @return true if createSnapshot should be posted */ private fun paginateInternal(startDisplayIndex: Int?, @@ -446,7 +448,7 @@ internal class DefaultTimeline( } /** - * This has to be called on TimelineThread as it access realm live results + * This has to be called on TimelineThread as it accesses realm live results */ private fun handleInitialLoad() { var shouldFetchInitialEvent = false @@ -478,7 +480,7 @@ internal class DefaultTimeline( } /** - * This has to be called on TimelineThread as it access realm live results + * This has to be called on TimelineThread as it accesses realm live results */ private fun handleUpdates(results: RealmResults, changeSet: OrderedCollectionChangeSet) { // If changeSet has deletion we are having a gap, so we clear everything @@ -516,68 +518,90 @@ internal class DefaultTimeline( } /** - * This has to be called on TimelineThread as it access realm live results + * This has to be called on TimelineThread as it accesses realm live results */ private fun executePaginationTask(direction: Timeline.Direction, limit: Int) { - val token = getTokenLive(direction) + val currentChunk = getLiveChunk() + val token = if (direction == Timeline.Direction.BACKWARDS) currentChunk?.prevToken else currentChunk?.nextToken if (token == null) { - updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } - return - } - val params = PaginationTask.Params(roomId = roomId, - from = token, - direction = direction.toPaginationDirection(), - limit = limit) - - Timber.v("Should fetch $limit items $direction") - cancelableBag += paginationTask - .configureWith(params) { - this.callback = object : MatrixCallback { - override fun onSuccess(data: TokenChunkEventPersistor.Result) { - when (data) { - TokenChunkEventPersistor.Result.SUCCESS -> { - Timber.v("Success fetching $limit items $direction from pagination request") - } - TokenChunkEventPersistor.Result.REACHED_END -> { - postSnapshot() - } - TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE -> - // Database won't be updated, so we force pagination request - BACKGROUND_HANDLER.post { - executePaginationTask(direction, limit) - } + if (direction == Timeline.Direction.FORWARDS && currentChunk?.hasBeenALastForwardChunk().orFalse()) { + // We are in the case that next event exists, but we do not know the next token. + // Fetch (again) the last event to get a nextToken + val lastKnownEventId = nonFilteredEvents.firstOrNull()?.eventId + if (lastKnownEventId == null) { + updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } + } else { + val params = FetchNextTokenAndPaginateTask.Params( + roomId = roomId, + limit = limit, + lastKnownEventId = lastKnownEventId + ) + cancelableBag += fetchNextTokenAndPaginateTask + .configureWith(params) { + this.callback = createPaginationCallback(limit, direction) } - } + .executeBy(taskExecutor) + } + } else { + updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } + } + } else { + val params = PaginationTask.Params( + roomId = roomId, + from = token, + direction = direction.toPaginationDirection(), + limit = limit + ) + Timber.v("Should fetch $limit items $direction") + cancelableBag += paginationTask + .configureWith(params) { + this.callback = createPaginationCallback(limit, direction) + } + .executeBy(taskExecutor) + } + } - override fun onFailure(failure: Throwable) { - updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } - postSnapshot() - Timber.v("Failure fetching $limit items $direction from pagination request") + // For debug purpose only + private fun dumpAndLogChunks() { + val liveChunk = getLiveChunk() + Timber.w("Live chunk: $liveChunk") + + Realm.getInstance(realmConfiguration).use { realm -> + ChunkEntity.where(realm, roomId).findAll() + .also { Timber.w("Found ${it.size} chunks") } + .forEach { + Timber.w("") + Timber.w("ChunkEntity: $it") + Timber.w("prevToken: ${it.prevToken}") + Timber.w("nextToken: ${it.nextToken}") + Timber.w("isLastBackward: ${it.isLastBackward}") + Timber.w("isLastForward: ${it.isLastForward}") + it.timelineEvents.forEach { tle -> + Timber.w(" TLE: ${tle.root?.content}") } } - } - .executeBy(taskExecutor) + } } /** - * This has to be called on TimelineThread as it access realm live results + * This has to be called on TimelineThread as it accesses realm live results */ - private fun getTokenLive(direction: Timeline.Direction): String? { val chunkEntity = getLiveChunk() ?: return null return if (direction == Timeline.Direction.BACKWARDS) chunkEntity.prevToken else chunkEntity.nextToken } /** - * This has to be called on TimelineThread as it access realm live results + * This has to be called on TimelineThread as it accesses realm live results + * Return the current Chunk */ private fun getLiveChunk(): ChunkEntity? { return nonFilteredEvents.firstOrNull()?.chunk?.firstOrNull() } /** - * This has to be called on TimelineThread as it access realm live results - * @return number of items who have been added + * This has to be called on TimelineThread as it accesses realm live results + * @return the number of items who have been added */ private fun buildTimelineEvents(startDisplayIndex: Int?, direction: Timeline.Direction, @@ -618,6 +642,8 @@ internal class DefaultTimeline( } val time = System.currentTimeMillis() - start Timber.v("Built ${offsetResults.size} items from db in $time ms") + // For the case where wo reach the lastForward chunk + updateLoadingStates(filteredEvents) return offsetResults.size } @@ -628,7 +654,7 @@ internal class DefaultTimeline( ) /** - * This has to be called on TimelineThread as it access realm live results + * This has to be called on TimelineThread as it accesses realm live results */ private fun getOffsetResults(startDisplayIndex: Int, direction: Timeline.Direction, @@ -713,6 +739,32 @@ internal class DefaultTimeline( forwardsState.set(State()) } + private fun createPaginationCallback(limit: Int, direction: Timeline.Direction): MatrixCallback { + return object : MatrixCallback { + override fun onSuccess(data: TokenChunkEventPersistor.Result) { + when (data) { + TokenChunkEventPersistor.Result.SUCCESS -> { + Timber.v("Success fetching $limit items $direction from pagination request") + } + TokenChunkEventPersistor.Result.REACHED_END -> { + postSnapshot() + } + TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE -> + // Database won't be updated, so we force pagination request + BACKGROUND_HANDLER.post { + executePaginationTask(direction, limit) + } + } + } + + override fun onFailure(failure: Throwable) { + updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } + postSnapshot() + Timber.v("Failure fetching $limit items $direction from pagination request") + } + } + } + // Extension methods *************************************************************************** private fun Timeline.Direction.toPaginationDirection(): PaginationDirection { @@ -724,8 +776,11 @@ internal class DefaultTimeline( `in`(TimelineEventEntityFields.ROOT.TYPE, settings.allowedTypes.toTypedArray()) } if (settings.filterEdits) { - not().like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.EDIT_TYPE) - not().like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.RESPONSE_TYPE) + not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT) + not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE) + } + if (settings.filterRedacted) { + not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED) } return this } @@ -737,13 +792,19 @@ internal class DefaultTimeline( } else { true } + if (!filterType) return@filter false + val filterEdits = if (settings.filterEdits && it.root.type == EventType.MESSAGE) { val messageContent = it.root.content.toModel() messageContent?.relatesTo?.type != RelationType.REPLACE } else { true } - filterType && filterEdits + if (!filterEdits) return@filter false + + val filterRedacted = settings.filterRedacted && it.root.isRedacted() + + filterRedacted } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt index c02bb915ef..ffa282d088 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt @@ -42,6 +42,7 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv private val contextOfEventTask: GetContextOfEventTask, private val eventDecryptor: TimelineEventDecryptor, private val paginationTask: PaginationTask, + private val fetchNextTokenAndPaginateTask: FetchNextTokenAndPaginateTask, private val timelineEventMapper: TimelineEventMapper, private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper ) : TimelineService { @@ -63,7 +64,8 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv settings = settings, hiddenReadReceipts = TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings), eventBus = eventBus, - eventDecryptor = eventDecryptor + eventDecryptor = eventDecryptor, + fetchNextTokenAndPaginateTask = fetchNextTokenAndPaginateTask ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/FetchNextTokenAndPaginateTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/FetchNextTokenAndPaginateTask.kt new file mode 100644 index 0000000000..1189e627c4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/FetchNextTokenAndPaginateTask.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.room.timeline + +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.internal.database.model.ChunkEntity +import im.vector.matrix.android.internal.database.query.findIncludingEvent +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 im.vector.matrix.android.internal.util.awaitTransaction +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface FetchNextTokenAndPaginateTask : Task { + + data class Params( + val roomId: String, + val lastKnownEventId: String, + val limit: Int + ) +} + +internal class DefaultFetchNextTokenAndPaginateTask @Inject constructor( + private val roomAPI: RoomAPI, + private val monarchy: Monarchy, + private val filterRepository: FilterRepository, + private val paginationTask: PaginationTask, + private val eventBus: EventBus +) : FetchNextTokenAndPaginateTask { + + override suspend fun execute(params: FetchNextTokenAndPaginateTask.Params): TokenChunkEventPersistor.Result { + val filter = filterRepository.getRoomFilter() + val response = executeRequest(eventBus) { + apiCall = roomAPI.getContextOfEvent(params.roomId, params.lastKnownEventId, 0, filter) + } + if (response.end == null) { + throw IllegalStateException("No next token found") + } + monarchy.awaitTransaction { + ChunkEntity.findIncludingEvent(it, params.lastKnownEventId)?.nextToken = response.end + } + val paginationParams = PaginationTask.Params( + roomId = params.roomId, + from = response.end, + direction = PaginationDirection.FORWARDS, + limit = params.limit + ) + return paginationTask.execute(paginationParams) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt index 056f942211..72e99701cd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt @@ -24,7 +24,7 @@ import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntit import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntityFields 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.query.FilterContent +import im.vector.matrix.android.internal.database.query.TimelineEventFilter import im.vector.matrix.android.internal.database.query.whereInRoom import io.realm.OrderedRealmCollectionChangeListener import io.realm.Realm @@ -149,16 +149,21 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu */ private fun RealmQuery.filterReceiptsWithSettings(): RealmQuery { beginGroup() + var needOr = false if (settings.filterTypes) { not().`in`("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", settings.allowedTypes.toTypedArray()) - } - if (settings.filterTypes && settings.filterEdits) { - or() + needOr = true } if (settings.filterEdits) { - like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", FilterContent.EDIT_TYPE) + if (needOr) or() + like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", TimelineEventFilter.Content.EDIT) or() - like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", FilterContent.RESPONSE_TYPE) + like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", TimelineEventFilter.Content.RESPONSE) + needOr = true + } + if (settings.filterRedacted) { + if (needOr) or() + like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.UNSIGNED_DATA}", TimelineEventFilter.Unsigned.REDACTED) } endGroup() return this diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEvent.kt index 95edf9bc49..7344f5598b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEvent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEvent.kt @@ -23,4 +23,6 @@ internal interface TokenChunkEvent { val end: String? val events: List val stateEvents: List + + fun hasMore() = start != end } 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 164626224b..e0f5b106d6 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 @@ -35,7 +35,7 @@ import im.vector.matrix.android.internal.database.query.copyToRealmOrIgnore import im.vector.matrix.android.internal.database.query.create import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.database.query.findAllIncludingEvents -import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom +import im.vector.matrix.android.internal.database.query.findLastForwardChunkOfRoom import im.vector.matrix.android.internal.database.query.getOrCreate import im.vector.matrix.android.internal.database.query.latestEvent import im.vector.matrix.android.internal.database.query.where @@ -149,7 +149,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy } ?: ChunkEntity.create(realm, prevToken, nextToken) - if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) { + if (receivedChunk.events.isEmpty() && !receivedChunk.hasMore()) { handleReachEnd(realm, roomId, direction, currentChunk) } else { handlePagination(realm, roomId, direction, receivedChunk, currentChunk) @@ -169,10 +169,10 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy private fun handleReachEnd(realm: Realm, roomId: String, direction: PaginationDirection, currentChunk: ChunkEntity) { Timber.v("Reach end of $roomId") if (direction == PaginationDirection.FORWARDS) { - val currentLiveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) - if (currentChunk != currentLiveChunk) { + val currentLastForwardChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) + if (currentChunk != currentLastForwardChunk) { currentChunk.isLastForward = true - currentLiveChunk?.deleteOnCascade() + currentLastForwardChunk?.deleteOnCascade() RoomSummaryEntity.where(realm, roomId).findFirst()?.apply { latestPreviewableEvent = TimelineEventEntity.latestEvent( realm, @@ -224,10 +224,13 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser) } + // Find all the chunks which contain at least one event from the list of eventIds val chunks = ChunkEntity.findAllIncludingEvents(realm, eventIds) + Timber.d("Found ${chunks.size} chunks containing at least one of the eventIds") val chunksToDelete = ArrayList() chunks.forEach { if (it != currentChunk) { + Timber.d("Merge $it") currentChunk.merge(roomId, it, direction) chunksToDelete.add(it) } @@ -246,6 +249,8 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy ) roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent } - RoomEntity.where(realm, roomId).findFirst()?.addOrUpdate(currentChunk) + if (currentChunk.isValid) { + RoomEntity.where(realm, roomId).findFirst()?.addOrUpdate(currentChunk) + } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/DefaultUploadsService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/DefaultUploadsService.kt new file mode 100644 index 0000000000..6ec9abf8e2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/DefaultUploadsService.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.room.uploads + +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.crypto.CryptoService +import im.vector.matrix.android.api.session.room.uploads.GetUploadsResult +import im.vector.matrix.android.api.session.room.uploads.UploadsService +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith + +internal class DefaultUploadsService @AssistedInject constructor( + @Assisted private val roomId: String, + private val taskExecutor: TaskExecutor, + private val getUploadsTask: GetUploadsTask, + private val cryptoService: CryptoService +) : UploadsService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): UploadsService + } + + override fun getUploads(numberOfEvents: Int, since: String?, callback: MatrixCallback): Cancelable { + return getUploadsTask + .configureWith(GetUploadsTask.Params(roomId, cryptoService.isRoomEncrypted(roomId), numberOfEvents, since)) { + this.callback = callback + } + .executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/GetUploadsTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/GetUploadsTask.kt new file mode 100644 index 0000000000..ecc7bc1b41 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/GetUploadsTask.kt @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.room.uploads + +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent +import im.vector.matrix.android.api.session.room.sender.SenderInfo +import im.vector.matrix.android.api.session.room.uploads.GetUploadsResult +import im.vector.matrix.android.api.session.room.uploads.UploadEvent +import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.model.EventEntityFields +import im.vector.matrix.android.internal.database.query.TimelineEventFilter +import im.vector.matrix.android.internal.database.query.whereType +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.filter.FilterFactory +import im.vector.matrix.android.internal.session.room.RoomAPI +import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper +import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection +import im.vector.matrix.android.internal.session.room.timeline.PaginationResponse +import im.vector.matrix.android.internal.session.sync.SyncTokenStore +import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetUploadsTask : Task { + + data class Params( + val roomId: String, + val isRoomEncrypted: Boolean, + val numberOfEvents: Int, + val since: String? + ) +} + +internal class DefaultGetUploadsTask @Inject constructor( + private val roomAPI: RoomAPI, + private val tokenStore: SyncTokenStore, + private val monarchy: Monarchy, + private val eventBus: EventBus) + : GetUploadsTask { + + override suspend fun execute(params: GetUploadsTask.Params): GetUploadsResult { + val result: GetUploadsResult + val events: List + + if (params.isRoomEncrypted) { + // Get a chunk of events from cache for e2e rooms + + result = GetUploadsResult( + uploadEvents = emptyList(), + nextToken = "", + hasMore = false + ) + + var eventsFromRealm = emptyList() + monarchy.doWithRealm { realm -> + eventsFromRealm = EventEntity.whereType(realm, EventType.ENCRYPTED, params.roomId) + .like(EventEntityFields.DECRYPTION_RESULT_JSON, TimelineEventFilter.DecryptedContent.URL) + .findAll() + .map { it.asDomain() } + // Exclude stickers + .filter { it.getClearType() != EventType.STICKER } + } + events = eventsFromRealm + } else { + val since = params.since ?: tokenStore.getLastToken() ?: throw IllegalStateException("No token available") + + val filter = FilterFactory.createUploadsFilter(params.numberOfEvents).toJSONString() + val chunk = executeRequest(eventBus) { + apiCall = roomAPI.getRoomMessagesFrom(params.roomId, since, PaginationDirection.BACKWARDS.value, params.numberOfEvents, filter) + } + + result = GetUploadsResult( + uploadEvents = emptyList(), + nextToken = chunk.end ?: "", + hasMore = chunk.hasMore() + ) + events = chunk.events + } + + var uploadEvents = listOf() + + val cacheOfSenderInfos = mutableMapOf() + + // Get a snapshot of all room members + monarchy.doWithRealm { realm -> + val roomMemberHelper = RoomMemberHelper(realm, params.roomId) + + uploadEvents = events.mapNotNull { event -> + val eventId = event.eventId ?: return@mapNotNull null + val messageContent = event.getClearContent()?.toModel() ?: return@mapNotNull null + val messageWithAttachmentContent = (messageContent as? MessageWithAttachmentContent) ?: return@mapNotNull null + val senderId = event.senderId ?: return@mapNotNull null + + val senderInfo = cacheOfSenderInfos.getOrPut(senderId) { + val roomMemberSummaryEntity = roomMemberHelper.getLastRoomMember(senderId) + SenderInfo( + userId = senderId, + displayName = roomMemberSummaryEntity?.displayName, + isUniqueDisplayName = roomMemberHelper.isUniqueDisplayName(roomMemberSummaryEntity?.displayName), + avatarUrl = roomMemberSummaryEntity?.avatarUrl + ) + } + + UploadEvent( + root = event, + eventId = eventId, + contentWithAttachmentContent = messageWithAttachmentContent, + senderInfo = senderInfo + ) + } + } + + return result.copy(uploadEvents = uploadEvents) + } +} 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 68df456831..021b3ed066 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 @@ -25,7 +25,6 @@ import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.launchToCallback import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers -import kotlinx.coroutines.GlobalScope import javax.inject.Inject internal class DefaultSignOutService @Inject constructor(private val signOutTask: SignOutTask, @@ -45,7 +44,7 @@ internal class DefaultSignOutService @Inject constructor(private val signOutTask override fun updateCredentials(credentials: Credentials, callback: MatrixCallback): Cancelable { - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { sessionParamsStore.updateCredentials(credentials) } } 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 0b8902e71b..5763d397c8 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 @@ -43,13 +43,13 @@ internal class DefaultSignInAgainTask @Inject constructor( apiCall = signOutAPI.loginAgain( PasswordLoginParams.userIdentifier( // Reuse the same userId - sessionParams.credentials.userId, + sessionParams.userId, params.password, // The spec says the initial device name will be ignored // https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-login // but https://github.com/matrix-org/synapse/issues/6525 // Reuse the same deviceId - deviceId = sessionParams.credentials.deviceId + deviceId = sessionParams.deviceId ) ) } 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 610ade5744..cca0af7feb 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 @@ -20,6 +20,7 @@ import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.cleanup.CleanupSession +import im.vector.matrix.android.internal.session.identity.IdentityDisconnectTask import im.vector.matrix.android.internal.task.Task import org.greenrobot.eventbus.EventBus import timber.log.Timber @@ -35,6 +36,7 @@ internal interface SignOutTask : Task { internal class DefaultSignOutTask @Inject constructor( private val signOutAPI: SignOutAPI, private val eventBus: EventBus, + private val identityDisconnectTask: IdentityDisconnectTask, private val cleanupSession: CleanupSession ) : SignOutTask { @@ -60,6 +62,10 @@ internal class DefaultSignOutTask @Inject constructor( } } + // Logout from identity server if any + runCatching { identityDisconnectTask.execute(Unit) } + .onFailure { Timber.w(it, "Unable to disconnect identity server") } + Timber.d("SignOut: cleanup session...") cleanupSession.handle() } 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 70c1e39334..a910944fbf 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 @@ -29,6 +29,7 @@ import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult import im.vector.matrix.android.internal.database.helper.addOrUpdate import im.vector.matrix.android.internal.database.helper.addTimelineEvent +import im.vector.matrix.android.internal.database.helper.deleteOnCascade import im.vector.matrix.android.internal.database.mapper.ContentMapper import im.vector.matrix.android.internal.database.mapper.toEntity import im.vector.matrix.android.internal.database.model.ChunkEntity @@ -36,7 +37,7 @@ import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.query.copyToRealmOrIgnore import im.vector.matrix.android.internal.database.query.find -import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom +import im.vector.matrix.android.internal.database.query.findLastForwardChunkOfRoom import im.vector.matrix.android.internal.database.query.getOrCreate import im.vector.matrix.android.internal.database.query.getOrNull import im.vector.matrix.android.internal.database.query.where @@ -220,12 +221,13 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle prevToken: String? = null, isLimited: Boolean = true, syncLocalTimestampMillis: Long): ChunkEntity { - val lastChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomEntity.roomId) + val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId) val chunkEntity = if (!isLimited && lastChunk != null) { lastChunk } else { realm.createObject().apply { this.prevToken = prevToken } } + // Only one chunk has isLastForward set to true lastChunk?.isLastForward = false chunkEntity.isLastForward = true @@ -271,6 +273,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle event.mxDecryptionResult = adapter.fromJson(json) } } + // Finally delete the local echo + sendingEventEntity.deleteOnCascade() } else { Timber.v("Can't find corresponding local echo for tx:$it") } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt index c508413665..c2e36604e3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt @@ -30,5 +30,7 @@ abstract class UserAccountData : AccountDataContent { const val TYPE_PREVIEW_URLS = "org.matrix.preview_urls" const val TYPE_WIDGETS = "m.widgets" const val TYPE_PUSH_RULES = "m.push_rules" + const val TYPE_IDENTITY_SERVER = "m.identity_server" + const val TYPE_ACCEPTED_TERMS = "m.accepted_terms" } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataAcceptedTerms.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataAcceptedTerms.kt new file mode 100644 index 0000000000..ef34503463 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataAcceptedTerms.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.sync.model.accountdata + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class UserAccountDataAcceptedTerms( + @Json(name = "type") override val type: String = TYPE_ACCEPTED_TERMS, + @Json(name = "content") val content: AcceptedTermsContent +) : UserAccountData() + +@JsonClass(generateAdapter = true) +internal data class AcceptedTermsContent( + @Json(name = "accepted") val acceptedTerms: List = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataIdentityServer.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataIdentityServer.kt new file mode 100644 index 0000000000..4af2034d64 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataIdentityServer.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.sync.model.accountdata + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class UserAccountDataIdentityServer( + @Json(name = "type") override val type: String = TYPE_IDENTITY_SERVER, + @Json(name = "content") val content: IdentityServerContent? = null +) : UserAccountData() + +@JsonClass(generateAdapter = true) +internal data class IdentityServerContent( + @Json(name = "base_url") val baseUrl: String? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/AcceptTermsBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/AcceptTermsBody.kt new file mode 100644 index 0000000000..c5827b822f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/AcceptTermsBody.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.terms + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class represent a list of urls of terms the user wants to accept + */ +@JsonClass(generateAdapter = true) +internal data class AcceptTermsBody( + @Json(name = "user_accepts") + val acceptedTermUrls: List +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/DefaultTermsService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/DefaultTermsService.kt new file mode 100644 index 0000000000..6d5e597da8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/DefaultTermsService.kt @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.terms + +import dagger.Lazy +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.terms.GetTermsResponse +import im.vector.matrix.android.api.session.terms.TermsService +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.network.NetworkConstants +import im.vector.matrix.android.internal.network.RetrofitFactory +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.identity.IdentityAuthAPI +import im.vector.matrix.android.internal.session.identity.IdentityRegisterTask +import im.vector.matrix.android.internal.session.identity.todelete.AccountDataDataSource +import im.vector.matrix.android.internal.session.openid.GetOpenIdTokenTask +import im.vector.matrix.android.internal.session.sync.model.accountdata.AcceptedTermsContent +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData +import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.launchToCallback +import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers +import okhttp3.OkHttpClient +import javax.inject.Inject + +internal class DefaultTermsService @Inject constructor( + @Unauthenticated + private val unauthenticatedOkHttpClient: Lazy, + private val accountDataDataSource: AccountDataDataSource, + private val termsAPI: TermsAPI, + private val retrofitFactory: RetrofitFactory, + private val getOpenIdTokenTask: GetOpenIdTokenTask, + private val identityRegisterTask: IdentityRegisterTask, + private val updateUserAccountDataTask: UpdateUserAccountDataTask, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val taskExecutor: TaskExecutor +) : TermsService { + override fun getTerms(serviceType: TermsService.ServiceType, + baseUrl: String, + callback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + val sep = if (baseUrl.endsWith("/")) "" else "/" + + val url = when (serviceType) { + TermsService.ServiceType.IntegrationManager -> "$baseUrl$sep${NetworkConstants.URI_INTEGRATION_MANAGER_PATH}" + TermsService.ServiceType.IdentityService -> "$baseUrl$sep${NetworkConstants.URI_IDENTITY_PATH_V2}" + } + + val termsResponse = executeRequest(null) { + apiCall = termsAPI.getTerms("${url}terms") + } + + GetTermsResponse(termsResponse, getAlreadyAcceptedTermUrlsFromAccountData()) + } + } + + override fun agreeToTerms(serviceType: TermsService.ServiceType, + baseUrl: String, + agreedUrls: List, + token: String?, + callback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + val sep = if (baseUrl.endsWith("/")) "" else "/" + + val url = when (serviceType) { + TermsService.ServiceType.IntegrationManager -> "$baseUrl$sep${NetworkConstants.URI_INTEGRATION_MANAGER_PATH}" + TermsService.ServiceType.IdentityService -> "$baseUrl$sep${NetworkConstants.URI_IDENTITY_PATH_V2}" + } + + val tokenToUse = token?.takeIf { it.isNotEmpty() } ?: getToken(baseUrl) + + executeRequest(null) { + apiCall = termsAPI.agreeToTerms("${url}terms", AcceptTermsBody(agreedUrls), "Bearer $tokenToUse") + } + + // client SHOULD update this account data section adding any the URLs + // of any additional documents that the user agreed to this list. + // Get current m.accepted_terms append new ones and update account data + val listOfAcceptedTerms = getAlreadyAcceptedTermUrlsFromAccountData() + + val newList = listOfAcceptedTerms.toMutableSet().apply { addAll(agreedUrls) }.toList() + + updateUserAccountDataTask.execute(UpdateUserAccountDataTask.AcceptedTermsParams( + acceptedTermsContent = AcceptedTermsContent(newList) + )) + } + } + + private suspend fun getToken(url: String): String { + // TODO This is duplicated code see DefaultIdentityService + val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java) + + val openIdToken = getOpenIdTokenTask.execute(Unit) + val token = identityRegisterTask.execute(IdentityRegisterTask.Params(api, openIdToken)) + + return token.token + } + + private fun getAlreadyAcceptedTermUrlsFromAccountData(): Set { + return accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_ACCEPTED_TERMS) + ?.content + ?.toModel() + ?.acceptedTerms + ?.toSet() + .orEmpty() + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/identity/IdentityPingApi.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsAPI.kt similarity index 53% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/identity/IdentityPingApi.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsAPI.kt index 2a0e00704c..03b745f8d7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/identity/IdentityPingApi.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsAPI.kt @@ -13,22 +13,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package im.vector.matrix.android.internal.identity -import im.vector.matrix.android.internal.network.NetworkConstants +package im.vector.matrix.android.internal.session.terms + +import im.vector.matrix.android.internal.network.HttpHeaders import retrofit2.Call +import retrofit2.http.Body import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.POST +import retrofit2.http.Url -internal interface IdentityPingApi { +internal interface TermsAPI { + /** + * This request does not require authentication + */ + @GET + fun getTerms(@Url url: String): Call /** - * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery - * Simple ping call to check if server alive - * - * Ref: https://matrix.org/docs/spec/identity_service/unstable#status-check - * - * @return 200 in case of success + * This request requires authentication */ - @GET(NetworkConstants.URI_API_PREFIX_IDENTITY) - fun ping(): Call + @POST + fun agreeToTerms(@Url url: String, @Body params: AcceptTermsBody, @Header(HttpHeaders.Authorization) token: String): Call } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsModule.kt new file mode 100644 index 0000000000..eee7e22134 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsModule.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.terms + +import dagger.Binds +import dagger.Lazy +import dagger.Module +import dagger.Provides +import im.vector.matrix.android.api.session.terms.TermsService +import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.network.RetrofitFactory +import im.vector.matrix.android.internal.session.SessionScope +import okhttp3.OkHttpClient + +@Module +internal abstract class TermsModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesTermsAPI(@Unauthenticated unauthenticatedOkHttpClient: Lazy, + retrofitFactory: RetrofitFactory): TermsAPI { + val retrofit = retrofitFactory.create(unauthenticatedOkHttpClient, "https://foo.bar") + return retrofit.create(TermsAPI::class.java) + } + } + + @Binds + abstract fun bindTermsService(service: DefaultTermsService): TermsService +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsResponse.kt new file mode 100644 index 0000000000..394ee958ac --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsResponse.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.terms + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.util.JsonDict +import im.vector.matrix.android.internal.auth.registration.LocalizedFlowDataLoginTerms + +/** + * This class represent a localized privacy policy for registration Flow. + */ +@JsonClass(generateAdapter = true) +data class TermsResponse( + @Json(name = "policies") + val policies: JsonDict? = null +) { + + fun getLocalizedTerms(userLanguage: String, + defaultLanguage: String = "en"): List { + return policies?.map { + val tos = policies[it.key] as? Map<*, *> ?: return@map null + ((tos[userLanguage] ?: tos[defaultLanguage]) as? Map<*, *>)?.let { termsMap -> + val name = termsMap[NAME] as? String + val url = termsMap[URL] as? String + LocalizedFlowDataLoginTerms( + policyName = it.key, + localizedUrl = url, + localizedName = name, + version = tos[VERSION] as? String + ) + } + }?.filterNotNull().orEmpty() + } + + private companion object { + const val VERSION = "version" + const val NAME = "name" + const val URL = "url" + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataModule.kt index 1fd4162d0a..2378ab3f0e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataModule.kt @@ -41,5 +41,5 @@ internal abstract class AccountDataModule { abstract fun bindSaveBreadcrumbsTask(task: DefaultSaveBreadcrumbsTask): SaveBreadcrumbsTask @Binds - abstract fun bindUpdateBreadcrumsTask(task: DefaultUpdateBreadcrumbsTask): UpdateBreadcrumbsTask + abstract fun bindUpdateBreadcrumbsTask(task: DefaultUpdateBreadcrumbsTask): UpdateBreadcrumbsTask } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DefaultAccountDataService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DefaultAccountDataService.kt index 7756b22510..31abc800c6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DefaultAccountDataService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DefaultAccountDataService.kt @@ -67,7 +67,7 @@ internal class DefaultAccountDataService @Inject constructor( entity.type?.let { type -> UserAccountDataEvent( type = type, - content = entity.contentStr?.let { adapter.fromJson(it) } ?: emptyMap() + content = entity.contentStr?.let { adapter.fromJson(it) }.orEmpty() ) } } @@ -84,7 +84,7 @@ internal class DefaultAccountDataService @Inject constructor( }, { entity -> UserAccountDataEvent( type = entity.type ?: "", - content = entity.contentStr?.let { adapter.fromJson(it) } ?: emptyMap() + content = entity.contentStr?.let { adapter.fromJson(it) }.orEmpty() ) }) } 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 beb3a0fcc0..07242984b5 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 @@ -18,7 +18,9 @@ package im.vector.matrix.android.internal.session.user.accountdata import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.sync.model.accountdata.AcceptedTermsContent import im.vector.matrix.android.internal.session.sync.model.accountdata.BreadcrumbsContent +import im.vector.matrix.android.internal.session.sync.model.accountdata.IdentityServerContent import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData import im.vector.matrix.android.internal.task.Task import org.greenrobot.eventbus.EventBus @@ -31,6 +33,24 @@ internal interface UpdateUserAccountDataTask : Task> diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/UrlUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/UrlUtils.kt index 8ad5e89605..7c81a03223 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/UrlUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/UrlUtils.kt @@ -26,3 +26,14 @@ internal fun String.isValidUrl(): Boolean { false } } + +/** + * Ensure string starts with "http". If it is not the case, "https://" is added, only if the String is not empty + */ +internal fun String.ensureProtocol(): String { + return when { + isEmpty() -> this + !startsWith("http") -> "https://$this" + else -> this + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/GetWellknownTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/GetWellknownTask.kt similarity index 96% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/GetWellknownTask.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/GetWellknownTask.kt index 8ed2cb3b0f..c6f6b8752d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/GetWellknownTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/GetWellknownTask.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.auth.wellknown +package im.vector.matrix.android.internal.wellknown import android.util.MalformedJsonException import dagger.Lazy @@ -23,10 +23,10 @@ import im.vector.matrix.android.api.auth.data.WellKnown import im.vector.matrix.android.api.auth.wellknown.WellknownResult import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.internal.di.Unauthenticated -import im.vector.matrix.android.internal.identity.IdentityPingApi import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.homeserver.CapabilitiesAPI +import im.vector.matrix.android.internal.session.identity.IdentityAuthAPI import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.isValidUrl import okhttp3.OkHttpClient @@ -119,7 +119,7 @@ internal class DefaultGetWellknownTask @Inject constructor( try { executeRequest(null) { - apiCall = capabilitiesAPI.getVersions() + apiCall = capabilitiesAPI.ping() } } catch (throwable: Throwable) { return WellknownResult.FailError @@ -153,7 +153,7 @@ internal class DefaultGetWellknownTask @Inject constructor( */ private suspend fun validateIdentityServer(identityServerBaseUrl: String): Boolean { val identityPingApi = retrofitFactory.create(okHttpClient, identityServerBaseUrl) - .create(IdentityPingApi::class.java) + .create(IdentityAuthAPI::class.java) return try { executeRequest(null) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/WellKnownAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/WellKnownAPI.kt similarity index 94% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/WellKnownAPI.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/WellKnownAPI.kt index 71928123bf..ec62707db7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/WellKnownAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/WellKnownAPI.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package im.vector.matrix.android.internal.auth.wellknown +package im.vector.matrix.android.internal.wellknown import im.vector.matrix.android.api.auth.data.WellKnown import retrofit2.Call diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/WellknownModule.kt similarity index 60% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/WellknownModule.kt index 6e89a28b7d..2705803fec 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/WellknownModule.kt @@ -1,11 +1,11 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright (c) 2020 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -14,10 +14,14 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.database.query +package im.vector.matrix.android.internal.wellknown -internal object FilterContent { +import dagger.Binds +import dagger.Module - internal const val EDIT_TYPE = """{*"m.relates_to"*"rel_type":*"m.replace"*}""" - internal const val RESPONSE_TYPE = """{*"m.relates_to"*"rel_type":*"m.response"*}""" +@Module +internal abstract class WellknownModule { + + @Binds + abstract fun bindGetWellknownTask(task: DefaultGetWellknownTask): GetWellknownTask } 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 871d01175e..d19396d932 100644 --- a/matrix-sdk-android/src/main/res/values-de/strings.xml +++ b/matrix-sdk-android/src/main/res/values-de/strings.xml @@ -104,7 +104,7 @@ Einhorn Schwein Elefant - Hase + Kaninchen %s hat diesen Raum aufgewertet. Panda diff --git a/matrix-sdk-android/src/main/res/values-et/strings.xml b/matrix-sdk-android/src/main/res/values-et/strings.xml index 1d52c2a7a1..6a09503857 100644 --- a/matrix-sdk-android/src/main/res/values-et/strings.xml +++ b/matrix-sdk-android/src/main/res/values-et/strings.xml @@ -185,4 +185,24 @@ %1$s lülitas sisse läbiva krüptimise. %1$s lülitas sisse läbiva krüptimise (tundmatu algoritm %2$s). + + %1$s lisas %2$s selle jututoa aadressiks. + %1$s lisas %2$s selle jututoa aadressideks. + + + + %1$s eemaldas %2$s kui selle jututoa aadressi. + %1$s eemaldas %2$s selle jututoa aadresside hulgast. + + + %1$s lisas %2$s ja eemaldas %3$s selle jututoa aadresside loendist. + + %1$s seadistas selle jututoa põhiaadressiks %2$s. + %1$s eemaldas selle jututoa põhiaadressi. + + %1$s lubas külalistel selle jututoaga liituda. + %1$s seadistas, et külalised ei või selle jututoaga liituda. + + %s soovib verifitseerida sinu võtmeid, kuid sinu kasutatav klient ei oska vestluse-sisest verifitseerimist teha. Sa pead kasutama traditsioonilist verifitseerimislahendust. + 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 4b2cc183a0..7f06f232c1 100644 --- a/matrix-sdk-android/src/main/res/values-eu/strings.xml +++ b/matrix-sdk-android/src/main/res/values-eu/strings.xml @@ -7,8 +7,8 @@ %s erabiltzailearen gonbidapena %1$s erabiltzaileak %2$s gonbidatu du %1$s erabiltzaileak gonbidatu zaitu - %1$s elkartu da - %1$s atera da + %1$s gelara elkartu da + %1$s gelatik atera da %1$s erabiltzaileak gonbidapena baztertu du %1$s erabiltzaileak %2$s kanporatu du %1$s erabiltzaileak debekua kendu dio %2$s erabiltzaileari @@ -176,8 +176,8 @@ %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 gelara elkartu da. Arrazoia: %2$s + %1$s gelatik 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 diff --git a/matrix-sdk-android/src/main/res/values-lt/strings.xml b/matrix-sdk-android/src/main/res/values-lt/strings.xml new file mode 100644 index 0000000000..b867219408 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-lt/strings.xml @@ -0,0 +1,8 @@ + + + %1$s: %2$s + %1$s išsiuntė atvaizdą. + %1$s išsiuntė lipduką. + + %s pakvietimas + diff --git a/matrix-sdk-android/src/main/res/values-sk/strings.xml b/matrix-sdk-android/src/main/res/values-sk/strings.xml index a18cff615c..c6eb6b896b 100644 --- a/matrix-sdk-android/src/main/res/values-sk/strings.xml +++ b/matrix-sdk-android/src/main/res/values-sk/strings.xml @@ -6,8 +6,8 @@ Pozvanie %s %1$s pozval %2$s %1$s vás pozval - %1$s vstúpil - %1$s opustil + %1$s sa pripojil/a do miestnosti + %1$s opustil/a miestnosť %1$s odmietol pozvanie %1$s vykázal %2$s %1$s povolil vstup %2$s @@ -177,15 +177,33 @@ Pozvanie od používateľa %1$s. Dôvod: %2$s %1$s pozval používateľa %2$s. Dôvod: %3$s %1$s vás pozval. Dôvod: %2$s - %1$s sa pridal. Dôvod: %2$s - Používateľ %1$s odišiel. Dôvod: %2$s + %1$s sa pripojil/a do miestnosti. Dôvod: %2$s + Používateľ %1$s odišiel z miestnosti. Dôvod: %2$s %1$s odmietol pozvanie. Dôvod: %2$s %1$s vyhodil používateľa %2$s. Dôvod: %3$s %1$s znovu pridaný používateľom %2$s. Dôvod: %3$s %1$s vyhodil %2$s. Dôvod: %3$s %1$s poslal pozvánku používateľovi %2$s, aby sa pripojil na miestnosť. Dôvod: %3$s - %1$s odvolal pozvánku pre používateľa %2$s, aby sa pripojil na miestnosť. Dôvod: %3$s + %1$s odvolal/a pozvánku pre používateľa %2$s na pripojenie sa na miestnosť. Dôvod: %3$s %1$s prijal pozvanie od používateľa %2$s. Dôvod: %3$s - %1$s odoprel pozvánku používateľa %2$s. Dôvod: %3$s + %1$s odoprel/a pozvánku používateľa %2$s. Dôvod: %3$s + + %1$s pridal/a adresu %2$s pre túto miestnosť. + %1$s pridal/a adresy %2$s pre túto miestnosť. + %1$s pridal/a adresy %2$s pre túto miestnosť. + + + + %1$s odstránil/a adresu %2$s pre túto miestnosť. + %1$s odstránil/a adresy %2$s pre túto miestnosť. + %1$s odstránil/a adresy %2$s pre túto miestnosť. + + + %1$s pridal/a adresu %2$s a odstránil/a adresu %3$s pre túto miestnosť. + + %1$s nastavil/a hlavnú adresu tejto miestnosti na %2$s. + %1$s odstránil/a hlavnú adresu pre túto miestnosť. + + %1$s povolil/a hosťom///návštevníkom prístup do tejto miestnosti. diff --git a/matrix-sdk-android/src/main/res/values/strings.xml b/matrix-sdk-android/src/main/res/values/strings.xml index 50169fd982..69907e5835 100644 --- a/matrix-sdk-android/src/main/res/values/strings.xml +++ b/matrix-sdk-android/src/main/res/values/strings.xml @@ -5,6 +5,7 @@ %1$s sent a sticker. %s\'s invitation + %1$s created the room %1$s invited %2$s %1$s invited you %1$s joined the 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 deleted file mode 100644 index 6eb46fd7df..0000000000 --- a/matrix-sdk-android/src/main/res/values/strings_RiotX.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - diff --git a/tools/import_from_riot.sh b/tools/import_from_riot.sh deleted file mode 100755 index a2b68a347c..0000000000 --- a/tools/import_from_riot.sh +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env bash - -# -# 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. -# - -# Exit on any error -set -e - -echo -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 -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-cs/strings.xml ./matrix-sdk-android/src/main/res/values-cs/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-da/strings.xml ./matrix-sdk-android/src/main/res/values-da/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-de/strings.xml ./matrix-sdk-android/src/main/res/values-de/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-el/strings.xml ./matrix-sdk-android/src/main/res/values-el/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-eo/strings.xml ./matrix-sdk-android/src/main/res/values-eo/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-en-rGB/strings.xml ./matrix-sdk-android/src/main/res/values-en-rGB/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-es/strings.xml ./matrix-sdk-android/src/main/res/values-es/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-es-rMX/strings.xml ./matrix-sdk-android/src/main/res/values-es-rMX/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-eu/strings.xml ./matrix-sdk-android/src/main/res/values-eu/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-fa/strings.xml ./matrix-sdk-android/src/main/res/values-fa/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-fi/strings.xml ./matrix-sdk-android/src/main/res/values-fi/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-fr/strings.xml ./matrix-sdk-android/src/main/res/values-fr/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-gl/strings.xml ./matrix-sdk-android/src/main/res/values-gl/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-hu/strings.xml ./matrix-sdk-android/src/main/res/values-hu/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-id/strings.xml ./matrix-sdk-android/src/main/res/values-id/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-in/strings.xml ./matrix-sdk-android/src/main/res/values-in/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-is/strings.xml ./matrix-sdk-android/src/main/res/values-is/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-it/strings.xml ./matrix-sdk-android/src/main/res/values-it/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-ja/strings.xml ./matrix-sdk-android/src/main/res/values-ja/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-ko/strings.xml ./matrix-sdk-android/src/main/res/values-ko/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-lv/strings.xml ./matrix-sdk-android/src/main/res/values-lv/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-nl/strings.xml ./matrix-sdk-android/src/main/res/values-nl/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-nn/strings.xml ./matrix-sdk-android/src/main/res/values-nn/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-pl/strings.xml ./matrix-sdk-android/src/main/res/values-pl/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-pt/strings.xml ./matrix-sdk-android/src/main/res/values-pt/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-pt-rBR/strings.xml ./matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-ru/strings.xml ./matrix-sdk-android/src/main/res/values-ru/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-sk/strings.xml ./matrix-sdk-android/src/main/res/values-sk/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-sq/strings.xml ./matrix-sdk-android/src/main/res/values-sq/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-te/strings.xml ./matrix-sdk-android/src/main/res/values-te/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-th/strings.xml ./matrix-sdk-android/src/main/res/values-th/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-uk/strings.xml ./matrix-sdk-android/src/main/res/values-uk/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-vls/strings.xml ./matrix-sdk-android/src/main/res/values-vls/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-zh-rCN/strings.xml ./matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-zh-rTW/strings.xml ./matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml - -echo -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 -cp ../riot-android/vector/src/main/res/values-bs/strings.xml ./vector/src/main/res/values-bs/strings.xml -cp ../riot-android/vector/src/main/res/values-ca/strings.xml ./vector/src/main/res/values-ca/strings.xml -cp ../riot-android/vector/src/main/res/values-cs/strings.xml ./vector/src/main/res/values-cs/strings.xml -cp ../riot-android/vector/src/main/res/values-cy/strings.xml ./vector/src/main/res/values-cy/strings.xml -cp ../riot-android/vector/src/main/res/values-da/strings.xml ./vector/src/main/res/values-da/strings.xml -cp ../riot-android/vector/src/main/res/values-de/strings.xml ./vector/src/main/res/values-de/strings.xml -cp ../riot-android/vector/src/main/res/values-el/strings.xml ./vector/src/main/res/values-el/strings.xml -cp ../riot-android/vector/src/main/res/values-eo/strings.xml ./vector/src/main/res/values-eo/strings.xml -cp ../riot-android/vector/src/main/res/values-es/strings.xml ./vector/src/main/res/values-es/strings.xml -cp ../riot-android/vector/src/main/res/values-es-rMX/strings.xml ./vector/src/main/res/values-es-rMX/strings.xml -cp ../riot-android/vector/src/main/res/values-eu/strings.xml ./vector/src/main/res/values-eu/strings.xml -cp ../riot-android/vector/src/main/res/values-fa/strings.xml ./vector/src/main/res/values-fa/strings.xml -cp ../riot-android/vector/src/main/res/values-fi/strings.xml ./vector/src/main/res/values-fi/strings.xml -cp ../riot-android/vector/src/main/res/values-fy/strings.xml ./vector/src/main/res/values-fy/strings.xml -cp ../riot-android/vector/src/main/res/values-fr/strings.xml ./vector/src/main/res/values-fr/strings.xml -cp ../riot-android/vector/src/main/res/values-fr-rCA/strings.xml ./vector/src/main/res/values-fr-rCA/strings.xml -cp ../riot-android/vector/src/main/res/values-gl/strings.xml ./vector/src/main/res/values-gl/strings.xml -cp ../riot-android/vector/src/main/res/values-hu/strings.xml ./vector/src/main/res/values-hu/strings.xml -cp ../riot-android/vector/src/main/res/values-id/strings.xml ./vector/src/main/res/values-id/strings.xml -cp ../riot-android/vector/src/main/res/values-in/strings.xml ./vector/src/main/res/values-in/strings.xml -cp ../riot-android/vector/src/main/res/values-is/strings.xml ./vector/src/main/res/values-is/strings.xml -cp ../riot-android/vector/src/main/res/values-it/strings.xml ./vector/src/main/res/values-it/strings.xml -cp ../riot-android/vector/src/main/res/values-ja/strings.xml ./vector/src/main/res/values-ja/strings.xml -cp ../riot-android/vector/src/main/res/values-ko/strings.xml ./vector/src/main/res/values-ko/strings.xml -cp ../riot-android/vector/src/main/res/values-lv/strings.xml ./vector/src/main/res/values-lv/strings.xml -cp ../riot-android/vector/src/main/res/values-nb-rNO/strings.xml ./vector/src/main/res/values-nb-rNO/strings.xml -cp ../riot-android/vector/src/main/res/values-nl/strings.xml ./vector/src/main/res/values-nl/strings.xml -cp ../riot-android/vector/src/main/res/values-nn/strings.xml ./vector/src/main/res/values-nn/strings.xml -cp ../riot-android/vector/src/main/res/values-pl/strings.xml ./vector/src/main/res/values-pl/strings.xml -cp ../riot-android/vector/src/main/res/values-pt/strings.xml ./vector/src/main/res/values-pt/strings.xml -cp ../riot-android/vector/src/main/res/values-pt-rBR/strings.xml ./vector/src/main/res/values-pt-rBR/strings.xml -cp ../riot-android/vector/src/main/res/values-ro/strings.xml ./vector/src/main/res/values-ro/strings.xml -cp ../riot-android/vector/src/main/res/values-ru/strings.xml ./vector/src/main/res/values-ru/strings.xml -cp ../riot-android/vector/src/main/res/values-sk/strings.xml ./vector/src/main/res/values-sk/strings.xml -cp ../riot-android/vector/src/main/res/values-sq/strings.xml ./vector/src/main/res/values-sq/strings.xml -cp ../riot-android/vector/src/main/res/values-sr/strings.xml ./vector/src/main/res/values-sr/strings.xml -cp ../riot-android/vector/src/main/res/values-te/strings.xml ./vector/src/main/res/values-te/strings.xml -cp ../riot-android/vector/src/main/res/values-th/strings.xml ./vector/src/main/res/values-th/strings.xml -cp ../riot-android/vector/src/main/res/values-tlh/strings.xml ./vector/src/main/res/values-tlh/strings.xml -cp ../riot-android/vector/src/main/res/values-tr/strings.xml ./vector/src/main/res/values-tr/strings.xml -cp ../riot-android/vector/src/main/res/values-uk/strings.xml ./vector/src/main/res/values-uk/strings.xml -cp ../riot-android/vector/src/main/res/values-vls/strings.xml ./vector/src/main/res/values-vls/strings.xml -cp ../riot-android/vector/src/main/res/values-zh-rCN/strings.xml ./vector/src/main/res/values-zh-rCN/strings.xml -cp ../riot-android/vector/src/main/res/values-zh-rTW/strings.xml ./vector/src/main/res/values-zh-rTW/strings.xml - -echo -echo "Success!" diff --git a/tools/templates/RiotXFeature/root/src/app_package/ViewModel.kt.ftl b/tools/templates/RiotXFeature/root/src/app_package/ViewModel.kt.ftl index f4090b40e6..1d2ec0a069 100644 --- a/tools/templates/RiotXFeature/root/src/app_package/ViewModel.kt.ftl +++ b/tools/templates/RiotXFeature/root/src/app_package/ViewModel.kt.ftl @@ -6,6 +6,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.riotx.core.extensions.exhaustive import im.vector.riotx.core.platform.VectorViewModel <#if createViewEvents> @@ -38,7 +39,8 @@ class ${viewModelClass} @AssistedInject constructor(@Assisted initialState: ${vi } override fun handle(action: ${actionClass}) { - //TODO - } + when (action) { + }.exhaustive + } } diff --git a/vector/build.gradle b/vector/build.gradle index 459b297fd6..6f1afc5038 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -15,7 +15,7 @@ androidExtensions { } ext.versionMajor = 0 -ext.versionMinor = 20 +ext.versionMinor = 21 ext.versionPatch = 0 static def getGitTimestamp() { @@ -260,6 +260,7 @@ dependencies { def autofill_version = "1.0.0" def work_version = '2.3.3' def arch_version = '2.1.0' + def lifecycle_version = '2.2.0' implementation project(":matrix-sdk-android") implementation project(":matrix-sdk-android-rx") @@ -282,6 +283,7 @@ dependencies { implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.7.0" implementation "com.squareup.moshi:moshi-adapters:$moshi_version" + implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" // Log diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index ae0ffa1f91..7c2939707f 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -161,6 +161,8 @@ android:name=".features.attachments.preview.AttachmentsPreviewActivity" android:theme="@style/AppTheme.AttachmentsPreview" /> + + () { + + class Holder : VectorEpoxyHolder() +} diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/attributes/ButtonStyle.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/attributes/ButtonStyle.kt new file mode 100644 index 0000000000..d6cc90c0e9 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/attributes/ButtonStyle.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.core.epoxy.attributes + +enum class ButtonStyle { + POSITIVE, + DESTRUCTIVE +} diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/attributes/ButtonType.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/attributes/ButtonType.kt new file mode 100644 index 0000000000..b3cbe9cd84 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/attributes/ButtonType.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.core.epoxy.attributes + +enum class ButtonType { + NO_BUTTON, + NORMAL, + SWITCH +} diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/attributes/IconMode.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/attributes/IconMode.kt new file mode 100644 index 0000000000..c6b1ebf505 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/attributes/IconMode.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.core.epoxy.attributes + +enum class IconMode { + NONE, + INFO, + ERROR +} diff --git a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt index 557bc93bb1..57e5c6381f 100644 --- a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt @@ -19,6 +19,7 @@ package im.vector.riotx.core.error import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.api.failure.isInvalidPassword +import im.vector.matrix.android.api.session.identity.IdentityServiceError import im.vector.riotx.R import im.vector.riotx.core.resources.StringProvider import java.net.HttpURLConnection @@ -37,6 +38,7 @@ class DefaultErrorFormatter @Inject constructor( override fun toHumanReadable(throwable: Throwable?): String { return when (throwable) { null -> null + is IdentityServiceError -> identityServerError(throwable) is Failure.NetworkConnection -> { when { throwable.ioException is SocketTimeoutException -> @@ -107,4 +109,16 @@ class DefaultErrorFormatter @Inject constructor( stringProvider.getQuantityString(R.plurals.login_error_limit_exceeded_retry_after, delaySeconds, delaySeconds) } } + + private fun identityServerError(identityServiceError: IdentityServiceError): String { + return stringProvider.getString(when (identityServiceError) { + IdentityServiceError.OutdatedIdentityServer -> R.string.identity_server_error_outdated_identity_server + IdentityServiceError.OutdatedHomeServer -> R.string.identity_server_error_outdated_home_server + IdentityServiceError.NoIdentityServerConfigured -> R.string.identity_server_error_no_identity_server_configured + IdentityServiceError.TermsNotSignedException -> R.string.identity_server_error_terms_not_signed + IdentityServiceError.BulkLookupSha256NotSupported -> R.string.identity_server_error_bulk_sha256_not_supported + IdentityServiceError.BindingError -> R.string.identity_server_error_binding_error + IdentityServiceError.NoCurrentBindingError -> R.string.identity_server_error_no_current_binding_error + }) + } } diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt index f9f5d3b3d2..b74f143e17 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt @@ -16,6 +16,7 @@ package im.vector.riotx.core.extensions +import android.app.Activity import android.os.Parcelable import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentTransaction @@ -59,3 +60,8 @@ fun VectorBaseActivity.addFragmentToBackstack(frameId: Int, fun VectorBaseActivity.hideKeyboard() { currentFocus?.hideKeyboard() } + +fun Activity.restart() { + startActivity(intent) + finish() +} diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Collections.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Collections.kt new file mode 100644 index 0000000000..af5d5babb6 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/extensions/Collections.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.core.extensions + +inline fun List.nextOrNull(index: Int) = getOrNull(index + 1) +inline fun List.prevOrNull(index: Int) = getOrNull(index - 1) diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt index b93ab3fdce..c28dcf12d3 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt @@ -80,5 +80,12 @@ fun VectorBaseFragment.addChildFragmentToBackstack(frameId: Int, } } +/** + * Return a list of all child Fragments, recursively + */ +fun Fragment.getAllChildFragments(): List { + return listOf(this) + childFragmentManager.fragments.map { it.getAllChildFragments() }.flatten() +} + // Define a missing constant const val POP_BACK_STACK_EXCLUSIVE = 0 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 3b3132229c..3762c52d45 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 @@ -21,6 +21,7 @@ import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.EpoxyController +import com.airbnb.epoxy.EpoxyVisibilityTracker import im.vector.riotx.R import im.vector.riotx.features.themes.ThemeUtils @@ -61,3 +62,5 @@ fun RecyclerView.configureWith(epoxyController: EpoxyController, fun RecyclerView.cleanup() { adapter = null } + +fun RecyclerView.trackItemsVisibilityChange() = EpoxyVisibilityTracker().attach(this) diff --git a/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt index e8e8f21259..58ec4b22c6 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt @@ -26,7 +26,6 @@ import im.vector.matrix.android.api.session.Session import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.hideKeyboard -import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.activity.* import javax.inject.Inject @@ -108,15 +107,4 @@ abstract class SimpleFragmentActivity : VectorBaseActivity() { } super.onBackPressed() } - - protected fun VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) { - viewEvents - .observe() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - hideWaitingView() - observer(it) - } - .disposeOnDestroy() - } } diff --git a/vector/src/main/java/im/vector/riotx/core/platform/StateView.kt b/vector/src/main/java/im/vector/riotx/core/platform/StateView.kt index 4c5a987b4b..bc24874f9f 100755 --- a/vector/src/main/java/im/vector/riotx/core/platform/StateView.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/StateView.kt @@ -21,6 +21,7 @@ import android.graphics.drawable.Drawable import android.util.AttributeSet import android.view.View import android.widget.FrameLayout +import androidx.core.view.isVisible import im.vector.riotx.R import kotlinx.android.synthetic.main.view_state.view.* @@ -31,6 +32,7 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet? object Content : State() object Loading : State() data class Empty(val title: CharSequence? = null, val image: Drawable? = null, val message: CharSequence? = null) : State() + data class Error(val message: CharSequence? = null) : State() } @@ -59,34 +61,21 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet? } private fun update(newState: State) { + progressBar.isVisible = newState is State.Loading + errorView.isVisible = newState is State.Error + emptyView.isVisible = newState is State.Empty + contentView?.isVisible = newState is State.Content + when (newState) { - is State.Content -> { - progressBar.visibility = View.INVISIBLE - errorView.visibility = View.INVISIBLE - emptyView.visibility = View.INVISIBLE - contentView?.visibility = View.VISIBLE - } - is State.Loading -> { - progressBar.visibility = View.VISIBLE - errorView.visibility = View.INVISIBLE - emptyView.visibility = View.INVISIBLE - contentView?.visibility = View.INVISIBLE - } + is State.Content -> Unit + is State.Loading -> Unit is State.Empty -> { - progressBar.visibility = View.INVISIBLE - errorView.visibility = View.INVISIBLE - emptyView.visibility = View.VISIBLE emptyImageView.setImageDrawable(newState.image) emptyMessageView.text = newState.message emptyTitleView.text = newState.title - contentView?.visibility = View.INVISIBLE } is State.Error -> { - progressBar.visibility = View.INVISIBLE - errorView.visibility = View.VISIBLE - emptyView.visibility = View.INVISIBLE errorMessageView.text = newState.message - contentView?.visibility = View.INVISIBLE } } } 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 08cf8e57e1..770a63a3fa 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 @@ -70,6 +70,7 @@ 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 +import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.Disposable import timber.log.Timber @@ -94,6 +95,18 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { protected val viewModelProvider get() = ViewModelProvider(this, viewModelFactory) + // TODO Other Activity should use this also + protected fun VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) { + viewEvents + .observe() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + hideWaitingView() + observer(it) + } + .disposeOnDestroy() + } + /* ========================================================================================== * DATA * ========================================================================================== */ @@ -179,7 +192,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { } }) - sessionListener = getVectorComponent().sessionListener() + sessionListener = vectorComponent.sessionListener() sessionListener.globalErrorLiveData.observeEvent(this) { handleGlobalError(it) } @@ -217,8 +230,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { handleInvalidToken(globalError) is GlobalError.ConsentNotGivenError -> consentNotGivenHelper.displayDialog(globalError.consentUri, - activeSessionHolder.getActiveSession().sessionParams.homeServerConnectionConfig.homeServerUri.host - ?: "") + activeSessionHolder.getActiveSession().sessionParams.homeServerHost ?: "") } } diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt index 6eb316456a..c4dcb0d996 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt @@ -39,6 +39,7 @@ import com.airbnb.mvrx.BaseMvRxFragment import com.airbnb.mvrx.MvRx import com.bumptech.glide.util.Util.assertMainThread import com.google.android.material.snackbar.Snackbar +import com.jakewharton.rxbinding3.view.clicks import im.vector.riotx.R import im.vector.riotx.core.di.DaggerScreenComponent import im.vector.riotx.core.di.HasScreenInjector @@ -49,6 +50,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.Disposable import timber.log.Timber +import java.util.concurrent.TimeUnit abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector { @@ -249,6 +251,18 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector { .disposeOnDestroyView() } + /* ========================================================================================== + * Views + * ========================================================================================== */ + + protected fun View.debouncedClicks(onClicked: () -> Unit) { + clicks() + .throttleFirst(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { onClicked() } + .disposeOnDestroyView() + } + /* ========================================================================================== * MENU MANAGEMENT * ========================================================================================== */ diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt index e82e8b3856..11cd9c485e 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt @@ -30,7 +30,7 @@ import io.reactivex.Single abstract class VectorViewModel(initialState: S) : BaseMvRxViewModel(initialState, false) { - interface Factory { + interface Factory { fun create(state: S): BaseMvRxViewModel } diff --git a/vector/src/main/java/im/vector/riotx/core/pushers/PushersManager.kt b/vector/src/main/java/im/vector/riotx/core/pushers/PushersManager.kt index aa1fbaca54..69367e529c 100644 --- a/vector/src/main/java/im/vector/riotx/core/pushers/PushersManager.kt +++ b/vector/src/main/java/im/vector/riotx/core/pushers/PushersManager.kt @@ -45,7 +45,7 @@ class PushersManager @Inject constructor( profileTag, localeProvider.current().language, appNameProvider.getAppName(), - currentSession.sessionParams.credentials.deviceId ?: "MOBILE", + currentSession.sessionParams.deviceId ?: "MOBILE", stringProvider.getString(R.string.pusher_http_url), append = false, withEventIdOnly = true diff --git a/vector/src/main/java/im/vector/riotx/core/resources/UserPreferencesProvider.kt b/vector/src/main/java/im/vector/riotx/core/resources/UserPreferencesProvider.kt index ac379a8f98..fa4b09ed4c 100644 --- a/vector/src/main/java/im/vector/riotx/core/resources/UserPreferencesProvider.kt +++ b/vector/src/main/java/im/vector/riotx/core/resources/UserPreferencesProvider.kt @@ -29,6 +29,10 @@ class UserPreferencesProvider @Inject constructor(private val vectorPreferences: return vectorPreferences.showReadReceipts() } + fun shouldShowRedactedMessages(): Boolean { + return vectorPreferences.showRedactedMessages() + } + fun shouldShowLongClickOnRoomHelp(): Boolean { return vectorPreferences.shouldShowLongClickOnRoomHelp() } diff --git a/vector/src/main/java/im/vector/riotx/core/ui/model/Size.kt b/vector/src/main/java/im/vector/riotx/core/ui/model/Size.kt index 65ab0ad2b2..4a07bb2cea 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/model/Size.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/model/Size.kt @@ -16,5 +16,7 @@ package im.vector.riotx.core.ui.model +import androidx.annotation.Px + // android.util.Size in API 21+ -data class Size(val width: Int, val height: Int) +data class Size(@Px val width: Int, @Px val height: Int) diff --git a/vector/src/main/java/im/vector/riotx/core/utils/DimensionConverter.kt b/vector/src/main/java/im/vector/riotx/core/utils/DimensionConverter.kt index 826d9a495a..01cd6a4f8f 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/DimensionConverter.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/DimensionConverter.kt @@ -17,10 +17,12 @@ package im.vector.riotx.core.utils import android.content.res.Resources import android.util.TypedValue +import androidx.annotation.Px import javax.inject.Inject class DimensionConverter @Inject constructor(val resources: Resources) { + @Px fun dpToPx(dp: Int): Int { return TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, @@ -29,6 +31,7 @@ class DimensionConverter @Inject constructor(val resources: Resources) { ).toInt() } + @Px fun spToPx(sp: Int): Int { return TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_SP, @@ -36,4 +39,8 @@ class DimensionConverter @Inject constructor(val resources: Resources) { resources.displayMetrics ).toInt() } + + fun pxToDp(@Px px: Int): Int { + return (px.toFloat() / resources.displayMetrics.density).toInt() + } } diff --git a/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt b/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt index afb7c4586a..e46d756523 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt @@ -256,7 +256,11 @@ fun shareMedia(context: Context, file: File, mediaMimeType: String?) { sendIntent.type = mediaMimeType sendIntent.putExtra(Intent.EXTRA_STREAM, mediaUri) - context.startActivity(sendIntent) + try { + context.startActivity(sendIntent) + } catch (activityNotFoundException: ActivityNotFoundException) { + context.toast(R.string.error_no_external_application_found) + } } } diff --git a/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt index d82134caf5..9e5af038ef 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt @@ -33,9 +33,6 @@ import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import im.vector.riotx.R import im.vector.riotx.features.notifications.NotificationUtils -import im.vector.riotx.features.settings.VectorLocale -import timber.log.Timber -import java.util.Locale /** * Tells if the application ignores battery optimizations. @@ -53,6 +50,10 @@ fun isIgnoringBatteryOptimizations(context: Context): Boolean { || (context.getSystemService(Context.POWER_SERVICE) as PowerManager?)?.isIgnoringBatteryOptimizations(context.packageName) == true } +fun isAirplaneModeOn(context: Context): Boolean { + return Settings.Global.getInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0) != 0 +} + /** * display the system dialog for granting this permission. If previously granted, the * system will not show it (so you should call this method). @@ -90,24 +91,6 @@ fun copyToClipboard(context: Context, text: CharSequence, showToast: Boolean = t } } -/** - * Provides the device locale - * - * @return the device locale - */ -fun getDeviceLocale(context: Context): Locale { - return try { - val packageManager = context.packageManager - val resources = packageManager.getResourcesForApplication("android") - @Suppress("DEPRECATION") - resources.configuration.locale - } catch (e: Exception) { - Timber.e(e, "## getDeviceLocale() failed") - // Fallback to application locale - VectorLocale.applicationLocale - } -} - /** * Shows notification settings for the current app. * In android O will directly opens the notification settings, in lower version it will show the App settings diff --git a/vector/src/main/java/im/vector/riotx/core/utils/UrlUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/UrlUtils.kt index 3de555f66e..0361fc9d71 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/UrlUtils.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/UrlUtils.kt @@ -26,3 +26,14 @@ fun String.isValidUrl(): Boolean { false } } + +/** + * Ensure string starts with "http". If it is not the case, "https://" is added, only if the String is not empty + */ +internal fun String.ensureProtocol(): String { + return when { + isEmpty() -> this + !startsWith("http") -> "https://$this" + else -> this + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewActivity.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewActivity.kt index 6c91f70131..21febd81a9 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewActivity.kt @@ -43,7 +43,7 @@ class AttachmentsPreviewActivity : VectorBaseActivity(), ToolbarConfigurable { } fun getOutput(intent: Intent): List { - return intent.getParcelableArrayListExtra(ATTACHMENTS_PREVIEW_RESULT) ?: emptyList() + return intent.getParcelableArrayListExtra(ATTACHMENTS_PREVIEW_RESULT).orEmpty() } fun getKeepOriginalSize(intent: Intent): Boolean { diff --git a/vector/src/main/java/im/vector/riotx/features/configuration/VectorConfiguration.kt b/vector/src/main/java/im/vector/riotx/features/configuration/VectorConfiguration.kt index a4b7ca263d..2ef69890ed 100644 --- a/vector/src/main/java/im/vector/riotx/features/configuration/VectorConfiguration.kt +++ b/vector/src/main/java/im/vector/riotx/features/configuration/VectorConfiguration.kt @@ -30,62 +30,30 @@ import javax.inject.Inject /** * Handle locale configuration change, such as theme, font size and locale chosen by the user */ - class VectorConfiguration @Inject constructor(private val context: Context) { - // TODO Import mLanguageReceiver From Riot? fun onConfigurationChanged() { if (Locale.getDefault().toString() != VectorLocale.applicationLocale.toString()) { Timber.v("## onConfigurationChanged(): the locale has been updated to ${Locale.getDefault()}") Timber.v("## onConfigurationChanged(): restore the expected value ${VectorLocale.applicationLocale}") - updateApplicationSettings(VectorLocale.applicationLocale, - FontScale.getFontScalePrefValue(context), - ThemeUtils.getApplicationTheme(context)) + Locale.setDefault(VectorLocale.applicationLocale) } } - private fun updateApplicationSettings(locale: Locale, textSize: String, theme: String) { - VectorLocale.saveApplicationLocale(context, locale) - FontScale.saveFontScale(context, textSize) - Locale.setDefault(locale) - - val config = Configuration(context.resources.configuration) - @Suppress("DEPRECATION") - config.locale = locale - config.fontScale = FontScale.getFontScale(context) - @Suppress("DEPRECATION") - context.resources.updateConfiguration(config, context.resources.displayMetrics) - - ThemeUtils.setApplicationTheme(context, theme) - // TODO PhoneNumberUtils.onLocaleUpdate() - } - - /** - * Update the application theme - * - * @param theme the new theme - */ - fun updateApplicationTheme(theme: String) { - ThemeUtils.setApplicationTheme(context, theme) - updateApplicationSettings(VectorLocale.applicationLocale, - FontScale.getFontScalePrefValue(context), - theme) - } - /** * Init the configuration from the saved one */ fun initConfiguration() { VectorLocale.init(context) val locale = VectorLocale.applicationLocale - val fontScale = FontScale.getFontScale(context) + val fontScale = FontScale.getFontScaleValue(context) val theme = ThemeUtils.getApplicationTheme(context) Locale.setDefault(locale) val config = Configuration(context.resources.configuration) @Suppress("DEPRECATION") config.locale = locale - config.fontScale = fontScale + config.fontScale = fontScale.scale @Suppress("DEPRECATION") context.resources.updateConfiguration(config, context.resources.displayMetrics) @@ -93,16 +61,6 @@ class VectorConfiguration @Inject constructor(private val context: Context) { ThemeUtils.setApplicationTheme(context, theme) } - /** - * Update the application locale - * - * @param locale - */ - // TODO Call from LanguagePickerActivity - fun updateApplicationLocale(locale: Locale) { - updateApplicationSettings(locale, FontScale.getFontScalePrefValue(context), ThemeUtils.getApplicationTheme(context)) - } - /** * Compute a localised context * @@ -115,7 +73,7 @@ class VectorConfiguration @Inject constructor(private val context: Context) { val resources = context.resources val locale = VectorLocale.applicationLocale val configuration = resources.configuration - configuration.fontScale = FontScale.getFontScale(context) + configuration.fontScale = FontScale.getFontScaleValue(context).scale if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { configuration.setLocale(locale) @@ -142,10 +100,9 @@ class VectorConfiguration @Inject constructor(private val context: Context) { * Compute the locale status value * @return the local status value */ - // TODO Create data class for this fun getHash(): String { return (VectorLocale.applicationLocale.toString() - + "_" + FontScale.getFontScalePrefValue(context) + + "_" + FontScale.getFontScaleValue(context).preferenceValue + "_" + ThemeUtils.getApplicationTheme(context)) } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt index 1fec404f7d..20046fa115 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt @@ -197,7 +197,7 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(private val s endIconResourceId(R.drawable.e2e_warning) } else { if (isSignatureValid) { - if (session.sessionParams.credentials.deviceId == it.deviceId) { + if (session.sessionParams.deviceId == it.deviceId) { description(stringProvider.getString(R.string.keys_backup_settings_valid_signature_from_this_device)) endIconResourceId(R.drawable.e2e_verified) } else { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt index 3522c5a752..93d6f43763 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt @@ -164,16 +164,16 @@ class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment() @OnClick(R.id.keys_backup_setup_step2_button) fun doNext() { when { - viewModel.passphrase.value.isNullOrEmpty() -> { + viewModel.passphrase.value.isNullOrEmpty() -> { viewModel.passphraseError.value = context?.getString(R.string.passphrase_empty_error_message) } viewModel.passphrase.value != viewModel.confirmPassphrase.value -> { viewModel.confirmPassphraseError.value = context?.getString(R.string.passphrase_passphrase_does_not_match) } - viewModel.passwordStrength.value?.score ?: 0 < 4 -> { + viewModel.passwordStrength.value?.score ?: 0 < 4 -> { viewModel.passphraseError.value = context?.getString(R.string.passphrase_passphrase_too_weak) } - else -> { + else -> { viewModel.megolmBackupCreationInfo = null viewModel.prepareRecoveryKey(activity!!, viewModel.passphrase.value) @@ -190,7 +190,7 @@ class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment() viewModel.prepareRecoveryKey(activity!!, null) } - else -> { + else -> { // User has entered a passphrase but want to skip this step. viewModel.passphraseError.value = context?.getString(R.string.keys_backup_passphrase_not_empty_error_message) } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt index 9175d6c081..1478b99d3b 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt @@ -123,7 +123,7 @@ class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment() .joinToString(" ") } - it.setOnClickListener { + it.debouncedClicks { copyToClipboard(activity!!, recoveryKey) } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStorageKeyFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStorageKeyFragment.kt index db4c5230fd..848166381e 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStorageKeyFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStorageKeyFragment.kt @@ -23,7 +23,6 @@ import android.view.View import android.view.inputmethod.EditorInfo import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.view.clicks import com.jakewharton.rxbinding3.widget.editorActionEvents import com.jakewharton.rxbinding3.widget.textChanges import im.vector.matrix.android.api.extensions.tryThis @@ -33,7 +32,6 @@ import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.utils.startImportTextFromFileIntent import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.fragment_ssss_access_from_key.* -import kotlinx.android.synthetic.main.fragment_ssss_access_from_passphrase.* import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -67,13 +65,7 @@ class SharedSecuredStorageKeyFragment @Inject constructor( } .disposeOnDestroyView() - ssss_key_use_file.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - startImportTextFromFileIntent(this, IMPORT_FILE_REQ) - } - .disposeOnDestroyView() + ssss_key_use_file.debouncedClicks { startImportTextFromFileIntent(this, IMPORT_FILE_REQ) } sharedViewModel.observeViewEvents { when (it) { @@ -83,13 +75,7 @@ class SharedSecuredStorageKeyFragment @Inject constructor( } } - ssss_key_submit.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - submit() - } - .disposeOnDestroyView() + ssss_key_submit.debouncedClicks { submit() } } fun submit() { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt index 82d27aea1b..f5eb450fe1 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt @@ -22,7 +22,6 @@ import android.view.inputmethod.EditorInfo import androidx.core.text.toSpannable import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.view.clicks import com.jakewharton.rxbinding3.widget.editorActionEvents import com.jakewharton.rxbinding3.widget.textChanges import im.vector.riotx.R @@ -37,7 +36,7 @@ import javax.inject.Inject class SharedSecuredStoragePassphraseFragment @Inject constructor( private val colorProvider: ColorProvider -): VectorBaseFragment() { +) : VectorBaseFragment() { override fun getLayoutResId() = R.layout.fragment_ssss_access_from_passphrase @@ -83,29 +82,9 @@ class SharedSecuredStoragePassphraseFragment @Inject constructor( } } - ssss_passphrase_submit.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - submit() - } - .disposeOnDestroyView() - - ssss_passphrase_use_key.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - sharedViewModel.handle(SharedSecureStorageAction.UseKey) - } - .disposeOnDestroyView() - - ssss_view_show_password.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - sharedViewModel.handle(SharedSecureStorageAction.TogglePasswordVisibility) - } - .disposeOnDestroyView() + ssss_passphrase_submit.debouncedClicks { submit() } + ssss_passphrase_use_key.debouncedClicks { sharedViewModel.handle(SharedSecureStorageAction.UseKey) } + ssss_view_show_password.debouncedClicks { sharedViewModel.handle(SharedSecureStorageAction.TogglePasswordVisibility) } } fun submit() { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapAccountPasswordFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapAccountPasswordFragment.kt index abe6e54092..fcedd2926e 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapAccountPasswordFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapAccountPasswordFragment.kt @@ -22,7 +22,6 @@ import android.view.inputmethod.EditorInfo import androidx.core.text.toSpannable import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.view.clicks import com.jakewharton.rxbinding3.widget.editorActionEvents import com.jakewharton.rxbinding3.widget.textChanges import im.vector.riotx.R @@ -75,21 +74,8 @@ class BootstrapAccountPasswordFragment @Inject constructor( } .disposeOnDestroyView() - ssss_view_show_password.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) - } - .disposeOnDestroyView() - - bootstrapPasswordButton.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - submit() - } - .disposeOnDestroyView() + ssss_view_show_password.debouncedClicks { sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) } + bootstrapPasswordButton.debouncedClicks { submit() } withState(sharedViewModel) { state -> (state.step as? BootstrapStep.AccountPassword)?.failure?.let { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConclusionFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConclusionFragment.kt index d84283b14c..fd7ba2dbb7 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConclusionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConclusionFragment.kt @@ -21,14 +21,11 @@ import android.view.View import androidx.core.text.toSpannable import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.view.clicks import im.vector.riotx.R import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.utils.colorizeMatchingText -import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.fragment_bootstrap_conclusion.* -import java.util.concurrent.TimeUnit import javax.inject.Inject class BootstrapConclusionFragment @Inject constructor( @@ -42,13 +39,7 @@ class BootstrapConclusionFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - bootstrapConclusionContinue.clickableView.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - sharedViewModel.handle(BootstrapActions.Completed) - } - .disposeOnDestroyView() + bootstrapConclusionContinue.clickableView.debouncedClicks { sharedViewModel.handle(BootstrapActions.Completed) } } override fun invalidate() = withState(sharedViewModel) { state -> diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt index d09eafee58..df4d741bf1 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt @@ -23,7 +23,6 @@ import androidx.core.text.toSpannable import androidx.core.view.isGone import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.view.clicks import com.jakewharton.rxbinding3.widget.editorActionEvents import com.jakewharton.rxbinding3.widget.textChanges import im.vector.riotx.R @@ -88,21 +87,8 @@ class BootstrapConfirmPassphraseFragment @Inject constructor( // } } - ssss_view_show_password.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) - } - .disposeOnDestroyView() - - bootstrapSubmit.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - submit() - } - .disposeOnDestroyView() + ssss_view_show_password.debouncedClicks { sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) } + bootstrapSubmit.debouncedClicks { submit() } } private fun submit() = withState(sharedViewModel) { state -> diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapEnterPassphraseFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapEnterPassphraseFragment.kt index 952b0e5d03..d1eee9ff3f 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapEnterPassphraseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapEnterPassphraseFragment.kt @@ -22,7 +22,6 @@ import android.view.inputmethod.EditorInfo import androidx.core.text.toSpannable import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.view.clicks import com.jakewharton.rxbinding3.widget.editorActionEvents import com.jakewharton.rxbinding3.widget.textChanges import im.vector.riotx.R @@ -83,21 +82,8 @@ class BootstrapEnterPassphraseFragment @Inject constructor( // } } - ssss_view_show_password.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) - } - .disposeOnDestroyView() - - bootstrapSubmit.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - submit() - } - .disposeOnDestroyView() + ssss_view_show_password.debouncedClicks { sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) } + bootstrapSubmit.debouncedClicks { submit() } } private fun submit() = withState(sharedViewModel) { state -> diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapMigrateBackupFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapMigrateBackupFragment.kt index f1847e5ab5..caf43721a0 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapMigrateBackupFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapMigrateBackupFragment.kt @@ -28,7 +28,6 @@ import androidx.core.text.toSpannable import androidx.core.view.isVisible import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.view.clicks import com.jakewharton.rxbinding3.widget.editorActionEvents import com.jakewharton.rxbinding3.widget.textChanges import im.vector.matrix.android.api.extensions.tryThis @@ -80,37 +79,10 @@ class BootstrapMigrateBackupFragment @Inject constructor( .disposeOnDestroyView() // sharedViewModel.observeViewEvents {} - bootstrapMigrateContinueButton.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - submit() - } - .disposeOnDestroyView() - - bootstrapMigrateShowPassword.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) - } - .disposeOnDestroyView() - - bootstrapMigrateForgotPassphrase.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - sharedViewModel.handle(BootstrapActions.HandleForgotBackupPassphrase) - } - .disposeOnDestroyView() - - bootstrapMigrateUseFile.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - startImportTextFromFileIntent(this, IMPORT_FILE_REQ) - } - .disposeOnDestroyView() + bootstrapMigrateContinueButton.debouncedClicks { submit() } + bootstrapMigrateShowPassword.debouncedClicks { sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) } + bootstrapMigrateForgotPassphrase.debouncedClicks { sharedViewModel.handle(BootstrapActions.HandleForgotBackupPassphrase) } + bootstrapMigrateUseFile.debouncedClicks { startImportTextFromFileIntent(this, IMPORT_FILE_REQ) } } private fun submit() = withState(sharedViewModel) { state -> diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt index 05c6f7a53f..4faa4168b0 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt @@ -25,19 +25,16 @@ import androidx.core.text.toSpannable import androidx.core.view.isVisible import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.view.clicks import im.vector.riotx.R import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.utils.colorizeMatchingText import im.vector.riotx.core.utils.startSharePlainTextIntent import im.vector.riotx.core.utils.toast -import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.fragment_bootstrap_save_key.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -import java.util.concurrent.TimeUnit import javax.inject.Inject class BootstrapSaveRecoveryKeyFragment @Inject constructor( @@ -51,34 +48,17 @@ class BootstrapSaveRecoveryKeyFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - bootstrapSaveText.text = getString(R.string.bootstrap_save_key_description, getString(R.string.message_key), getString(R.string.recovery_passphrase)) + val messageKey = getString(R.string.message_key) + val recoveryPassphrase = getString(R.string.recovery_passphrase) + val color = colorProvider.getColorFromAttribute(R.attr.vctr_toolbar_link_text_color) + bootstrapSaveText.text = getString(R.string.bootstrap_save_key_description, messageKey, recoveryPassphrase) .toSpannable() - .colorizeMatchingText(getString(R.string.recovery_passphrase), colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) - .colorizeMatchingText(getString(R.string.message_key), colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) + .colorizeMatchingText(messageKey, color) + .colorizeMatchingText(recoveryPassphrase, color) - recoverySave.clickableView.clicks() - .debounce(600, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - downloadRecoveryKey() - } - .disposeOnDestroyView() - - recoveryCopy.clickableView.clicks() - .debounce(600, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - shareRecoveryKey() - } - .disposeOnDestroyView() - - recoveryContinue.clickableView.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - sharedViewModel.handle(BootstrapActions.GoToCompleted) - } - .disposeOnDestroyView() + recoverySave.clickableView.debouncedClicks { downloadRecoveryKey() } + recoveryCopy.clickableView.debouncedClicks { shareRecoveryKey() } + recoveryContinue.clickableView.debouncedClicks { sharedViewModel.handle(BootstrapActions.GoToCompleted) } } private fun downloadRecoveryKey() = withState(sharedViewModel) { _ -> diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt index 7a003c3722..dce33255ce 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt @@ -367,7 +367,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( ) if (trustResult.isVerified()) { // Sign this device and upload the signature - session.sessionParams.credentials.deviceId?.let { deviceId -> + session.sessionParams.deviceId?.let { deviceId -> session.cryptoService() .crossSigningService().trustDevice(deviceId, object : MatrixCallback { override fun onFailure(failure: Throwable) { diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsAction.kt b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsAction.kt new file mode 100644 index 0000000000..57b23d26d2 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsAction.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.discovery + +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.riotx.core.platform.VectorViewModelAction + +sealed class DiscoverySettingsAction : VectorViewModelAction { + object RetrieveBinding : DiscoverySettingsAction() + object Refresh : DiscoverySettingsAction() + + object DisconnectIdentityServer : DiscoverySettingsAction() + data class ChangeIdentityServer(val url: String) : DiscoverySettingsAction() + data class RevokeThreePid(val threePid: ThreePid) : DiscoverySettingsAction() + data class ShareThreePid(val threePid: ThreePid) : DiscoverySettingsAction() + data class FinalizeBind3pid(val threePid: ThreePid) : DiscoverySettingsAction() + data class SubmitMsisdnToken(val threePid: ThreePid.Msisdn, val code: String) : DiscoverySettingsAction() + data class CancelBinding(val threePid: ThreePid) : DiscoverySettingsAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsController.kt b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsController.kt new file mode 100644 index 0000000000..f92cb1a8bb --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsController.kt @@ -0,0 +1,377 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotx.features.discovery + +import android.view.View +import com.airbnb.epoxy.TypedEpoxyController +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Incomplete +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import com.google.i18n.phonenumbers.PhoneNumberUtil +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.session.identity.SharedState +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.attributes.ButtonStyle +import im.vector.riotx.core.epoxy.attributes.ButtonType +import im.vector.riotx.core.epoxy.attributes.IconMode +import im.vector.riotx.core.epoxy.loadingItem +import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.resources.StringProvider +import timber.log.Timber +import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection + +class DiscoverySettingsController @Inject constructor( + private val colorProvider: ColorProvider, + private val stringProvider: StringProvider, + private val errorFormatter: ErrorFormatter +) : TypedEpoxyController() { + + var listener: Listener? = null + + private val codes = mutableMapOf() + + override fun buildModels(data: DiscoverySettingsState) { + when (data.identityServer) { + is Loading -> { + loadingItem { + id("identityServerLoading") + } + } + is Fail -> { + settingsInfoItem { + id("identityServerError") + helperText(data.identityServer.error.message) + } + } + is Success -> { + buildIdentityServerSection(data) + val hasIdentityServer = data.identityServer().isNullOrBlank().not() + if (hasIdentityServer && !data.termsNotSigned) { + buildEmailsSection(data.emailList) + buildMsisdnSection(data.phoneNumbersList) + } + } + } + } + + private fun buildIdentityServerSection(data: DiscoverySettingsState) { + val identityServer = data.identityServer() ?: stringProvider.getString(R.string.none) + + settingsSectionTitleItem { + id("idServerTitle") + titleResId(R.string.identity_server) + } + + settingsItem { + id("idServer") + title(identityServer) + } + + if (data.identityServer() != null && data.termsNotSigned) { + settingsInfoItem { + id("idServerFooter") + helperText(stringProvider.getString(R.string.settings_agree_to_terms, identityServer)) + showCompoundDrawable(true) + itemClickListener(View.OnClickListener { listener?.openIdentityServerTerms() }) + } + settingsButtonItem { + id("seeTerms") + colorProvider(colorProvider) + buttonTitle(stringProvider.getString(R.string.open_terms_of, identityServer)) + buttonClickListener { listener?.openIdentityServerTerms() } + } + } else { + settingsInfoItem { + id("idServerFooter") + showCompoundDrawable(false) + if (data.identityServer() != null) { + helperText(stringProvider.getString(R.string.settings_discovery_identity_server_info, identityServer)) + } else { + helperTextResId(R.string.settings_discovery_identity_server_info_none) + } + } + } + + settingsButtonItem { + id("change") + colorProvider(colorProvider) + if (data.identityServer() == null) { + buttonTitleId(R.string.add_identity_server) + } else { + buttonTitleId(R.string.change_identity_server) + } + buttonClickListener { listener?.onTapChangeIdentityServer() } + } + + if (data.identityServer() != null) { + settingsInfoItem { + id("removeInfo") + helperTextResId(R.string.settings_discovery_disconnect_identity_server_info) + } + settingsButtonItem { + id("remove") + colorProvider(colorProvider) + buttonTitleId(R.string.disconnect_identity_server) + buttonStyle(ButtonStyle.DESTRUCTIVE) + buttonClickListener { listener?.onTapDisconnectIdentityServer() } + } + } + } + + private fun buildEmailsSection(emails: Async>) { + settingsSectionTitleItem { + id("emails") + titleResId(R.string.settings_discovery_emails_title) + } + when (emails) { + is Incomplete -> { + loadingItem { + id("emailsLoading") + } + } + is Fail -> { + settingsInfoItem { + id("emailsError") + helperText(emails.error.message) + } + } + is Success -> { + if (emails().isEmpty()) { + settingsInfoItem { + id("emailsEmpty") + helperText(stringProvider.getString(R.string.settings_discovery_no_mails)) + } + } else { + emails().forEach { buildEmail(it) } + } + } + } + } + + private fun buildEmail(pidInfo: PidInfo) { + buildThreePid(pidInfo) + + if (pidInfo.isShared is Fail) { + buildSharedFail(pidInfo) + } else if (pidInfo.isShared() == SharedState.BINDING_IN_PROGRESS) { + when (pidInfo.finalRequest) { + is Uninitialized, + is Loading -> + settingsInformationItem { + id("info${pidInfo.threePid.value}") + colorProvider(colorProvider) + message(stringProvider.getString(R.string.settings_discovery_confirm_mail, pidInfo.threePid.value)) + } + is Fail -> + settingsInformationItem { + id("info${pidInfo.threePid.value}") + colorProvider(colorProvider) + message(stringProvider.getString(R.string.settings_discovery_confirm_mail_not_clicked, pidInfo.threePid.value)) + textColorId(R.color.riotx_destructive_accent) + } + is Success -> Unit /* Cannot happen */ + } + when (pidInfo.finalRequest) { + is Uninitialized, + is Fail -> + buildContinueCancel(pidInfo.threePid) + is Loading -> + settingsProgressItem { + id("progress${pidInfo.threePid.value}") + } + is Success -> Unit /* Cannot happen */ + } + } + } + + private fun buildMsisdnSection(msisdns: Async>) { + settingsSectionTitleItem { + id("msisdn") + titleResId(R.string.settings_discovery_msisdn_title) + } + + when (msisdns) { + is Incomplete -> { + loadingItem { + id("msisdnLoading") + } + } + is Fail -> { + settingsInfoItem { + id("msisdnListError") + helperText(msisdns.error.message) + } + } + is Success -> { + if (msisdns().isEmpty()) { + settingsInfoItem { + id("no_msisdn") + helperText(stringProvider.getString(R.string.settings_discovery_no_msisdn)) + } + } else { + msisdns().forEach { buildMsisdn(it) } + } + } + } + } + + private fun buildMsisdn(pidInfo: PidInfo) { + val phoneNumber = try { + PhoneNumberUtil.getInstance().parse("+${pidInfo.threePid.value}", null) + } catch (t: Throwable) { + Timber.e(t, "Unable to parse the phone number") + null + } + ?.let { + PhoneNumberUtil.getInstance().format(it, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL) + } + ?: pidInfo.threePid.value + + buildThreePid(pidInfo, phoneNumber) + + if (pidInfo.isShared is Fail) { + buildSharedFail(pidInfo) + } else if (pidInfo.isShared() == SharedState.BINDING_IN_PROGRESS) { + val errorText = if (pidInfo.finalRequest is Fail) { + val error = pidInfo.finalRequest.error + // Deal with error 500 + // Ref: https://github.com/matrix-org/sydent/issues/292 + if (error is Failure.ServerError + && error.httpCode == HttpsURLConnection.HTTP_INTERNAL_ERROR /* 500 */) { + stringProvider.getString(R.string.settings_text_message_sent_wrong_code) + } else { + errorFormatter.toHumanReadable(error) + } + } else { + null + } + settingsEditTextItem { + id("msisdnVerification${pidInfo.threePid.value}") + descriptionText(stringProvider.getString(R.string.settings_text_message_sent, phoneNumber)) + errorText(errorText) + inProgress(pidInfo.finalRequest is Loading) + interactionListener(object : SettingsEditTextItem.Listener { + override fun onValidate() { + val code = codes[pidInfo.threePid] + if (pidInfo.threePid is ThreePid.Msisdn && code != null) { + listener?.sendMsisdnVerificationCode(pidInfo.threePid, code) + } + } + + override fun onCodeChange(code: String) { + codes[pidInfo.threePid] = code + } + }) + } + buildContinueCancel(pidInfo.threePid) + } + } + + private fun buildThreePid(pidInfo: PidInfo, title: String = pidInfo.threePid.value) { + settingsTextButtonSingleLineItem { + id(pidInfo.threePid.value) + title(title) + colorProvider(colorProvider) + stringProvider(stringProvider) + when (pidInfo.isShared) { + is Loading -> { + buttonIndeterminate(true) + } + is Fail -> { + buttonType(ButtonType.NORMAL) + buttonStyle(ButtonStyle.DESTRUCTIVE) + buttonTitle(stringProvider.getString(R.string.global_retry)) + iconMode(IconMode.ERROR) + buttonClickListener { listener?.onTapRetryToRetrieveBindings() } + } + is Success -> when (pidInfo.isShared()) { + SharedState.SHARED, + SharedState.NOT_SHARED -> { + buttonType(ButtonType.SWITCH) + checked(pidInfo.isShared() == SharedState.SHARED) + switchChangeListener { _, checked -> + if (checked) { + listener?.onTapShare(pidInfo.threePid) + } else { + listener?.onTapRevoke(pidInfo.threePid) + } + } + } + SharedState.BINDING_IN_PROGRESS -> { + buttonType(ButtonType.NO_BUTTON) + when (pidInfo.finalRequest) { + is Incomplete -> iconMode(IconMode.INFO) + is Fail -> iconMode(IconMode.ERROR) + else -> iconMode(IconMode.NONE) + } + } + } + } + } + } + + private fun buildSharedFail(pidInfo: PidInfo) { + settingsInformationItem { + id("info${pidInfo.threePid.value}") + colorProvider(colorProvider) + textColorId(R.color.vector_error_color) + message((pidInfo.isShared as? Fail)?.error?.message ?: "") + } + } + + private fun buildContinueCancel(threePid: ThreePid) { + settingsContinueCancelItem { + id("bottom${threePid.value}") + interactionListener(object : SettingsContinueCancelItem.Listener { + override fun onContinue() { + when (threePid) { + is ThreePid.Email -> { + listener?.checkEmailVerification(threePid) + } + is ThreePid.Msisdn -> { + val code = codes[threePid] + if (code != null) { + listener?.sendMsisdnVerificationCode(threePid, code) + } + } + } + } + + override fun onCancel() { + listener?.cancelBinding(threePid) + } + }) + } + } + + interface Listener { + fun openIdentityServerTerms() + fun onTapRevoke(threePid: ThreePid) + fun onTapShare(threePid: ThreePid) + fun checkEmailVerification(threePid: ThreePid.Email) + fun cancelBinding(threePid: ThreePid) + fun sendMsisdnVerificationCode(threePid: ThreePid.Msisdn, code: String) + fun onTapChangeIdentityServer() + fun onTapDisconnectIdentityServer() + fun onTapRetryToRetrieveBindings() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsFragment.kt b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsFragment.kt new file mode 100644 index 0000000000..b772db7322 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsFragment.kt @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotx.features.discovery + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AlertDialog +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.matrix.android.api.session.identity.SharedState +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.matrix.android.api.session.terms.TermsService +import im.vector.riotx.R +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.configureWith +import im.vector.riotx.core.extensions.exhaustive +import im.vector.riotx.core.extensions.observeEvent +import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.utils.ensureProtocol +import im.vector.riotx.features.discovery.change.SetIdentityServerFragment +import im.vector.riotx.features.terms.ReviewTermsActivity +import kotlinx.android.synthetic.main.fragment_generic_recycler.* +import javax.inject.Inject + +class DiscoverySettingsFragment @Inject constructor( + private val controller: DiscoverySettingsController, + val viewModelFactory: DiscoverySettingsViewModel.Factory +) : VectorBaseFragment(), DiscoverySettingsController.Listener { + + override fun getLayoutResId() = R.layout.fragment_generic_recycler + + private val viewModel by fragmentViewModel(DiscoverySettingsViewModel::class) + + lateinit var sharedViewModel: DiscoverySharedViewModel + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + sharedViewModel = activityViewModelProvider.get(DiscoverySharedViewModel::class.java) + + controller.listener = this + recyclerView.configureWith(controller) + + sharedViewModel.navigateEvent.observeEvent(this) { + when (it) { + is DiscoverySharedViewModelAction.ChangeIdentityServer -> + viewModel.handle(DiscoverySettingsAction.ChangeIdentityServer(it.newUrl)) + }.exhaustive + } + + viewModel.observeViewEvents { + when (it) { + is DiscoverySettingsViewEvents.Failure -> { + displayErrorDialog(it.throwable) + } + }.exhaustive + } + } + + override fun onDestroyView() { + recyclerView.cleanup() + controller.listener = null + super.onDestroyView() + } + + override fun invalidate() = withState(viewModel) { state -> + controller.setData(state) + } + + override fun onResume() { + super.onResume() + (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_discovery_category) + + // If some 3pids are pending, we can try to check if they have been verified here + viewModel.handle(DiscoverySettingsAction.Refresh) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == ReviewTermsActivity.TERMS_REQUEST_CODE) { + if (Activity.RESULT_OK == resultCode) { + viewModel.handle(DiscoverySettingsAction.RetrieveBinding) + } else { + // add some error? + } + } + + super.onActivityResult(requestCode, resultCode, data) + } + + override fun openIdentityServerTerms() = withState(viewModel) { state -> + if (state.termsNotSigned) { + navigator.openTerms( + this, + TermsService.ServiceType.IdentityService, + state.identityServer()?.ensureProtocol() ?: "", + null) + } + } + + override fun onTapRevoke(threePid: ThreePid) { + viewModel.handle(DiscoverySettingsAction.RevokeThreePid(threePid)) + } + + override fun onTapShare(threePid: ThreePid) { + viewModel.handle(DiscoverySettingsAction.ShareThreePid(threePid)) + } + + override fun checkEmailVerification(threePid: ThreePid.Email) { + viewModel.handle(DiscoverySettingsAction.FinalizeBind3pid(threePid)) + } + + override fun sendMsisdnVerificationCode(threePid: ThreePid.Msisdn, code: String) { + viewModel.handle(DiscoverySettingsAction.SubmitMsisdnToken(threePid, code)) + } + + override fun cancelBinding(threePid: ThreePid) { + viewModel.handle(DiscoverySettingsAction.CancelBinding(threePid)) + } + + override fun onTapChangeIdentityServer() = withState(viewModel) { state -> + // we should prompt if there are bound items with current is + val pidList = state.emailList().orEmpty() + state.phoneNumbersList().orEmpty() + val hasBoundIds = pidList.any { it.isShared() == SharedState.SHARED } + + if (hasBoundIds) { + // we should prompt + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.change_identity_server) + .setMessage(getString(R.string.settings_discovery_disconnect_with_bound_pid, state.identityServer(), state.identityServer())) + .setPositiveButton(R.string._continue) { _, _ -> navigateToChangeIdentityServerFragment() } + .setNegativeButton(R.string.cancel, null) + .show() + Unit + } else { + navigateToChangeIdentityServerFragment() + } + } + + override fun onTapDisconnectIdentityServer() { + // we should prompt if there are bound items with current is + withState(viewModel) { state -> + val pidList = state.emailList().orEmpty() + state.phoneNumbersList().orEmpty() + val hasBoundIds = pidList.any { it.isShared() == SharedState.SHARED } + + val message = if (hasBoundIds) { + getString(R.string.settings_discovery_disconnect_with_bound_pid, state.identityServer(), state.identityServer()) + } else { + getString(R.string.disconnect_identity_server_dialog_content, state.identityServer()) + } + + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.disconnect_identity_server) + .setMessage(message) + .setPositiveButton(R.string.disconnect) { _, _ -> viewModel.handle(DiscoverySettingsAction.DisconnectIdentityServer) } + .setNegativeButton(R.string.cancel, null) + .show() + } + } + + override fun onTapRetryToRetrieveBindings() { + viewModel.handle(DiscoverySettingsAction.RetrieveBinding) + } + + private fun navigateToChangeIdentityServerFragment() { + parentFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.anim_slide_in_bottom, R.anim.anim_slide_out_bottom, R.anim.anim_slide_in_bottom, R.anim.anim_slide_out_bottom) + .replace(R.id.vector_settings_page, SetIdentityServerFragment::class.java, null) + .addToBackStack(null) + .commit() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsState.kt b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsState.kt new file mode 100644 index 0000000000..5dc4b2354a --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsState.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.discovery + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized + +data class DiscoverySettingsState( + val identityServer: Async = Uninitialized, + val emailList: Async> = Uninitialized, + val phoneNumbersList: Async> = Uninitialized, + // Can be true if terms are updated + val termsNotSigned: Boolean = false +) : MvRxState diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsViewEvents.kt new file mode 100644 index 0000000000..6fd45394a2 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsViewEvents.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.discovery + +import im.vector.riotx.core.platform.VectorViewEvents + +sealed class DiscoverySettingsViewEvents : VectorViewEvents { + data class Failure(val throwable: Throwable) : DiscoverySettingsViewEvents() +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsViewModel.kt new file mode 100644 index 0000000000..9693b6a4ad --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsViewModel.kt @@ -0,0 +1,373 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotx.features.discovery + +import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.identity.IdentityServiceError +import im.vector.matrix.android.api.session.identity.IdentityServiceListener +import im.vector.matrix.android.api.session.identity.SharedState +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.matrix.android.internal.util.awaitCallback +import im.vector.matrix.rx.rx +import im.vector.riotx.core.extensions.exhaustive +import im.vector.riotx.core.platform.VectorViewModel +import kotlinx.coroutines.launch + +class DiscoverySettingsViewModel @AssistedInject constructor( + @Assisted initialState: DiscoverySettingsState, + private val session: Session) + : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: DiscoverySettingsState): DiscoverySettingsViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: DiscoverySettingsState): DiscoverySettingsViewModel? { + val fragment: DiscoverySettingsFragment = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.viewModelFactory.create(state) + } + } + + private val identityService = session.identityService() + + private val identityServerManagerListener = object : IdentityServiceListener { + override fun onIdentityServerChange() = withState { state -> + val identityServerUrl = identityService.getCurrentIdentityServerUrl() + val currentIS = state.identityServer() + setState { + copy(identityServer = Success(identityServerUrl)) + } + if (currentIS != identityServerUrl) retrieveBinding() + } + } + + init { + setState { + copy(identityServer = Success(identityService.getCurrentIdentityServerUrl())) + } + startListenToIdentityManager() + observeThreePids() + } + + private fun observeThreePids() { + session.rx() + .liveThreePIds(true) + .subscribe { + retrieveBinding(it) + } + .disposeOnClear() + } + + override fun onCleared() { + super.onCleared() + stopListenToIdentityManager() + } + + override fun handle(action: DiscoverySettingsAction) { + when (action) { + DiscoverySettingsAction.Refresh -> refreshPendingEmailBindings() + DiscoverySettingsAction.RetrieveBinding -> retrieveBinding() + DiscoverySettingsAction.DisconnectIdentityServer -> disconnectIdentityServer() + is DiscoverySettingsAction.ChangeIdentityServer -> changeIdentityServer(action) + is DiscoverySettingsAction.RevokeThreePid -> revokeThreePid(action) + is DiscoverySettingsAction.ShareThreePid -> shareThreePid(action) + is DiscoverySettingsAction.FinalizeBind3pid -> finalizeBind3pid(action, true) + is DiscoverySettingsAction.SubmitMsisdnToken -> submitMsisdnToken(action) + is DiscoverySettingsAction.CancelBinding -> cancelBinding(action) + }.exhaustive + } + + private fun disconnectIdentityServer() { + setState { copy(identityServer = Loading()) } + + viewModelScope.launch { + try { + awaitCallback { session.identityService().disconnect(it) } + setState { copy(identityServer = Success(null)) } + } catch (failure: Throwable) { + setState { copy(identityServer = Fail(failure)) } + } + } + } + + private fun changeIdentityServer(action: DiscoverySettingsAction.ChangeIdentityServer) { + setState { copy(identityServer = Loading()) } + + viewModelScope.launch { + try { + val data = awaitCallback { + session.identityService().setNewIdentityServer(action.url, it) + } + setState { copy(identityServer = Success(data)) } + retrieveBinding() + } catch (failure: Throwable) { + setState { copy(identityServer = Fail(failure)) } + } + } + } + + private fun shareThreePid(action: DiscoverySettingsAction.ShareThreePid) = withState { state -> + if (state.identityServer() == null) return@withState + changeThreePidState(action.threePid, Loading()) + + viewModelScope.launch { + try { + awaitCallback { identityService.startBindThreePid(action.threePid, it) } + changeThreePidState(action.threePid, Success(SharedState.BINDING_IN_PROGRESS)) + } catch (failure: Throwable) { + _viewEvents.post(DiscoverySettingsViewEvents.Failure(failure)) + changeThreePidState(action.threePid, Fail(failure)) + } + } + } + + private fun changeThreePidState(threePid: ThreePid, state: Async) { + setState { + val currentMails = emailList().orEmpty() + val phones = phoneNumbersList().orEmpty() + copy( + emailList = Success( + currentMails.map { + if (it.threePid == threePid) { + it.copy(isShared = state) + } else { + it + } + } + ), + phoneNumbersList = Success( + phones.map { + if (it.threePid == threePid) { + it.copy(isShared = state) + } else { + it + } + } + ) + ) + } + } + + private fun changeThreePidSubmitState(threePid: ThreePid, submitState: Async) { + setState { + val currentMails = emailList().orEmpty() + val phones = phoneNumbersList().orEmpty() + copy( + emailList = Success( + currentMails.map { + if (it.threePid == threePid) { + it.copy(finalRequest = submitState) + } else { + it + } + } + ), + phoneNumbersList = Success( + phones.map { + if (it.threePid == threePid) { + it.copy(finalRequest = submitState) + } else { + it + } + } + ) + ) + } + } + + private fun revokeThreePid(action: DiscoverySettingsAction.RevokeThreePid) { + when (action.threePid) { + is ThreePid.Email -> revokeEmail(action.threePid) + is ThreePid.Msisdn -> revokeMsisdn(action.threePid) + }.exhaustive + } + + private fun revokeEmail(threePid: ThreePid.Email) = withState { state -> + if (state.identityServer() == null) return@withState + if (state.emailList() == null) return@withState + changeThreePidState(threePid, Loading()) + + viewModelScope.launch { + try { + awaitCallback { identityService.unbindThreePid(threePid, it) } + changeThreePidState(threePid, Success(SharedState.NOT_SHARED)) + } catch (failure: Throwable) { + _viewEvents.post(DiscoverySettingsViewEvents.Failure(failure)) + changeThreePidState(threePid, Fail(failure)) + } + } + } + + private fun revokeMsisdn(threePid: ThreePid.Msisdn) = withState { state -> + if (state.identityServer() == null) return@withState + if (state.phoneNumbersList() == null) return@withState + changeThreePidState(threePid, Loading()) + + viewModelScope.launch { + try { + awaitCallback { identityService.unbindThreePid(threePid, it) } + changeThreePidState(threePid, Success(SharedState.NOT_SHARED)) + } catch (failure: Throwable) { + _viewEvents.post(DiscoverySettingsViewEvents.Failure(failure)) + changeThreePidState(threePid, Fail(failure)) + } + } + } + + private fun cancelBinding(action: DiscoverySettingsAction.CancelBinding) { + viewModelScope.launch { + try { + awaitCallback { identityService.cancelBindThreePid(action.threePid, it) } + changeThreePidState(action.threePid, Success(SharedState.NOT_SHARED)) + changeThreePidSubmitState(action.threePid, Uninitialized) + } catch (failure: Throwable) { + // This could never fail + } + } + } + + private fun startListenToIdentityManager() { + identityService.addListener(identityServerManagerListener) + } + + private fun stopListenToIdentityManager() { + identityService.addListener(identityServerManagerListener) + } + + private fun retrieveBinding() { + retrieveBinding(session.getThreePids()) + } + + private fun retrieveBinding(threePids: List) = withState { state -> + if (state.identityServer().isNullOrBlank()) return@withState + + val emails = threePids.filterIsInstance() + val msisdns = threePids.filterIsInstance() + + setState { + copy( + emailList = Success(emails.map { PidInfo(it, Loading()) }), + phoneNumbersList = Success(msisdns.map { PidInfo(it, Loading()) }) + ) + } + + viewModelScope.launch { + try { + val data = awaitCallback> { + identityService.getShareStatus(threePids, it) + } + setState { + copy( + emailList = Success(data.filter { it.key is ThreePid.Email }.toPidInfoList()), + phoneNumbersList = Success(data.filter { it.key is ThreePid.Msisdn }.toPidInfoList()), + termsNotSigned = false + ) + } + } catch (failure: Throwable) { + if (failure !is IdentityServiceError.TermsNotSignedException) { + _viewEvents.post(DiscoverySettingsViewEvents.Failure(failure)) + } + + setState { + copy( + emailList = Success(emails.map { PidInfo(it, Fail(failure)) }), + phoneNumbersList = Success(msisdns.map { PidInfo(it, Fail(failure)) }), + termsNotSigned = failure is IdentityServiceError.TermsNotSignedException + ) + } + } + } + } + + private fun Map.toPidInfoList(): List { + return map { threePidStatus -> + PidInfo( + threePid = threePidStatus.key, + isShared = Success(threePidStatus.value) + ) + } + } + + private fun submitMsisdnToken(action: DiscoverySettingsAction.SubmitMsisdnToken) = withState { state -> + if (state.identityServer().isNullOrBlank()) return@withState + + changeThreePidSubmitState(action.threePid, Loading()) + + viewModelScope.launch { + try { + awaitCallback { + identityService.submitValidationToken(action.threePid, action.code, it) + } + changeThreePidSubmitState(action.threePid, Uninitialized) + finalizeBind3pid(DiscoverySettingsAction.FinalizeBind3pid(action.threePid), true) + } catch (failure: Throwable) { + changeThreePidSubmitState(action.threePid, Fail(failure)) + } + } + } + + private fun finalizeBind3pid(action: DiscoverySettingsAction.FinalizeBind3pid, fromUser: Boolean) = withState { state -> + val threePid = when (action.threePid) { + is ThreePid.Email -> { + state.emailList()?.find { it.threePid.value == action.threePid.email }?.threePid ?: return@withState + } + is ThreePid.Msisdn -> { + state.phoneNumbersList()?.find { it.threePid.value == action.threePid.msisdn }?.threePid ?: return@withState + } + } + + changeThreePidSubmitState(action.threePid, Loading()) + + viewModelScope.launch { + try { + awaitCallback { identityService.finalizeBindThreePid(threePid, it) } + changeThreePidSubmitState(action.threePid, Uninitialized) + changeThreePidState(action.threePid, Success(SharedState.SHARED)) + } catch (failure: Throwable) { + // If this is not from user (user did not click to "Continue", but this is a refresh when Fragment is resumed), do no display the error + if (fromUser) { + changeThreePidSubmitState(action.threePid, Fail(failure)) + } else { + changeThreePidSubmitState(action.threePid, Uninitialized) + } + } + } + } + + private fun refreshPendingEmailBindings() = withState { state -> + state.emailList()?.forEach { info -> + when (info.isShared()) { + SharedState.BINDING_IN_PROGRESS -> finalizeBind3pid(DiscoverySettingsAction.FinalizeBind3pid(info.threePid), false) + else -> Unit + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySharedViewModel.kt b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySharedViewModel.kt new file mode 100644 index 0000000000..cde326d824 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySharedViewModel.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotx.features.discovery + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import im.vector.riotx.core.extensions.postLiveEvent +import im.vector.riotx.core.utils.LiveEvent +import javax.inject.Inject + +class DiscoverySharedViewModel @Inject constructor() : ViewModel() { + var navigateEvent = MutableLiveData>() + + fun requestChangeToIdentityServer(serverUrl: String) { + navigateEvent.postLiveEvent(DiscoverySharedViewModelAction.ChangeIdentityServer(serverUrl)) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySharedViewModelAction.kt b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySharedViewModelAction.kt new file mode 100644 index 0000000000..5889ce4a63 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySharedViewModelAction.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.discovery + +sealed class DiscoverySharedViewModelAction { + data class ChangeIdentityServer(val newUrl: String) : DiscoverySharedViewModelAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/PidInfo.kt b/vector/src/main/java/im/vector/riotx/features/discovery/PidInfo.kt new file mode 100644 index 0000000000..67739c48ce --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/PidInfo.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.discovery + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.session.identity.SharedState +import im.vector.matrix.android.api.session.identity.ThreePid + +data class PidInfo( + // Retrieved from the homeserver + val threePid: ThreePid, + // Retrieved from IdentityServer, or transient state + val isShared: Async, + // Contains information about a current request to submit the token (for instance SMS code received by SMS) + // Or a current binding finalization, for email + val finalRequest: Async = Uninitialized +) diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/SettingsButtonItem.kt b/vector/src/main/java/im/vector/riotx/features/discovery/SettingsButtonItem.kt new file mode 100644 index 0000000000..11a2737496 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/SettingsButtonItem.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotx.features.discovery + +import android.widget.Button +import androidx.annotation.StringRes +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.ClickListener +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.attributes.ButtonStyle +import im.vector.riotx.core.epoxy.onClick +import im.vector.riotx.core.extensions.setTextOrHide +import im.vector.riotx.core.resources.ColorProvider + +@EpoxyModelClass(layout = R.layout.item_settings_button) +abstract class SettingsButtonItem : EpoxyModelWithHolder() { + + @EpoxyAttribute + lateinit var colorProvider: ColorProvider + + @EpoxyAttribute + var buttonTitle: String? = null + + @EpoxyAttribute + @StringRes + var buttonTitleId: Int? = null + + @EpoxyAttribute + var buttonStyle: ButtonStyle = ButtonStyle.POSITIVE + + @EpoxyAttribute + var buttonClickListener: ClickListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + if (buttonTitleId != null) { + holder.button.setText(buttonTitleId!!) + } else { + holder.button.setTextOrHide(buttonTitle) + } + + when (buttonStyle) { + ButtonStyle.POSITIVE -> { + holder.button.setTextColor(colorProvider.getColor(R.color.riotx_accent)) + } + ButtonStyle.DESTRUCTIVE -> { + holder.button.setTextColor(colorProvider.getColor(R.color.riotx_destructive_accent)) + } + } + + holder.button.onClick(buttonClickListener) + } + + class Holder : VectorEpoxyHolder() { + val button by bind