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 bc04df2e39..b63432c489 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,14 +1,17 @@ -Changes in RiotX 0.20.0 (2020-XX-XX) +Changes in RiotX 0.21.0 (2020-XX-XX) =================================================== Features ✨: - - + - Identity server support (#607) + - Switch language support (#41) Improvements 🙌: - - + - Better connectivity lost indicator when airplane mode is on + - Add a setting to hide redacted events (#951) Bugfix 🐛: - After jump to unread, newer messages are never loaded (#1008) + - Fix issues with FontScale switch (#69, #645) Translations 🗣: - @@ -22,6 +25,26 @@ Build 🧱: Other changes: - +Changes in RiotX 0.20.0 (2020-05-15) +=================================================== + +Features ✨: + - Add Direct Shortcuts (#652) + +Improvements 🙌: + - Invite member(s) to an existing room (#1276) + - Improve notification accessibility with ticker text (#1226) + - Support homeserver discovery from MXID (DISABLED: waiting for design) (#476) + +Bugfix 🐛: + - Fix | Verify Manually by Text crashes if private SSK not known (#1337) + - Sometimes the same device appears twice in the list of devices of a user (#1329) + - Random Crashes while doing sth with cross signing keys (#1364) + - Crash | crash while restoring key backup (#1366) + +SDK API changes ⚠️: + - excludedUserIds parameter added to the UserService.getPagedUsersLive() function + Changes in RiotX 0.19.0 (2020-05-04) =================================================== 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/gradle.properties b/gradle.properties index 2e2b110f15..d9d9e57cbc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ # The setting is particularly useful for tweaking memory settings. android.enableJetifier=true android.useAndroidX=true -org.gradle.jvmargs=-Xmx1536m +org.gradle.jvmargs=-Xmx8192m # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt index 193b5c3fbf..469bc514e0 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt @@ -28,6 +28,7 @@ import im.vector.matrix.android.api.session.room.send.UserDraft import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.toOptional +import io.reactivex.Completable import io.reactivex.Observable import io.reactivex.Single @@ -95,6 +96,10 @@ class RxRoom(private val room: Room) { fun liveNotificationState(): Observable { return room.getLiveRoomNotificationState().asObservable() } + + fun invite(userId: String, reason: String? = null): Completable = completableBuilder { + room.invite(userId, reason, it) + } } fun Room.rx(): RxRoom { 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 c2c8978500..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 @@ -90,8 +91,13 @@ class RxSession(private val session: Session) { return session.getIgnoredUsersLive().asObservable() } - fun livePagedUsers(filter: String? = null): Observable> { - return session.getPagedUsersLive(filter).asObservable() + fun livePagedUsers(filter: String? = null, excludedUserIds: Set? = null): Observable> { + 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 { 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 9b3ebad03b..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 @@ -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) { 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 e4aa7872aa..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 @@ -248,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/KeysBackupScenarioData.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupScenarioData.kt index f10f2fef0e..0270c34a37 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupScenarioData.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupScenarioData.kt @@ -19,13 +19,13 @@ package im.vector.matrix.android.internal.crypto.keysbackup import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.common.CommonTestHelper import im.vector.matrix.android.common.CryptoTestData -import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper +import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper2 /** * Data class to store result of [KeysBackupTestHelper.createKeysBackupScenarioWithPassword] */ data class KeysBackupScenarioData(val cryptoTestData: CryptoTestData, - val aliceKeys: List, + val aliceKeys: List, val prepareKeysBackupDataResult: PrepareKeysBackupDataResult, val aliceSession2: Session) { fun cleanUp(testHelper: CommonTestHelper) { 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..9bdd8f1131 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 @@ -579,7 +579,7 @@ class SASTest : InstrumentedTest { requestID!!, cryptoTestData.roomId, bobSession.myUserId, - bobSession.sessionParams.credentials.deviceId!!, + bobSession.sessionParams.deviceId!!, null) bobVerificationService.beginKeyVerificationInDMs( @@ -587,7 +587,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/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 140d1c259f..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,9 +20,9 @@ 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 import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable @@ -30,13 +30,17 @@ import im.vector.matrix.android.api.util.Cancelable * This interface defines methods to authenticate or to create an account to a matrix server. */ interface AuthenticationService { - /** * Request the supported login flows for this homeserver. * This is the first method to call to be able to get a wizard to login or the create an account */ 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,19 +78,26 @@ 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 */ fun createSessionFromSso(homeServerConnectionConfig: HomeServerConnectionConfig, credentials: Credentials, callback: MatrixCallback): Cancelable + + /** + * Perform a wellknown request, using the domain from the matrixId + */ + fun getWellKnownData(matrixId: String, + callback: MatrixCallback): Cancelable + + /** + * Authenticate with a matrixId and a password + * Usually call this after a successful call to getWellKnownData() + */ + fun directAuthentication(homeServerConnectionConfig: HomeServerConnectionConfig, + matrixId: String, + password: String, + initialDeviceName: String, + callback: MatrixCallback): Cancelable } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt index 72affe24bb..d88cd5e74d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt @@ -24,16 +24,38 @@ import im.vector.matrix.android.internal.util.md5 * This data class hold credentials user data. * You shouldn't have to instantiate it. * The access token should be use to authenticate user in all server requests. + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-login */ @JsonClass(generateAdapter = true) data class Credentials( + /** + * The fully-qualified Matrix ID that has been registered. + */ @Json(name = "user_id") val userId: String, - @Json(name = "home_server") val homeServer: String, + /** + * An access token for the account. This access token can then be used to authorize other requests. + */ @Json(name = "access_token") val accessToken: String, + /** + * Not documented + */ @Json(name = "refresh_token") val refreshToken: String?, + /** + * The server_name of the homeserver on which the account has been registered. + * @Deprecated. Clients should extract the server_name from user_id (by splitting at the first colon) + * if they require it. Note also that homeserver is not spelt this way. + */ + @Json(name = "home_server") val homeServer: String, + /** + * ID of the logged-in device. Will be the same as the corresponding parameter in the request, if one was specified. + */ @Json(name = "device_id") val deviceId: String?, - // Optional data that may contain info to override home server and/or identity server - @Json(name = "well_known") val wellKnown: WellKnown? = null + /** + * Optional client configuration provided by the server. If present, clients SHOULD use the provided object to + * reconfigure themselves, optionally validating the URLs within. + * This object takes the same form as the one returned from .well-known autodiscovery. + */ + @Json(name = "well_known") val discoveryInformation: DiscoveryInformation? = null ) internal fun Credentials.sessionId(): String { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/DiscoveryInformation.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/DiscoveryInformation.kt new file mode 100644 index 0000000000..2aa741bad3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/DiscoveryInformation.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.auth.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This is a light version of Wellknown model, used for login response + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-login + */ +@JsonClass(generateAdapter = true) +data class DiscoveryInformation( + /** + * Required. Used by clients to discover homeserver information. + */ + @Json(name = "m.homeserver") + val homeServer: WellKnownBaseConfig? = null, + + /** + * Used by clients to discover identity server information. + * Note: matrix.org does not send this field + */ + @Json(name = "m.identity_server") + val identityServer: WellKnownBaseConfig? = null +) 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/auth/data/WellKnown.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnown.kt index bdad4702b7..9dd1fa2012 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnown.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnown.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.api.auth.data import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.util.JsonDict /** * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery @@ -52,7 +53,7 @@ data class WellKnown( val identityServer: WellKnownBaseConfig? = null, @Json(name = "m.integrations") - val integrations: Map? = null + val integrations: JsonDict? = null ) { /** * Returns the list of integration managers proposed diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnownManagerConfig.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnownManagerConfig.kt index 33ed412a2a..ffdea37afe 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnownManagerConfig.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnownManagerConfig.kt @@ -16,6 +16,6 @@ package im.vector.matrix.android.api.auth.data data class WellKnownManagerConfig( - val apiUrl : String, + val apiUrl: String, val uiUrl: String ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/wellknown/WellknownResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/wellknown/WellknownResult.kt new file mode 100644 index 0000000000..58c7cf730e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/wellknown/WellknownResult.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.api.auth.wellknown + +import im.vector.matrix.android.api.auth.data.WellKnown + +/** + * Ref: https://matrix.org/docs/spec/client_server/latest#well-known-uri + */ +sealed class WellknownResult { + /** + * The provided matrixId is no valid. Unable to extract a domain name. + */ + object InvalidMatrixId : WellknownResult() + + /** + * Retrieve the specific piece of information from the user in a way which fits within the existing client user experience, + * if the client is inclined to do so. Failure can take place instead if no good user experience for this is possible at this point. + */ + data class Prompt(val homeServerUrl: String, + val identityServerUrl: String?, + val wellKnown: WellKnown) : WellknownResult() + + /** + * Stop the current auto-discovery mechanism. If no more auto-discovery mechanisms are available, + * then the client may use other methods of determining the required parameters, such as prompting the user, or using default values. + */ + object Ignore : WellknownResult() + + /** + * Inform the user that auto-discovery failed due to invalid/empty data and PROMPT for the parameter. + */ + object FailPrompt : WellknownResult() + + /** + * Inform the user that auto-discovery did not return any usable URLs. Do not continue further with the current login process. + * At this point, valid data was obtained, but no homeserver is available to serve the client. + * No further guess should be attempted and the user should make a conscientious decision what to do next. + */ + object FailError : WellknownResult() +} 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/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/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/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/session/user/UserService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt index 453400bc99..1abda8ec05 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt @@ -61,9 +61,10 @@ interface UserService { /** * Observe a live [PagedList] of users sorted alphabetically. You can filter the users. * @param filter the filter. It will look into userId and displayName. + * @param excludedUserIds userId list which will be excluded from the result list. * @return a Livedata of users */ - fun getPagedUsersLive(filter: String? = null): LiveData> + fun getPagedUsersLive(filter: String? = null, excludedUserIds: Set? = null): LiveData> /** * Get list of ignored users diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/model/User.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/model/User.kt index 753c9b609c..9f4f997b3b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/model/User.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/model/User.kt @@ -22,6 +22,9 @@ package im.vector.matrix.android.api.session.user.model */ data class User( val userId: String, + /** + * For usage in UI, consider using [getBestName] + */ val displayName: String? = null, val avatarUrl: String? = null ) { 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 6b6321de36..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,12 +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.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 @@ -59,14 +62,17 @@ internal abstract class AuthModule { } @Binds - abstract fun bindSessionParamsStore(sessionParamsStore: RealmSessionParamsStore): SessionParamsStore + abstract fun bindSessionParamsStore(store: RealmSessionParamsStore): SessionParamsStore @Binds - abstract fun bindPendingSessionStore(pendingSessionStore: RealmPendingSessionStore): PendingSessionStore + abstract fun bindPendingSessionStore(store: RealmPendingSessionStore): PendingSessionStore @Binds - abstract fun bindAuthenticationService(authenticationService: DefaultAuthenticationService): AuthenticationService + abstract fun bindAuthenticationService(service: DefaultAuthenticationService): AuthenticationService @Binds - abstract fun bindSessionCreator(sessionCreator: DefaultSessionCreator): SessionCreator + abstract fun bindSessionCreator(creator: DefaultSessionCreator): SessionCreator + + @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 85c2cdbf3d..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,28 +23,33 @@ 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 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 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.di.Unauthenticated import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.network.executeRequest +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 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 @@ -59,7 +64,10 @@ internal class DefaultAuthenticationService @Inject constructor( private val sessionParamsStore: SessionParamsStore, private val sessionManager: SessionManager, private val sessionCreator: SessionCreator, - private val pendingSessionStore: PendingSessionStore + private val pendingSessionStore: PendingSessionStore, + private val getWellknownTask: GetWellknownTask, + private val directLoginTask: DirectLoginTask, + private val taskExecutor: TaskExecutor ) : AuthenticationService { private var pendingSessionData: PendingSessionData? = pendingSessionStore.getPendingSessionData() @@ -78,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 { @@ -148,27 +163,71 @@ internal class DefaultAuthenticationService @Inject constructor( val authAPI = buildAuthAPI(homeServerConnectionConfig) // Ok, try to get the config.json file of a RiotWeb client - val riotConfig = executeRequest(null) { - apiCall = authAPI.getRiotConfig() - } - - if (riotConfig.defaultHomeServerUrl?.isNotBlank() == true) { - // Ok, good sign, we got a default hs url - val newHomeServerConnectionConfig = homeServerConnectionConfig.copy( - homeServerUri = Uri.parse(riotConfig.defaultHomeServerUrl) - ) - - val newAuthAPI = buildAuthAPI(newHomeServerConnectionConfig) - - val versions = executeRequest(null) { - apiCall = newAuthAPI.versions() + return runCatching { + executeRequest(null) { + apiCall = authAPI.getRiotConfig() } - - return getLoginFlowResult(newAuthAPI, versions, riotConfig.defaultHomeServerUrl) - } else { - // Config exists, but there is no default homeserver url (ex: https://riot.im/app) - throw Failure.OtherServerError("", HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) } + .map { riotConfig -> + if (riotConfig.defaultHomeServerUrl?.isNotBlank() == true) { + // Ok, good sign, we got a default hs url + val newHomeServerConnectionConfig = homeServerConnectionConfig.copy( + homeServerUri = Uri.parse(riotConfig.defaultHomeServerUrl) + ) + + val newAuthAPI = buildAuthAPI(newHomeServerConnectionConfig) + + val versions = executeRequest(null) { + apiCall = newAuthAPI.versions() + } + + getLoginFlowResult(newAuthAPI, versions, riotConfig.defaultHomeServerUrl) + } else { + // Config exists, but there is no default homeserver url (ex: https://riot.im/app) + throw Failure.OtherServerError("", HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) + } + } + .fold( + { + it + }, + { + if (it is Failure.OtherServerError + && it.httpCode == HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) { + // Try with wellknown + getWellknownLoginFlowInternal(homeServerConnectionConfig) + } else { + throw it + } + } + ) + } + + private suspend fun getWellknownLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig): LoginFlowResult { + val domain = homeServerConnectionConfig.homeServerUri.host + ?: throw Failure.OtherServerError("", HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) + + // Create a fake userId, for the getWellknown task + val fakeUserId = "@alice:$domain" + val wellknownResult = getWellknownTask.execute(GetWellknownTask.Params(fakeUserId)) + + return when (wellknownResult) { + is WellknownResult.Prompt -> { + val newHomeServerConnectionConfig = homeServerConnectionConfig.copy( + homeServerUri = Uri.parse(wellknownResult.homeServerUrl), + identityServerUri = wellknownResult.identityServerUrl?.let { Uri.parse(it) } + ) + + val newAuthAPI = buildAuthAPI(newHomeServerConnectionConfig) + + val versions = executeRequest(null) { + apiCall = newAuthAPI.versions() + } + + getLoginFlowResult(newAuthAPI, versions, wellknownResult.homeServerUrl) + } + else -> throw Failure.OtherServerError("", HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) + }.exhaustive } private suspend fun getLoginFlowResult(authAPI: AuthAPI, versions: Versions, homeServerUrl: String): LoginFlowResult { @@ -193,7 +252,8 @@ internal class DefaultAuthenticationService @Inject constructor( retrofitFactory, coroutineDispatchers, sessionCreator, - pendingSessionStore + pendingSessionStore, + taskExecutor.executorScope ).also { currentRegistrationWizard = it } @@ -213,7 +273,8 @@ internal class DefaultAuthenticationService @Inject constructor( retrofitFactory, coroutineDispatchers, sessionCreator, - pendingSessionStore + pendingSessionStore, + taskExecutor.executorScope ).also { currentLoginWizard = it } @@ -230,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() @@ -247,7 +308,7 @@ internal class DefaultAuthenticationService @Inject constructor( pendingSessionData = null - GlobalScope.launch(coroutineDispatchers.main) { + taskExecutor.executorScope.launch(coroutineDispatchers.main) { pendingSessionStore.delete() } } @@ -255,11 +316,31 @@ 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) } } + override fun getWellKnownData(matrixId: String, callback: MatrixCallback): Cancelable { + return getWellknownTask + .configureWith(GetWellknownTask.Params(matrixId)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun directAuthentication(homeServerConnectionConfig: HomeServerConnectionConfig, + matrixId: String, + password: String, + initialDeviceName: String, + callback: MatrixCallback): Cancelable { + return directLoginTask + .configureWith(DirectLoginTask.Params(homeServerConnectionConfig, matrixId, password, initialDeviceName)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + private suspend fun createSessionFromSso(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session = withContext(coroutineDispatchers.computation) { sessionCreator.createSession(credentials, homeServerConnectionConfig) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionCreator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionCreator.kt index 95a9fbb506..74f7cad67d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionCreator.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionCreator.kt @@ -46,14 +46,14 @@ internal class DefaultSessionCreator @Inject constructor( val sessionParams = SessionParams( credentials = credentials, homeServerConnectionConfig = homeServerConnectionConfig.copy( - homeServerUri = credentials.wellKnown?.homeServer?.baseURL + homeServerUri = credentials.discoveryInformation?.homeServer?.baseURL // remove trailing "/" ?.trim { it == '/' } ?.takeIf { it.isNotBlank() } ?.also { Timber.d("Overriding homeserver url to $it") } ?.let { Uri.parse(it) } ?: homeServerConnectionConfig.homeServerUri, - identityServerUri = credentials.wellKnown?.identityServer?.baseURL + identityServerUri = credentials.discoveryInformation?.identityServer?.baseURL // remove trailing "/" ?.trim { it == '/' } ?.takeIf { it.isNotBlank() } 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/login/DirectLoginTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DirectLoginTask.kt new file mode 100644 index 0000000000..90eddf2e14 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DirectLoginTask.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.auth.login + +import dagger.Lazy +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.internal.auth.AuthAPI +import im.vector.matrix.android.internal.auth.SessionCreator +import im.vector.matrix.android.internal.auth.data.PasswordLoginParams +import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.network.RetrofitFactory +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task +import okhttp3.OkHttpClient +import javax.inject.Inject + +internal interface DirectLoginTask : Task { + data class Params( + val homeServerConnectionConfig: HomeServerConnectionConfig, + val userId: String, + val password: String, + val deviceName: String + ) +} + +internal class DefaultDirectLoginTask @Inject constructor( + @Unauthenticated + private val okHttpClient: Lazy, + private val retrofitFactory: RetrofitFactory, + private val sessionCreator: SessionCreator +) : DirectLoginTask { + + override suspend fun execute(params: DirectLoginTask.Params): Session { + val authAPI = retrofitFactory.create(okHttpClient, params.homeServerConnectionConfig.homeServerUri.toString()) + .create(AuthAPI::class.java) + + val loginParams = PasswordLoginParams.userIdentifier(params.userId, params.password, params.deviceName) + + val credentials = executeRequest(null) { + apiCall = authAPI.login(loginParams) + } + + return sessionCreator.createSession(credentials, params.homeServerConnectionConfig) + } +} 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/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 1d3c0f4dcd..d529cf4ae5 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 @@ -446,7 +446,7 @@ internal class DefaultCryptoService @Inject constructor( } override fun getCryptoDeviceInfo(userId: String): List { - return cryptoStore.getUserDevices(userId)?.map { it.value }?.sortedBy { it.deviceId } ?: emptyList() + return cryptoStore.getUserDeviceList(userId) ?: emptyList() } override fun getLiveCryptoDeviceInfo(): LiveData> { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt index d6d8b06b5f..9e116d8223 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt @@ -21,7 +21,7 @@ import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.util.JSON_DICT_PARAMETERIZED_TYPE import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult -import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper +import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper2 import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.di.MoshiProvider @@ -488,7 +488,7 @@ internal class MXOlmDevice @Inject constructor( forwardingCurve25519KeyChain: List, keysClaimed: Map, exportFormat: Boolean): Boolean { - val session = OlmInboundGroupSessionWrapper(sessionKey, exportFormat) + val session = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat) runCatching { getInboundGroupSession(sessionId, senderKey, roomId) } .fold( { @@ -543,18 +543,18 @@ internal class MXOlmDevice @Inject constructor( * @param megolmSessionsData the megolm sessions data * @return the successfully imported sessions. */ - fun importInboundGroupSessions(megolmSessionsData: List): List { - val sessions = ArrayList(megolmSessionsData.size) + fun importInboundGroupSessions(megolmSessionsData: List): List { + val sessions = ArrayList(megolmSessionsData.size) for (megolmSessionData in megolmSessionsData) { val sessionId = megolmSessionData.sessionId val senderKey = megolmSessionData.senderKey val roomId = megolmSessionData.roomId - var session: OlmInboundGroupSessionWrapper? = null + var session: OlmInboundGroupSessionWrapper2? = null try { - session = OlmInboundGroupSessionWrapper(megolmSessionData) + session = OlmInboundGroupSessionWrapper2(megolmSessionData) } catch (e: Exception) { Timber.e(e, "## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId") } @@ -741,7 +741,7 @@ internal class MXOlmDevice @Inject constructor( * @param senderKey the base64-encoded curve25519 key of the sender. * @return the inbound group session. */ - fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): OlmInboundGroupSessionWrapper { + fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): OlmInboundGroupSessionWrapper2 { if (sessionId.isNullOrBlank() || senderKey.isNullOrBlank()) { throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/SetDeviceVerificationAction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/SetDeviceVerificationAction.kt index d6538f041d..d9fa8d5955 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/SetDeviceVerificationAction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/SetDeviceVerificationAction.kt @@ -48,7 +48,7 @@ internal class SetDeviceVerificationAction @Inject constructor( if (device.trustLevel != trustLevel) { device.trustLevel = trustLevel - cryptoStore.storeUserDevice(userId, device) + cryptoStore.setDeviceTrust(userId, deviceId, trustLevel.crossSigningVerified, trustLevel.locallyVerified) } } } 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/keysbackup/DefaultKeysBackupService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/DefaultKeysBackupService.kt index ebef751925..38dae20a83 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/DefaultKeysBackupService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/DefaultKeysBackupService.kt @@ -66,7 +66,7 @@ import im.vector.matrix.android.internal.crypto.keysbackup.tasks.UpdateKeysBacku import im.vector.matrix.android.internal.crypto.keysbackup.util.computeRecoveryKey import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult -import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper +import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper2 import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.SavedKeyBackupKeyInfo import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity @@ -1318,7 +1318,7 @@ internal class DefaultKeysBackupService @Inject constructor( @VisibleForTesting @WorkerThread - fun encryptGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper): KeyBackupData { + fun encryptGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper2): KeyBackupData { // Gather information for each key val device = cryptoStore.deviceWithIdentityKey(olmInboundGroupSessionWrapper.senderKey!!) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/OlmInboundGroupSessionWrapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/OlmInboundGroupSessionWrapper.kt index 9be08d9f2d..cf1a3b237a 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/OlmInboundGroupSessionWrapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/OlmInboundGroupSessionWrapper.kt @@ -103,11 +103,10 @@ class OlmInboundGroupSessionWrapper : Serializable { /** * Export the inbound group session keys - * @param index the index to export. If null, the first known index will be used * * @return the inbound group session as MegolmSessionData if the operation succeeds */ - fun exportKeys(index: Long? = null): MegolmSessionData? { + fun exportKeys(): MegolmSessionData? { return try { if (null == forwardingCurve25519KeyChain) { forwardingCurve25519KeyChain = ArrayList() @@ -117,8 +116,6 @@ class OlmInboundGroupSessionWrapper : Serializable { return null } - val wantedIndex = index ?: olmInboundGroupSession!!.firstKnownIndex - MegolmSessionData( senderClaimedEd25519Key = keysClaimed?.get("ed25519"), forwardingCurve25519KeyChain = ArrayList(forwardingCurve25519KeyChain!!), @@ -126,7 +123,7 @@ class OlmInboundGroupSessionWrapper : Serializable { senderClaimedKeys = keysClaimed, roomId = roomId, sessionId = olmInboundGroupSession!!.sessionIdentifier(), - sessionKey = olmInboundGroupSession!!.export(wantedIndex), + sessionKey = olmInboundGroupSession!!.export(olmInboundGroupSession!!.firstKnownIndex), algorithm = MXCRYPTO_ALGORITHM_MEGOLM ) } catch (e: Exception) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/OlmInboundGroupSessionWrapper2.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/OlmInboundGroupSessionWrapper2.kt new file mode 100755 index 0000000000..c51e707aeb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/OlmInboundGroupSessionWrapper2.kt @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.model + +import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import im.vector.matrix.android.internal.crypto.MegolmSessionData +import org.matrix.olm.OlmInboundGroupSession +import timber.log.Timber +import java.io.Serializable + +/** + * This class adds more context to a OlmInboundGroupSession object. + * This allows additional checks. The class implements Serializable so that the context can be stored. + */ +class OlmInboundGroupSessionWrapper2 : Serializable { + + // The associated olm inbound group session. + var olmInboundGroupSession: OlmInboundGroupSession? = null + + // The room in which this session is used. + var roomId: String? = null + + // The base64-encoded curve25519 key of the sender. + var senderKey: String? = null + + // Other keys the sender claims. + var keysClaimed: Map? = null + + // Devices which forwarded this session to us (normally empty). + var forwardingCurve25519KeyChain: List? = ArrayList() + + /** + * @return the first known message index + */ + val firstKnownIndex: Long? + get() { + if (null != olmInboundGroupSession) { + try { + return olmInboundGroupSession!!.firstKnownIndex + } catch (e: Exception) { + Timber.e(e, "## getFirstKnownIndex() : getFirstKnownIndex failed") + } + } + + return null + } + + /** + * Constructor + * + * @param sessionKey the session key + * @param isImported true if it is an imported session key + */ + constructor(sessionKey: String, isImported: Boolean) { + try { + if (!isImported) { + olmInboundGroupSession = OlmInboundGroupSession(sessionKey) + } else { + olmInboundGroupSession = OlmInboundGroupSession.importSession(sessionKey) + } + } catch (e: Exception) { + Timber.e(e, "Cannot create") + } + } + + constructor() { + // empty + } + /** + * Create a new instance from the provided keys map. + * + * @param megolmSessionData the megolm session data + * @throws Exception if the data are invalid + */ + @Throws(Exception::class) + constructor(megolmSessionData: MegolmSessionData) { + try { + olmInboundGroupSession = OlmInboundGroupSession.importSession(megolmSessionData.sessionKey!!) + + if (olmInboundGroupSession!!.sessionIdentifier() != megolmSessionData.sessionId) { + throw Exception("Mismatched group session Id") + } + + senderKey = megolmSessionData.senderKey + keysClaimed = megolmSessionData.senderClaimedKeys + roomId = megolmSessionData.roomId + } catch (e: Exception) { + throw Exception(e.message) + } + } + + /** + * Export the inbound group session keys + * @param index the index to export. If null, the first known index will be used + * + * @return the inbound group session as MegolmSessionData if the operation succeeds + */ + fun exportKeys(index: Long? = null): MegolmSessionData? { + return try { + if (null == forwardingCurve25519KeyChain) { + forwardingCurve25519KeyChain = ArrayList() + } + + if (keysClaimed == null) { + return null + } + + val wantedIndex = index ?: olmInboundGroupSession!!.firstKnownIndex + + MegolmSessionData( + senderClaimedEd25519Key = keysClaimed?.get("ed25519"), + forwardingCurve25519KeyChain = ArrayList(forwardingCurve25519KeyChain!!), + senderKey = senderKey, + senderClaimedKeys = keysClaimed, + roomId = roomId, + sessionId = olmInboundGroupSession!!.sessionIdentifier(), + sessionKey = olmInboundGroupSession!!.export(wantedIndex), + algorithm = MXCRYPTO_ALGORITHM_MEGOLM + ) + } catch (e: Exception) { + Timber.e(e, "## export() : senderKey $senderKey failed") + null + } + } + + /** + * Export the session for a message index. + * + * @param messageIndex the message index + * @return the exported data + */ + fun exportSession(messageIndex: Long): String? { + if (null != olmInboundGroupSession) { + try { + return olmInboundGroupSession!!.export(messageIndex) + } catch (e: Exception) { + Timber.e(e, "## exportSession() : export failed") + } + } + + return null + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt index 18c85f78fb..69f0985391 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt @@ -30,7 +30,7 @@ import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest import im.vector.matrix.android.internal.crypto.OutgoingSecretRequest import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo -import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper +import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper2 import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody @@ -59,7 +59,7 @@ internal interface IMXCryptoStore { * * @return the list of all known group sessions, to export them. */ - fun getInboundGroupSessions(): List + fun getInboundGroupSessions(): List /** * @return true to unilaterally blacklist all unverified devices. @@ -164,14 +164,6 @@ internal interface IMXCryptoStore { */ fun saveOlmAccount() - /** - * Store a device for a user. - * - * @param userId the user's id. - * @param device the device to store. - */ - fun storeUserDevice(userId: String?, deviceInfo: CryptoDeviceInfo?) - /** * Retrieve a device for a user. * @@ -282,7 +274,7 @@ internal interface IMXCryptoStore { * * @param sessions the inbound group sessions to store. */ - fun storeInboundGroupSessions(sessions: List) + fun storeInboundGroupSessions(sessions: List) /** * Retrieve an inbound group session. @@ -291,7 +283,7 @@ internal interface IMXCryptoStore { * @param senderKey the base64-encoded curve25519 key of the sender. * @return an inbound group session. */ - fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper? + fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2? /** * Remove an inbound group session @@ -315,7 +307,7 @@ internal interface IMXCryptoStore { * * @param sessions the sessions */ - fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List) + fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List) /** * Retrieve inbound group sessions that are not yet backed up. @@ -323,7 +315,7 @@ internal interface IMXCryptoStore { * @param limit the maximum number of sessions to return. * @return an array of non backed up inbound group sessions. */ - fun inboundGroupSessionsToBackup(limit: Int): List + fun inboundGroupSessionsToBackup(limit: Int): List /** * Number of stored inbound group sessions. @@ -415,7 +407,7 @@ internal interface IMXCryptoStore { fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo? fun setUserKeysAsTrusted(userId: String, trusted: Boolean = true) - fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified: Boolean) + fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified: Boolean?) fun clearOtherUserTrust() 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 b033272448..7064663995 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 @@ -38,7 +38,7 @@ import im.vector.matrix.android.internal.crypto.OutgoingSecretRequest import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo -import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper +import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper2 import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody @@ -108,7 +108,7 @@ internal class RealmCryptoStore @Inject constructor( private val olmSessionsToRelease = HashMap() // Cache for InboundGroupSession, to release them properly - private val inboundGroupSessionToRelease = HashMap() + private val inboundGroupSessionToRelease = HashMap() private val newSessionListeners = ArrayList() @@ -233,29 +233,6 @@ internal class RealmCryptoStore @Inject constructor( return olmAccount!! } - override fun storeUserDevice(userId: String?, deviceInfo: CryptoDeviceInfo?) { - if (userId == null || deviceInfo == null) { - return - } - - doRealmTransaction(realmConfiguration) { realm -> - val user = UserEntity.getOrCreate(realm, userId) - - // Create device info - val deviceInfoEntity = CryptoMapper.mapToEntity(deviceInfo) - realm.insertOrUpdate(deviceInfoEntity) -// val deviceInfoEntity = DeviceInfoEntity.getOrCreate(it, userId, deviceInfo.deviceId).apply { -// deviceId = deviceInfo.deviceId -// identityKey = deviceInfo.identityKey() -// putDeviceInfo(deviceInfo) -// } - - if (!user.devices.contains(deviceInfoEntity)) { - user.devices.add(deviceInfoEntity) - } - } - } - override fun getUserDevice(userId: String, deviceId: String): CryptoDeviceInfo? { return doWithRealm(realmConfiguration) { it.where() @@ -654,7 +631,7 @@ internal class RealmCryptoStore @Inject constructor( .toMutableSet() } - override fun storeInboundGroupSessions(sessions: List) { + override fun storeInboundGroupSessions(sessions: List) { if (sessions.isEmpty()) { return } @@ -692,7 +669,7 @@ internal class RealmCryptoStore @Inject constructor( } } - override fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper? { + override fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2? { val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey) // If not in cache (or not found), try to read it from realm @@ -712,10 +689,10 @@ internal class RealmCryptoStore @Inject constructor( } /** - * Note: the result will be only use to export all the keys and not to use the OlmInboundGroupSessionWrapper, + * Note: the result will be only use to export all the keys and not to use the OlmInboundGroupSessionWrapper2, * so there is no need to use or update `inboundGroupSessionToRelease` for native memory management */ - override fun getInboundGroupSessions(): MutableList { + override fun getInboundGroupSessions(): MutableList { return doWithRealm(realmConfiguration) { it.where() .findAll() @@ -787,7 +764,7 @@ internal class RealmCryptoStore @Inject constructor( } } - override fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List) { + override fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List) { if (olmInboundGroupSessionWrappers.isEmpty()) { return } @@ -810,7 +787,7 @@ internal class RealmCryptoStore @Inject constructor( } } - override fun inboundGroupSessionsToBackup(limit: Int): List { + override fun inboundGroupSessionsToBackup(limit: Int): List { return doWithRealm(realmConfiguration) { it.where() .equalTo(OlmInboundGroupSessionEntityFields.BACKED_UP, false) @@ -1276,7 +1253,7 @@ internal class RealmCryptoStore @Inject constructor( } } - override fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified: Boolean) { + override fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified: Boolean?) { doRealmTransaction(realmConfiguration) { realm -> realm.where(DeviceInfoEntity::class.java) .equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId)) @@ -1289,7 +1266,7 @@ internal class RealmCryptoStore @Inject constructor( deviceInfoEntity.trustLevelEntity = it } } else { - trustEntity.locallyVerified = locallyVerified + locallyVerified?.let { trustEntity.locallyVerified = it } trustEntity.crossSignedVerified = crossSignedVerified } } @@ -1429,7 +1406,7 @@ internal class RealmCryptoStore @Inject constructor( } else { // Just override existing, caller should check and untrust id needed val existing = CrossSigningInfoEntity.getOrCreate(realm, userId) - existing.crossSigningKeys.forEach { it.deleteFromRealm() } + existing.crossSigningKeys.deleteAllFromRealm() existing.crossSigningKeys.addAll( info.crossSigningKeys.map { crossSigningKeysMapper.map(it) 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 6ff8a49e28..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 @@ -21,6 +21,8 @@ import com.squareup.moshi.Types import im.vector.matrix.android.api.extensions.tryThis import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper +import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper2 import im.vector.matrix.android.internal.crypto.store.db.mapper.CrossSigningKeysMapper import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMetadataEntityFields @@ -29,6 +31,7 @@ import im.vector.matrix.android.internal.crypto.store.db.model.GossipingEventEnt import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntityFields +import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields @@ -42,7 +45,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi // Version 1L added Cross Signing info persistence companion object { - const val CRYPTO_STORE_SCHEMA_VERSION = 5L + const val CRYPTO_STORE_SCHEMA_VERSION = 6L } override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { @@ -53,6 +56,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi if (oldVersion <= 2) migrateTo3(realm) if (oldVersion <= 3) migrateTo4(realm) if (oldVersion <= 4) migrateTo5(realm) + if (oldVersion <= 5) migrateTo6(realm) } private fun migrateTo1(realm: DynamicRealm) { @@ -216,6 +220,23 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi } } catch (failure: Throwable) { } + + // Migrate frozen classes + val inboundGroupSessions = realm.where("OlmInboundGroupSessionEntity").findAll() + inboundGroupSessions.forEach { dynamicObject -> + dynamicObject.getString(OlmInboundGroupSessionEntityFields.OLM_INBOUND_GROUP_SESSION_DATA)?.let { serializedObject -> + try { + deserializeFromRealm(serializedObject)?.let { oldFormat -> + val newFormat = oldFormat.exportKeys()?.let { + OlmInboundGroupSessionWrapper2(it) + } + dynamicObject.setString(OlmInboundGroupSessionEntityFields.OLM_INBOUND_GROUP_SESSION_DATA, serializeForRealm(newFormat)) + } + } catch (failure: Throwable) { + Timber.e(failure, "## OlmInboundGroupSessionEntity migration failed") + } + } + } } private fun migrateTo5(realm: DynamicRealm) { @@ -238,4 +259,22 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi } } } + + // Fixes duplicate devices in UserEntity#devices + private fun migrateTo6(realm: DynamicRealm) { + val userEntities = realm.where("UserEntity").findAll() + userEntities.forEach { + try { + val deviceList = it.getList(UserEntityFields.DEVICES.`$`) + ?: return@forEach + val distinct = deviceList.distinctBy { it.getString(DeviceInfoEntityFields.DEVICE_ID) } + if (distinct.size != deviceList.size) { + deviceList.clear() + deviceList.addAll(distinct) + } + } catch (failure: Throwable) { + Timber.w(failure, "Crypto Data base migration error for migrateTo6") + } + } + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt index 763e852cd1..125fc94d1e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt @@ -16,12 +16,12 @@ package im.vector.matrix.android.internal.crypto.store.db.model -import im.vector.matrix.android.api.extensions.tryThis -import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper +import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper2 import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm import io.realm.RealmObject import io.realm.annotations.PrimaryKey +import timber.log.Timber internal fun OlmInboundGroupSessionEntity.Companion.createPrimaryKey(sessionId: String?, senderKey: String?) = "$sessionId|$senderKey" @@ -36,11 +36,16 @@ internal open class OlmInboundGroupSessionEntity( var backedUp: Boolean = false) : RealmObject() { - fun getInboundGroupSession(): OlmInboundGroupSessionWrapper? { - return tryThis { deserializeFromRealm(olmInboundGroupSessionData) } + fun getInboundGroupSession(): OlmInboundGroupSessionWrapper2? { + return try { + deserializeFromRealm(olmInboundGroupSessionData) + } catch (failure: Throwable) { + Timber.e(failure, "## Deserialization failure") + return null + } } - fun putInboundGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper?) { + fun putInboundGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper2?) { olmInboundGroupSessionData = serializeForRealm(olmInboundGroupSessionWrapper) } 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/DefaultOutgoingSASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt index 689829b8e3..1480029d6d 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 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..7479c55aa3 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") } 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/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/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/TimelineEventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt index c3e9a8288b..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 @@ -62,8 +62,8 @@ internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm, 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..ea8122bc6d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventFilter.kt @@ -0,0 +1,37 @@ +/* + * 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.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/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 c6c10d9a8f..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 @@ -26,4 +26,11 @@ internal object NetworkConstants { // Media private const val URI_API_MEDIA_PREFIX_PATH = "_matrix/media" const val URI_API_MEDIA_PREFIX_PATH_R0 = "$URI_API_MEDIA_PREFIX_PATH/r0/" + + // Identity server + const val URI_IDENTITY_PREFIX_PATH = "_matrix/identity/v2" + const val URI_IDENTITY_PATH_V2 = "$URI_IDENTITY_PREFIX_PATH/" + + // 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 b20235448a..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,8 @@ 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 import im.vector.matrix.android.internal.session.pushers.PushersModule @@ -50,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 @@ -70,6 +73,9 @@ import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers CacheModule::class, 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..9d5772b82a 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,6 +221,10 @@ internal abstract class SessionModule { } } + @Binds + @Authenticated + abstract fun bindAccessTokenProvider(provider: HomeserverAccessTokenProvider): AccessTokenProvider + @Binds abstract fun bindSession(session: DefaultSession): Session 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/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/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomSharedActionViewModel.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityApiProvider.kt similarity index 62% rename from vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomSharedActionViewModel.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityApiProvider.kt index 91c21378d2..3262a56398 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomSharedActionViewModel.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityApiProvider.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,9 +14,13 @@ * limitations under the License. */ -package im.vector.riotx.features.createdirect +package im.vector.matrix.android.internal.session.identity -import im.vector.riotx.core.platform.VectorSharedActionViewModel +import im.vector.matrix.android.internal.session.SessionScope import javax.inject.Inject -class CreateDirectRoomSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel() +@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..4627911b72 --- /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) } ?: emptyMap() + ) + } +} 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/openid/GetOpenIdTokenTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/openid/GetOpenIdTokenTask.kt new file mode 100644 index 0000000000..c8f394dc47 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/openid/GetOpenIdTokenTask.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.openid + +import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetOpenIdTokenTask : Task + +internal class DefaultGetOpenIdTokenTask @Inject constructor( + @UserId private val userId: String, + private val openIdAPI: OpenIdAPI, + private val eventBus: EventBus) : GetOpenIdTokenTask { + + override suspend fun execute(params: Unit): RequestOpenIdTokenResponse { + return executeRequest(eventBus) { + apiCall = openIdAPI.openIdToken(userId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/openid/OpenIdAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/openid/OpenIdAPI.kt new file mode 100644 index 0000000000..ee2e85a33e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/openid/OpenIdAPI.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.openid + +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.POST +import retrofit2.http.Path + +internal interface OpenIdAPI { + + /** + * Gets a bearer token from the homeserver that the user can + * present to a third party in order to prove their ownership + * of the Matrix account they are logged into. + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-user-userid-openid-request-token + * + * @param userId the user id + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/openid/request_token") + fun openIdToken(@Path("userId") userId: String, @Body body: JsonDict = emptyMap()): Call +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/openid/OpenIdModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/openid/OpenIdModule.kt new file mode 100644 index 0000000000..c6993167e8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/openid/OpenIdModule.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.openid + +import dagger.Binds +import dagger.Module +import dagger.Provides +import retrofit2.Retrofit + +@Module +internal abstract class OpenIdModule { + + @Module + companion object { + @JvmStatic + @Provides + fun providesOpenIdAPI(retrofit: Retrofit): OpenIdAPI { + return retrofit.create(OpenIdAPI::class.java) + } + } + + @Binds + abstract fun bindGetOpenIdTokenTask(task: DefaultGetOpenIdTokenTask): GetOpenIdTokenTask +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/openid/RequestOpenIdTokenResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/openid/RequestOpenIdTokenResponse.kt new file mode 100644 index 0000000000..4beb3fe420 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/openid/RequestOpenIdTokenResponse.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.openid + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class RequestOpenIdTokenResponse( + /** + * Required. An access token the consumer may use to verify the identity of the person who generated the token. + * This is given to the federation API GET /openid/userinfo to verify the user's identity. + */ + @Json(name = "access_token") + val openIdToken: String, + + /** + * Required. The string "Bearer". + */ + @Json(name = "token_type") + val tokenType: String, + + /** + * Required. The homeserver domain the consumer should use when attempting to verify the user's identity. + */ + @Json(name = "matrix_server_name") + val matrixServerName: String, + + /** + * Required. The number of seconds before this token expires and a new one must be generated. + */ + @Json(name = "expires_in") + val expiresIn: Int +) 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/membership/joining/InviteTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteTask.kt index 93b3889455..5a8b302f1b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteTask.kt @@ -39,6 +39,8 @@ internal class DefaultInviteTask @Inject constructor( return executeRequest(eventBus) { val body = InviteBody(params.userId, params.reason) apiCall = roomAPI.invite(params.roomId, body) + isRetryable = true + maxRetryCount = 3 } } } 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 76cdf8c485..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 @@ -36,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 @@ -776,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 } @@ -789,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/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/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/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/session/terms/TermsAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsAPI.kt new file mode 100644 index 0000000000..03b745f8d7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsAPI.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.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 TermsAPI { + /** + * This request does not require authentication + */ + @GET + fun getTerms(@Url url: String): Call + + /** + * This request requires authentication + */ + @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..7c6451e3a0 --- /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() ?: emptyList() + } + + 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/DefaultUserService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt index 761c810b41..7cd2f1b743 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt @@ -91,7 +91,7 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona ) } - override fun getPagedUsersLive(filter: String?): LiveData> { + override fun getPagedUsersLive(filter: String?, excludedUserIds: Set?): LiveData> { realmDataSourceFactory.updateQuery { realm -> val query = realm.where(UserEntity::class.java) if (filter.isNullOrEmpty()) { @@ -104,6 +104,11 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona .contains(UserEntityFields.USER_ID, filter) .endGroup() } + excludedUserIds + ?.takeIf { it.isNotEmpty() } + ?.let { + query.not().`in`(UserEntityFields.USER_ID, it.toTypedArray()) + } query.sort(UserEntityFields.DISPLAY_NAME) } return monarchy.findAllPagedWithChanges(realmDataSourceFactory, livePagedListBuilder) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/SearchUserAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/SearchUserAPI.kt index e57daed617..4adcee88aa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/SearchUserAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/SearchUserAPI.kt @@ -16,7 +16,7 @@ package im.vector.matrix.android.internal.session.user -import im.vector.matrix.android.internal.network.NetworkConstants.URI_API_PREFIX_PATH_R0 +import im.vector.matrix.android.internal.network.NetworkConstants import im.vector.matrix.android.internal.session.user.model.SearchUsersParams import im.vector.matrix.android.internal.session.user.model.SearchUsersResponse import retrofit2.Call @@ -30,6 +30,6 @@ internal interface SearchUserAPI { * * @param searchUsersParams the search params. */ - @POST(URI_API_PREFIX_PATH_R0 + "user_directory/search") + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user_directory/search") fun searchUsers(@Body searchUsersParams: SearchUsersParams): Call } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataAPI.kt index 824af2d1c3..65ec05e76e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataAPI.kt @@ -19,7 +19,6 @@ package im.vector.matrix.android.internal.session.user.accountdata import im.vector.matrix.android.internal.network.NetworkConstants import retrofit2.Call import retrofit2.http.Body -import retrofit2.http.POST import retrofit2.http.PUT import retrofit2.http.Path @@ -34,15 +33,4 @@ interface AccountDataAPI { */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/account_data/{type}") fun setAccountData(@Path("userId") userId: String, @Path("type") type: String, @Body params: Any): Call - - /** - * Gets a bearer token from the homeserver that the user can - * present to a third party in order to prove their ownership - * of the Matrix account they are logged into. - * - * @param userId the user id - * @param body the body content - */ - @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/openid/request_token") - fun openIdToken(@Path("userId") userId: String, @Body body: Map): Call> } 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 new file mode 100644 index 0000000000..7c81a03223 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/UrlUtils.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.util + +import java.net.URL + +internal fun String.isValidUrl(): Boolean { + return try { + URL(this) + true + } catch (t: Throwable) { + 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/wellknown/GetWellknownTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/GetWellknownTask.kt new file mode 100644 index 0000000000..c6f6b8752d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/GetWellknownTask.kt @@ -0,0 +1,199 @@ +/* + * 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.wellknown + +import android.util.MalformedJsonException +import dagger.Lazy +import im.vector.matrix.android.api.MatrixPatterns +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.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 +import java.io.EOFException +import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection + +internal interface GetWellknownTask : Task { + data class Params( + val matrixId: String + ) +} + +/** + * Inspired from AutoDiscovery class from legacy Matrix Android SDK + */ +internal class DefaultGetWellknownTask @Inject constructor( + @Unauthenticated + private val okHttpClient: Lazy, + private val retrofitFactory: RetrofitFactory +) : GetWellknownTask { + + override suspend fun execute(params: GetWellknownTask.Params): WellknownResult { + if (!MatrixPatterns.isUserId(params.matrixId)) { + return WellknownResult.InvalidMatrixId + } + + val homeServerDomain = params.matrixId.substringAfter(":") + + return findClientConfig(homeServerDomain) + } + + /** + * Find client config + * + * - Do the .well-known request + * - validate homeserver url and identity server url if provide in .well-known result + * - return action and .well-known data + * + * @param domain: homeserver domain, deduced from mx userId (ex: "matrix.org" from userId "@user:matrix.org") + */ + private suspend fun findClientConfig(domain: String): WellknownResult { + val wellKnownAPI = retrofitFactory.create(okHttpClient, "https://dummy.org") + .create(WellKnownAPI::class.java) + + return try { + val wellKnown = executeRequest(null) { + apiCall = wellKnownAPI.getWellKnown(domain) + } + + // Success + val homeServerBaseUrl = wellKnown.homeServer?.baseURL + if (homeServerBaseUrl.isNullOrBlank()) { + WellknownResult.FailPrompt + } else { + if (homeServerBaseUrl.isValidUrl()) { + // Check that HS is a real one + validateHomeServer(homeServerBaseUrl, wellKnown) + } else { + WellknownResult.FailError + } + } + } catch (throwable: Throwable) { + when (throwable) { + is Failure.NetworkConnection -> { + WellknownResult.Ignore + } + is Failure.OtherServerError -> { + when (throwable.httpCode) { + HttpsURLConnection.HTTP_NOT_FOUND -> WellknownResult.Ignore + else -> WellknownResult.FailPrompt + } + } + is MalformedJsonException, is EOFException -> { + WellknownResult.FailPrompt + } + else -> { + throw throwable + } + } + } + } + + /** + * Return true if home server is valid, and (if applicable) if identity server is pingable + */ + private suspend fun validateHomeServer(homeServerBaseUrl: String, wellKnown: WellKnown): WellknownResult { + val capabilitiesAPI = retrofitFactory.create(okHttpClient, homeServerBaseUrl) + .create(CapabilitiesAPI::class.java) + + try { + executeRequest(null) { + apiCall = capabilitiesAPI.ping() + } + } catch (throwable: Throwable) { + return WellknownResult.FailError + } + + return if (wellKnown.identityServer == null) { + // No identity server + WellknownResult.Prompt(homeServerBaseUrl, null, wellKnown) + } else { + // if m.identity_server is present it must be valid + val identityServerBaseUrl = wellKnown.identityServer.baseURL + if (identityServerBaseUrl.isNullOrBlank()) { + WellknownResult.FailError + } else { + if (identityServerBaseUrl.isValidUrl()) { + if (validateIdentityServer(identityServerBaseUrl)) { + // All is ok + WellknownResult.Prompt(homeServerBaseUrl, identityServerBaseUrl, wellKnown) + } else { + WellknownResult.FailError + } + } else { + WellknownResult.FailError + } + } + } + } + + /** + * Return true if identity server is pingable + */ + private suspend fun validateIdentityServer(identityServerBaseUrl: String): Boolean { + val identityPingApi = retrofitFactory.create(okHttpClient, identityServerBaseUrl) + .create(IdentityAuthAPI::class.java) + + return try { + executeRequest(null) { + apiCall = identityPingApi.ping() + } + + true + } catch (throwable: Throwable) { + false + } + } + + /** + * Try to get an identity server URL from a home server URL, using a .wellknown request + */ + /* + fun getIdentityServer(homeServerUrl: String, callback: ApiCallback) { + if (homeServerUrl.startsWith("https://")) { + wellKnownRestClient.getWellKnown(homeServerUrl.substring("https://".length), + object : SimpleApiCallback(callback) { + override fun onSuccess(info: WellKnown) { + callback.onSuccess(info.identityServer?.baseURL) + } + }) + } else { + callback.onUnexpectedError(InvalidParameterException("malformed url")) + } + } + + fun getServerPreferredIntegrationManagers(homeServerUrl: String, callback: ApiCallback>) { + if (homeServerUrl.startsWith("https://")) { + wellKnownRestClient.getWellKnown(homeServerUrl.substring("https://".length), + object : SimpleApiCallback(callback) { + override fun onSuccess(info: WellKnown) { + callback.onSuccess(info.getIntegrationManagers()) + } + }) + } else { + callback.onUnexpectedError(InvalidParameterException("malformed url")) + } + } + */ +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/WellKnownAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/WellKnownAPI.kt new file mode 100644 index 0000000000..ec62707db7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/WellKnownAPI.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.wellknown + +import im.vector.matrix.android.api.auth.data.WellKnown +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Path + +internal interface WellKnownAPI { + @GET("https://{domain}/.well-known/matrix/client") + fun getWellKnown(@Path("domain") domain: String): Call +} diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomSharedAction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/WellknownModule.kt similarity index 61% rename from vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomSharedAction.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/WellknownModule.kt index eeffc1f119..2705803fec 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomSharedAction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/WellknownModule.kt @@ -1,5 +1,5 @@ /* - * 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. @@ -14,12 +14,14 @@ * limitations under the License. */ -package im.vector.riotx.features.createdirect +package im.vector.matrix.android.internal.wellknown -import im.vector.riotx.core.platform.VectorSharedAction +import dagger.Binds +import dagger.Module -sealed class CreateDirectRoomSharedAction : VectorSharedAction { - object OpenUsersDirectory : CreateDirectRoomSharedAction() - object Close : CreateDirectRoomSharedAction() - object GoBack : CreateDirectRoomSharedAction() +@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 dc874c2b94..871d01175e 100644 --- a/matrix-sdk-android/src/main/res/values-de/strings.xml +++ b/matrix-sdk-android/src/main/res/values-de/strings.xml @@ -179,8 +179,8 @@ %1$s\'s Einladung. Grund: %2$s %1$s hat %2$s eingeladen. Grund: %3$s %1$s hat dich eingeladen. Grund: %2$s - %1$s beigetreten. Grund: %2$s - %1$s ging. Grund: %2$s + %1$s ist dem Raum beigetreten. Grund: %2$s + %1$s hat den Raum verlassen. Grund: %2$s %1$s hat die Einladung abgelehnt. Grund: %2$s %1$s hat %2$s gekickt. Grund: %3$s %1$s hat Sperre von %2$s aufgehoben. Grund: %3$s diff --git a/matrix-sdk-android/src/main/res/values-eo/strings.xml b/matrix-sdk-android/src/main/res/values-eo/strings.xml index c88d96d610..69600394ac 100644 --- a/matrix-sdk-android/src/main/res/values-eo/strings.xml +++ b/matrix-sdk-android/src/main/res/values-eo/strings.xml @@ -3,20 +3,210 @@ %1$s sendis bildon. %1$s sendis glumarkon. - invito de %s - %1$s invitis %2$s + Invito de %s + %1$s invitis uzanton %2$s %1$s invitis vin %1$s alvenis %1$s foriris %1$s malakceptis la inviton - %1$s forpelis %2$s - %1$s malforbaris %2$s - %1$s forbaris %2$s - %1$s malinvitis %2$s + %1$s forpelis uzanton %2$s + %1$s malforbaris uzanton %2$s + %1$s forbaris uzanton %2$s + %1$s nuligis inviton por %2$s %1$s ŝanĝis sian profilbildon ** Ne eblas malĉifri: %s ** La aparato de la sendanto ne sendis al ni la ŝlosilojn por tiu mesaĝo. - Respondanta al + Responde al + + %1$s: %2$s + %1$s ŝanĝis sian vidigan nomon al %2$s + %1$s ŝanĝis sian vidigan nomon de %2$s al %3$s + %1$s forigis sian vidigan nomon (%2$s) + %1$s ŝanĝis la temon al: %2$s + %1$s ŝanĝis nomon de la ĉambro al: %2$s + %s vidvokis. + %s voĉvokis. + %s respondis la vokon. + %s finis la vokon. + %1$s videbligis estontan historion de ĉambro al %2$s + ĉiuj ĉambranoj, ekde iliaj invitoj. + ĉiuj ĉambranoj, ekde iliaj aliĝoj. + ĉiuj ĉambranoj. + ĉiu ajn. + nekonata (%s). + %1$s ŝaltis tutvojan ĉifradon (%2$s) + %s gradaltigis la ĉambron. + + Mesaĝo foriĝis + Mesaĝo foriĝis de %1$s + Mesaĝo foriĝis [kialo: %1$s] + Mesaĝo foriĝis de %1$s [kialo: %2$s] + %1$s ĝisdatigis sian profilon %2$s + %1$s sendis aliĝan inviton al %2$s + %1$s nuligis la aliĝan inviton por %2$s + %1$s akceptis la inviton por %2$s + + Ne povis redakti + Ne povas sendi mesaĝon + + Malsukcesis alŝuti bildon + + Reta eraro + Matrix-eraro + + Nun ne eblas re-aliĝi al malplena ĉambro + + Ĉifrita mesaĝo + + Retpoŝtadreso + Telefonnumero + + sendis bildon. + sendis filmon. + sendis sondosieron. + sendis dosieron. + + Invito de %s + Ĉambra invito + + %1$s kaj %2$s + + + %1$s kaj 1 alia + %1$s kaj %2$d aliaj + + + Malplena ĉambro + + + Hundo + Kato + Leono + Ĉevalo + Unukorno + Porko + Elefanto + Kuniklo + Pando + Koko + Pingveno + Testudo + Fiŝo + Polpo + Papilio + Floro + Arbo + Kakto + Fungo + Globo + Luno + Nubo + Fajro + Banano + Pomo + Frago + Maizo + Pico + Kuko + Koro + Mieneto + Roboto + Ĉapelo + Okulvitroj + Boltilo + Kristnaska viro + Dikfingro supren + Ombrelo + Sablohorloĝo + Horloĝo + Donaco + Lampo + Libro + Grifelo + Paperkuntenilo + Tondilo + Seruro + Ŝlosilo + Martelo + Telefono + Flago + Vagonaro + Biciklo + Aviadilo + Raketo + Trofeo + Pilko + Gitaro + Trumpeto + Sonorilo + Ankro + Kapaŭdilo + Dosierujo + Pinglo + + Komenca spegulado: +\nEnportante konton… + Komenca spegulado: +\nEnportante ĉifrilojn + Komenca spegulado: +\nEnportante ĉambrojn + Komenca spegulado: +\nEnportante aliĝitajn ĉambrojn + Komenca spegulado: +\nEnportante ĉambrojn de invitoj + Komenca spegulado: +\nEnportante forlasitajn ĉambrojn + Komenca spegulado: +\nEnportante komunumojn + Komenca spegulado: +\nEnportante datumojn de konto + + Sendante mesaĝon… + Vakigi sendan atendovicon + + %1$s petis grupan vokon + Grupa voko komenciĝis + Grupa voko finiĝis + + (ankaŭ profilbildo ŝanĝiĝis) + %1$s forigis nomon de la ĉambro + %1$s forigis temon de la ĉambro + Invito de %1$s. Kialo: %2$s + %1$s invitis uzanton %2$s. Kialo: %3$s + %1$s invitis vin. Kialo: %2$s + %1$s aliĝis al la ĉambro. Kialo: %2$s + %1$s foriris de la ĉambro. Kialo: %2$s + %1$s rifuzis la inviton. Kialo: %2$s + %1$s forpelis uzanton %2$s. Kialo: %3$s + %1$s malforbaris uzanton %2$s. Kialo: %3$s + %1$s forbaris uzanton %2$s. Kialo: %3$s + %1$s sendis inviton al la ĉambro al %2$s. Kialo: %3$s + %1$s nuligis la inviton al la ĉambro al %2$s. Kialo: %3$s + %1$s akceptis la inviton por %2$s. Kialo: %3$s + %1$s nuligis la inviton al %2$s. Kialo: %3$s + + + %1$s aldonis %2$s kiel adreson por ĉi tiu ĉambro. + %1$s aldonis %2$s kiel adresojn por ĉi tiu ĉambro. + + + + %1$s forigis %2$s kiel adreson por ĉi tiu ĉambro. + %1$s forigis %2$s kiel adresojn por ĉi tiu ĉambro. + + + %1$s aldonis %2$s kaj forigis %3$s kiel adresojn por ĉi tiu ĉambro. + + %1$s agordis la ĉefadreson por ĉi tiu ĉambro al %2$s. + %1$s forigis la ĉefadreson de ĉi tiu ĉambro. + + %1$s permesis al gastoj aliĝi al la ĉambro. + %1$s malpermesis al gastoj aliĝi al la ĉambro. + + %1$s ŝaltis tutvojan ĉifradon. + %1$s ŝaltis tutvojan ĉifradon (kun nerekonita algoritmo %2$s). + + %s petas kontrolon de via ŝlosilo, sed via kliento ne subtenas kontrolon de ŝlosiloj en la babilujo. Vi devos uzi malnovecan kontrolon de ŝlosiloj. diff --git a/matrix-sdk-android/src/main/res/values-et/strings.xml b/matrix-sdk-android/src/main/res/values-et/strings.xml new file mode 100644 index 0000000000..1d52c2a7a1 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-et/strings.xml @@ -0,0 +1,188 @@ + + + %1$s: %2$s + %1$s saatis pildi. + %1$s saatis kleepsu. + + Kasutaja %s kutse + %1$s kutsus kasutajat %2$s + %1$s kutsus sind + %1$s liitus jututoaga + %1$s lahkus jututoast + %1$s lükkas tagasi kutse + %1$s müksas kasutajat %2$s + %1$s võttis tagasi kutse kasutajale %2$s + %1$s muutis oma avatari + %1$s määras oma kuvatavaks nimeks %2$s + %1$s muutis senise kuvatava nime %2$s uueks nimeks %3$s + %1$s eemaldas oma kuvatava nime (%2$s) + %1$s muutis uueks teemaks %2$s + %1$s muutis jututoa uueks nimeks %2$s + %s alustas videokõnet. + %s alustas häälkõnet. + %s vastas kõnele. + %s lõpetas kõne. + %1$s seadistas, et tulevane jututoa ajalugu on nähtav kasutajale %2$s + kõikidele jututoa liikmetele alates kutsumise hetkest. + kõikidele jututoa liikmetele alates liitumise hetkest. + kõikidele jututoa liikmetele. + kõikidele. + teadmata (%s). + %1$s lülitas sisse läbiva krüptimise (%2$s) + %s uuendas seda jututuba. + + %1$s saatis VoIP konverentsi kutse + VoIP-konverents algas + VoIP-konverents lõppes + + (samuti sai avatar muudetud) + %1$s eemaldas jututoa nime + %1$s eemaldas jututoa teema + Sõnum on eemaldatud + Sõnum on eemaldatud %1$s poolt + Sõnum on eemaldatud [põhjus: %1$s] + Sõnum on eemaldatud %1$s poolt [põhjus: %2$s] + %1$s uuendas oma profiili %2$s + %1$s saatis jututoaga liitumiseks kutse kasutajale %2$s + %1$s võttis tagasi jututoaga liitumise kutse kasutajalt %2$s + %1$s võttis vastu kutse %2$s nimel + + ** Ei õnnestu dekrüptida: %s ** + Sõnumi saatja seade ei ole selle sõnumi jaoks saatnud dekrüptimisvõtmeid. + + Vastuseks kasutajale + + Ei saanud muuta sõnumit + Sõnumi saatmine ei õnnestunud + + Faili üles laadimine ei õnnestunud + + Võrguühenduse viga + Matrix\'i viga + + Hetkel ei ole võimalik uuesti liituda tühja jututoaga. + + Krüptitud sõnum + + E-posti aadress + Telefoninumber + + saatis pildi. + saatis video. + saatis helifaili. + saatis faili. + + Kutse kasutajalt %s + Kutse jututuppa + + %1$s ja %2$s + + + %1$s ja üks muu + %1$s ja %2$d muud + + + Tühi jututuba + + + Koer + Kass + Lõvi + Hobune + Ükssarvik + Siga + Elevant + Jänes + Panda + Kukk + Pingviin + Kilpkonn + Kala + Kaheksajalg + Liblikas + Lill + Puu + Kaktus + Seen + Maakera + Kuu + Pilv + Tuli + Banaan + Õun + Maasikas + Mais + Pitsa + Kook + Süda + Smaili + Robot + Müts + Prillid + Mutrivõti + Jõuluvana + Pöidlad püsti + Vihmavari + Liivakell + Kell + Kingitus + Lambipirn + Raamat + Pliiats + Kirjaklamber + Käärid + Lukk + Võti + Haamer + Telefon + Lipp + Rong + Jalgratas + Lennuk + Rakett + Auhind + Pall + Kitarr + Trompet + Kelluke + Ankur + Kõrvaklapid + Kaust + Knopka + + Alglaadimine: +\nImpordin kontot… + Alglaadimine: +\nImpordin krüptoseadistusi + Alglaadimine: +\nImpordin jututubasid + Alglaadimine: +\nImpordin liitutud jututubasid + Alglaadimine: +\nImpordin kutsutud jututubasid + Alglaadimine: +\nImpordin lahkutud jututubasid + Alglaadimine: +\nImpordin kogukondi + Alglaadimine: +\nImpordin kontoandmeid + + Saadan sõnumit… + Tühjenda saatmisjärjekord + + Kasutaja %1$s kutse. Põhjus: %2$s + %1$s kutsus kasutajat %2$s. Põhjus: %3$s + %1$s kutsus sind. Põhjus: %2$s + %1$s liitus jututoaga. Põhjus: %2$s + %1$s lahkus jututoast. Põhjus: %2$s + %1$s lükkas kutse tagasi. Põhjus: %2$s + %1$s müksas välja kasutaja %2$s. Põhjus: %3$s + %1$s saatis kasutajale %2$s kutse jututoaga liitumiseks. Põhjus: %3$s + %1$s tühistas kasutajale %2$s saadetud kutse jututoaga liitumiseks. Põhjus: %3$s + %1$s võttis vastu kutse %2$s jututoaga liitumiseks. Põhjus: %3$s + %1$s võttis tagasi kasutajale %2$s saadetud kutse. Põhjus: %3$s + + %1$s lülitas sisse läbiva krüptimise. + %1$s lülitas sisse läbiva krüptimise (tundmatu algoritm %2$s). + + diff --git a/matrix-sdk-android/src/main/res/values-fi/strings.xml b/matrix-sdk-android/src/main/res/values-fi/strings.xml index 9487aa7db4..8dd87b6b6a 100644 --- a/matrix-sdk-android/src/main/res/values-fi/strings.xml +++ b/matrix-sdk-android/src/main/res/values-fi/strings.xml @@ -209,4 +209,6 @@ %1$s laittoi päälle osapuolten välisen salauksen. %1$s laittoi päälle osapuolisten välisen salauksen (tuntematon algoritmi %2$s). + %s haluaa varmentaa salausavaimesi, mutta asiakasohjelmasi ei tue keskustelun aikana tapahtuvaa avainten varmennusta. Joudut käyttämään perinteistä varmennustapaa. + diff --git a/matrix-sdk-android/src/main/res/values-sq/strings.xml b/matrix-sdk-android/src/main/res/values-sq/strings.xml index e966f22064..521c805be8 100644 --- a/matrix-sdk-android/src/main/res/values-sq/strings.xml +++ b/matrix-sdk-android/src/main/res/values-sq/strings.xml @@ -4,8 +4,8 @@ %1$s dërgoi një figurë. %1$s ftoi %2$s %1$s ju ftoi - %1$s u bë pjesë - %1$s iku + %1$s hyri në dhomë + %1$s doli nga dhoma %1$s hodhi tej ftesën %1$s përzuri %2$s %1$s dëboi %2$s @@ -172,8 +172,8 @@ Ftesë e %1$s. Arsye: %2$s %1$s ftoi %2$s. Arsye: %3$s %1$s ju ftoi. Arsye: %2$s - %1$s erdhi. Arsye: %2$s - %1$s iku. Arsye: %2$s + %1$s erdhi në dhomë. Arsye: %2$s + %1$s doli nga dhoma. Arsye: %2$s %1$s hodhi poshtë ftesën. Arsye: %2$s %1$s përzuri %2$s. Arsye: %3$s %1$s hoqi dëbimin për %2$s. Arsye: %3$s diff --git a/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml b/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml index 38affc0599..cdd9c5eb8d 100644 --- a/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml +++ b/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml @@ -5,8 +5,8 @@ %s 的邀请 %1$s 邀请了 %2$s %1$s 邀请了您 - %1$s 加入了 - %1$s 离开了 + %1$s 加入了聊天室 + %1$s 离开了聊天室 %1$s 拒绝了邀请 %1$s 移除了 %2$s %1$s 解封了 %2$s @@ -173,8 +173,8 @@ %1$s 的邀请。理由:%2$s %1$s 邀请了 %2$s。理由:%3$s %1$s 邀请了您。理由:%2$s - %1$s 已加入。理由:%2$s - %1$s 已离开。理由:%2$s + %1$s 加入了聊天室。理由:%2$s + %1$s 离开了聊天室。理由:%2$s %1$s 已拒绝邀请。理由:%2$s %1$s 踢走了 %2$s。理由:%3$s %1$s 取消封锁了 %2$s。理由:%3$s 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/release/sign_apk_unsafe.sh b/tools/release/sign_apk_unsafe.sh index 022f3618eb..bf021e8345 100755 --- a/tools/release/sign_apk_unsafe.sh +++ b/tools/release/sign_apk_unsafe.sh @@ -23,7 +23,7 @@ PARAM_KS_PASS=$3 PARAM_KEY_PASS=$4 # Other params -BUILD_TOOLS_VERSION="28.0.3" +BUILD_TOOLS_VERSION="29.0.3" MIN_SDK_VERSION=19 echo "Signing APK with build-tools version ${BUILD_TOOLS_VERSION} for min SDK version ${MIN_SDK_VERSION}..." 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..74fc96a425 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() { diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 092817a6cc..7c2939707f 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -85,6 +85,7 @@ + @@ -160,6 +161,8 @@ android:name=".features.attachments.preview.AttachmentsPreviewActivity" android:theme="@style/AppTheme.AttachmentsPreview" /> + + 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/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/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/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/createdirect/CreateDirectRoomAction.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomAction.kt index 0e74ff71fd..f995f82ff7 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomAction.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, @@ -20,10 +20,5 @@ import im.vector.matrix.android.api.session.user.model.User import im.vector.riotx.core.platform.VectorViewModelAction sealed class CreateDirectRoomAction : VectorViewModelAction { - object CreateRoomAndInviteSelectedUsers : CreateDirectRoomAction() - data class FilterKnownUsers(val value: String) : CreateDirectRoomAction() - data class SearchDirectoryUsers(val value: String) : CreateDirectRoomAction() - object ClearFilterKnownUsers : CreateDirectRoomAction() - data class SelectUser(val user: User) : CreateDirectRoomAction() - data class RemoveSelectedUser(val user: User) : CreateDirectRoomAction() + data class CreateRoomAndInviteSelectedUsers(val selectedUsers: Set) : CreateDirectRoomAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt index 3ae206cd21..ef3e9bdeff 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt @@ -37,6 +37,12 @@ import im.vector.riotx.core.extensions.addFragment import im.vector.riotx.core.extensions.addFragmentToBackstack import im.vector.riotx.core.platform.SimpleFragmentActivity import im.vector.riotx.core.platform.WaitingViewData +import im.vector.riotx.features.userdirectory.KnownUsersFragment +import im.vector.riotx.features.userdirectory.KnownUsersFragmentArgs +import im.vector.riotx.features.userdirectory.UserDirectoryFragment +import im.vector.riotx.features.userdirectory.UserDirectorySharedAction +import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel +import im.vector.riotx.features.userdirectory.UserDirectoryViewModel import kotlinx.android.synthetic.main.activity.* import java.net.HttpURLConnection import javax.inject.Inject @@ -44,7 +50,8 @@ import javax.inject.Inject class CreateDirectRoomActivity : SimpleFragmentActivity() { private val viewModel: CreateDirectRoomViewModel by viewModel() - private lateinit var sharedActionViewModel: CreateDirectRoomSharedActionViewModel + private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel + @Inject lateinit var userDirectoryViewModelFactory: UserDirectoryViewModel.Factory @Inject lateinit var createDirectRoomViewModelFactory: CreateDirectRoomViewModel.Factory @Inject lateinit var errorFormatter: ErrorFormatter @@ -56,26 +63,40 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) toolbar.visibility = View.GONE - sharedActionViewModel = viewModelProvider.get(CreateDirectRoomSharedActionViewModel::class.java) + sharedActionViewModel = viewModelProvider.get(UserDirectorySharedActionViewModel::class.java) sharedActionViewModel .observe() .subscribe { sharedAction -> when (sharedAction) { - CreateDirectRoomSharedAction.OpenUsersDirectory -> - addFragmentToBackstack(R.id.container, CreateDirectRoomDirectoryUsersFragment::class.java) - CreateDirectRoomSharedAction.Close -> finish() - CreateDirectRoomSharedAction.GoBack -> onBackPressed() + UserDirectorySharedAction.OpenUsersDirectory -> + addFragmentToBackstack(R.id.container, UserDirectoryFragment::class.java) + UserDirectorySharedAction.Close -> finish() + UserDirectorySharedAction.GoBack -> onBackPressed() + is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction) } } .disposeOnDestroy() if (isFirstCreation()) { - addFragment(R.id.container, CreateDirectRoomKnownUsersFragment::class.java) + addFragment( + R.id.container, + KnownUsersFragment::class.java, + KnownUsersFragmentArgs( + title = getString(R.string.fab_menu_create_chat), + menuResId = R.menu.vector_create_direct_room + ) + ) } viewModel.selectSubscribe(this, CreateDirectRoomViewState::createAndInviteState) { renderCreateAndInviteState(it) } } + private fun onMenuItemSelected(action: UserDirectorySharedAction.OnMenuItemSelected) { + if (action.itemId == R.id.action_create_direct_room) { + viewModel.handle(CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(action.selectedUsers)) + } + } + private fun renderCreateAndInviteState(state: Async) { when (state) { is Loading -> renderCreationLoading() diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewEvents.kt index 0ed584ac6b..5ea344115a 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewEvents.kt @@ -18,7 +18,4 @@ package im.vector.riotx.features.createdirect import im.vector.riotx.core.platform.VectorViewEvents -/** - * Transient events for create direct room screen - */ sealed class CreateDirectRoomViewEvents : VectorViewEvents diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt index 71fae11486..1800759da6 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt @@ -1,42 +1,31 @@ /* + * Copyright (c) 2020 New Vector Ltd * - * * 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. + * 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.createdirect -import arrow.core.Option import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext -import com.jakewharton.rxrelay2.BehaviorRelay import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams -import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.rx.rx -import im.vector.riotx.core.extensions.toggle import im.vector.riotx.core.platform.VectorViewModel -import io.reactivex.Single -import io.reactivex.android.schedulers.AndroidSchedulers -import java.util.concurrent.TimeUnit - -private typealias KnowUsersFilter = String -private typealias DirectoryUsersSearch = String class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted initialState: CreateDirectRoomViewState, @@ -48,9 +37,6 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted fun create(initialState: CreateDirectRoomViewState): CreateDirectRoomViewModel } - private val knownUsersFilter = BehaviorRelay.createDefault>(Option.empty()) - private val directoryUsersSearch = BehaviorRelay.create() - companion object : MvRxViewModelFactory { @JvmStatic @@ -60,25 +46,15 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted } } - init { - observeKnownUsers() - observeDirectoryUsers() - } - override fun handle(action: CreateDirectRoomAction) { when (action) { - is CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers -> createRoomAndInviteSelectedUsers() - is CreateDirectRoomAction.FilterKnownUsers -> knownUsersFilter.accept(Option.just(action.value)) - is CreateDirectRoomAction.ClearFilterKnownUsers -> knownUsersFilter.accept(Option.empty()) - is CreateDirectRoomAction.SearchDirectoryUsers -> directoryUsersSearch.accept(action.value) - is CreateDirectRoomAction.SelectUser -> handleSelectUser(action) - is CreateDirectRoomAction.RemoveSelectedUser -> handleRemoveSelectedUser(action) + is CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers -> createRoomAndInviteSelectedUsers(action.selectedUsers) } } - private fun createRoomAndInviteSelectedUsers() = withState { currentState -> + private fun createRoomAndInviteSelectedUsers(selectedUsers: Set) { val roomParams = CreateRoomParams( - invitedUserIds = currentState.selectedUsers.map { it.userId } + invitedUserIds = selectedUsers.map { it.userId } ) .setDirectMessage() .enableEncryptionIfInvitedUsersSupportIt() @@ -89,52 +65,4 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted copy(createAndInviteState = it) } } - - private fun handleRemoveSelectedUser(action: CreateDirectRoomAction.RemoveSelectedUser) = withState { state -> - val selectedUsers = state.selectedUsers.minus(action.user) - setState { copy(selectedUsers = selectedUsers) } - } - - private fun handleSelectUser(action: CreateDirectRoomAction.SelectUser) = withState { state -> - // Reset the filter asap - directoryUsersSearch.accept("") - val selectedUsers = state.selectedUsers.toggle(action.user) - setState { copy(selectedUsers = selectedUsers) } - } - - private fun observeDirectoryUsers() { - directoryUsersSearch - .debounce(300, TimeUnit.MILLISECONDS) - .switchMapSingle { search -> - val stream = if (search.isBlank()) { - Single.just(emptyList()) - } else { - session.rx() - .searchUsersDirectory(search, 50, emptySet()) - .map { users -> - users.sortedBy { it.toMatrixItem().firstLetterOfDisplayName() } - } - } - stream.toAsync { - copy(directoryUsers = it, directorySearchTerm = search) - } - } - .subscribe() - .disposeOnClear() - } - - private fun observeKnownUsers() { - knownUsersFilter - .throttleLast(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .switchMap { - session.rx().livePagedUsers(it.orNull()) - } - .execute { async -> - copy( - knownUsers = async, - filterKnownUsersValue = knownUsersFilter.value ?: Option.empty() - ) - } - } } diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewState.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewState.kt index dcf86ef6f1..8bb8c3ce58 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewState.kt @@ -1,41 +1,25 @@ /* + * Copyright (c) 2020 New Vector Ltd * - * * 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. + * 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.createdirect -import androidx.paging.PagedList -import arrow.core.Option import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized -import im.vector.matrix.android.api.session.user.model.User data class CreateDirectRoomViewState( - val knownUsers: Async> = Uninitialized, - val directoryUsers: Async> = Uninitialized, - val selectedUsers: Set = emptySet(), - val createAndInviteState: Async = Uninitialized, - val directorySearchTerm: String = "", - val filterKnownUsersValue: Option = Option.empty() -) : MvRxState { - - enum class DisplayMode { - KNOWN_USERS, - DIRECTORY_USERS - } -} + val createAndInviteState: Async = Uninitialized +) : MvRxState diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyViewModel.kt index c8406570d3..faada7ba3e 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyViewModel.kt @@ -51,7 +51,7 @@ class KeysBackupRestoreFromKeyViewModel @Inject constructor( try { sharedViewModel.recoverUsingBackupPass(recoveryKey) } catch (failure: Throwable) { - recoveryCodeErrorText.value = stringProvider.getString(R.string.keys_backup_recovery_code_error_decrypt) + recoveryCodeErrorText.postValue(stringProvider.getString(R.string.keys_backup_recovery_code_error_decrypt)) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseFragment.kt index 0947c144d8..e334603b74 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseFragment.kt @@ -15,7 +15,6 @@ */ package im.vector.riotx.features.crypto.keysbackup.restore -import android.content.Context import android.os.Bundle import android.text.Editable import android.text.SpannableString @@ -70,7 +69,7 @@ class KeysBackupRestoreFromPassphraseFragment @Inject constructor() : VectorBase mPassphraseInputLayout.error = newValue }) - helperTextWithLink.text = spannableStringForHelperText(context!!) + helperTextWithLink.text = spannableStringForHelperText() viewModel.showPasswordMode.observe(viewLifecycleOwner, Observer { val shouldBeVisible = it ?: false @@ -87,9 +86,9 @@ class KeysBackupRestoreFromPassphraseFragment @Inject constructor() : VectorBase } } - private fun spannableStringForHelperText(context: Context): SpannableString { - val clickableText = context.getString(R.string.keys_backup_restore_use_recovery_key) - val helperText = context.getString(R.string.keys_backup_restore_with_passphrase_helper_with_link, clickableText) + private fun spannableStringForHelperText(): SpannableString { + val clickableText = getString(R.string.keys_backup_restore_use_recovery_key) + val helperText = getString(R.string.keys_backup_restore_with_passphrase_helper_with_link, clickableText) val spanString = SpannableString(helperText) @@ -117,7 +116,7 @@ class KeysBackupRestoreFromPassphraseFragment @Inject constructor() : VectorBase fun onRestoreBackup() { val value = viewModel.passphrase.value if (value.isNullOrBlank()) { - viewModel.passphraseErrorText.value = context?.getString(R.string.passphrase_empty_error_message) + viewModel.passphraseErrorText.value = getString(R.string.passphrase_empty_error_message) } else { viewModel.recoverKeys(sharedViewModel) } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseViewModel.kt index 46e8d5fa18..9c9c12b824 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseViewModel.kt @@ -51,7 +51,7 @@ class KeysBackupRestoreFromPassphraseViewModel @Inject constructor( try { sharedViewModel.recoverUsingBackupPass(passphrase.value!!) } catch (failure: Throwable) { - passphraseErrorText.value = stringProvider.getString(R.string.keys_backup_passphrase_error_decrypt) + passphraseErrorText.postValue(stringProvider.getString(R.string.keys_backup_passphrase_error_decrypt)) } } } 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/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsViewEvents.kt similarity index 60% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt rename to vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsViewEvents.kt index 6e89a28b7d..6fd45394a2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt +++ b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsViewEvents.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,10 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.database.query +package im.vector.riotx.features.discovery -internal object FilterContent { +import im.vector.riotx.core.platform.VectorViewEvents - internal const val EDIT_TYPE = """{*"m.relates_to"*"rel_type":*"m.replace"*}""" - internal const val RESPONSE_TYPE = """{*"m.relates_to"*"rel_type":*"m.response"*}""" +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..7c5086afa7 --- /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() ?: emptyList() + val phones = phoneNumbersList() ?: emptyList() + 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() ?: emptyList() + val phones = phoneNumbersList() ?: emptyList() + 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