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 803b974413..a4767f8be9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,30 @@ -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 🐛: + - Fix issues with FontScale switch (#69, #645) + +Translations 🗣: + - + +SDK API changes ⚠️: + - + +Build 🧱: + - + +Other changes: + - + +Changes in RiotX 0.20.0 (2020-05-15) =================================================== Features ✨: @@ -13,18 +39,10 @@ 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) - -Translations 🗣: - - + - Crash | crash while restoring key backup (#1366) SDK API changes ⚠️: - - excludedUserIds parameter add to to UserService.getPagedUsersLive() function - -Build 🧱: - - - -Other 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/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt index e92da1e424..a60e83ec96 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt @@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo import im.vector.matrix.android.api.session.group.GroupSummaryQueryParams import im.vector.matrix.android.api.session.group.model.GroupSummary +import im.vector.matrix.android.api.session.identity.ThreePid import im.vector.matrix.android.api.session.pushers.Pusher import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams import im.vector.matrix.android.api.session.room.model.RoomSummary @@ -94,6 +95,11 @@ class RxSession(private val session: Session) { return session.getPagedUsersLive(filter, excludedUserIds).asObservable() } + fun liveThreePIds(refreshData: Boolean): Observable> { + return session.getThreePidsLive(refreshData).asObservable() + .startWithCallable { session.getThreePids() } + } + fun createRoom(roomParams: CreateRoomParams): Single = singleBuilder { session.createRoom(roomParams, it) } diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 514d1accae..abc860d1ff 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -158,6 +158,9 @@ dependencies { // Bus implementation 'org.greenrobot:eventbus:3.1.1' + // Phone number https://github.com/google/libphonenumber + implementation 'com.googlecode.libphonenumber:libphonenumber:8.10.23' + debugImplementation 'com.airbnb.okreplay:okreplay:1.5.0' releaseImplementation 'com.airbnb.okreplay:noop:1.5.0' androidTestImplementation 'com.airbnb.okreplay:espresso:1.5.0' diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt index 5bc8653f3d..3ca04a86d1 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 9278bed918..f9aef3604a 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 @@ -246,7 +246,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { assertNotNull(eventWireContent.get("session_id")) assertNotNull(eventWireContent.get("sender_key")) - assertEquals(senderSession.sessionParams.credentials.deviceId, eventWireContent.get("device_id")) + assertEquals(senderSession.sessionParams.deviceId, eventWireContent.get("device_id")) assertNotNull(event.eventId) assertEquals(roomId, event.roomId) diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/crosssigning/XSigningTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/crosssigning/XSigningTest.kt index f8d30a2679..da3bbdc23c 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/crosssigning/XSigningTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/crosssigning/XSigningTest.kt @@ -122,7 +122,7 @@ class XSigningTest : InstrumentedTest { // We will want to test that in alice POV, this new device would be trusted by cross signing val bobSession2 = mTestHelper.logIntoAccount(bobUserId, SessionTestParams(true)) - val bobSecondDeviceId = bobSession2.sessionParams.credentials.deviceId!! + val bobSecondDeviceId = bobSession2.sessionParams.deviceId!! // Check that bob first session sees the new login val data = mTestHelper.doSync> { diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt index bb6e020d89..57ab4aaf33 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt @@ -148,7 +148,7 @@ class KeyShareTests : InstrumentedTest { // Mark the device as trusted aliceSession.cryptoService().setDeviceVerification(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession.myUserId, - aliceSession2.sessionParams.credentials.deviceId ?: "") + aliceSession2.sessionParams.deviceId ?: "") // Re request aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root) @@ -253,12 +253,12 @@ class KeyShareTests : InstrumentedTest { }) val txId: String = "m.testVerif12" - aliceVerificationService2.beginKeyVerification(VerificationMethod.SAS, aliceSession1.myUserId, aliceSession1.sessionParams.credentials.deviceId + aliceVerificationService2.beginKeyVerification(VerificationMethod.SAS, aliceSession1.myUserId, aliceSession1.sessionParams.deviceId ?: "", txId) mTestHelper.waitWithLatch { latch -> mTestHelper.retryPeriodicallyWithLatch(latch) { - aliceSession1.cryptoService().getDeviceInfo(aliceSession1.myUserId, aliceSession2.sessionParams.credentials.deviceId ?: "")?.isVerified == true + aliceSession1.cryptoService().getDeviceInfo(aliceSession1.myUserId, aliceSession2.sessionParams.deviceId ?: "")?.isVerified == true } } diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt index 59ef24beec..2e698a929e 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt @@ -835,7 +835,7 @@ class KeysBackupTest : InstrumentedTest { assertTrue(signature.valid) assertNotNull(signature.device) assertEquals(cryptoTestData.firstSession.cryptoService().getMyDevice().deviceId, signature.deviceId) - assertEquals(signature.device!!.deviceId, cryptoTestData.firstSession.sessionParams.credentials.deviceId) + assertEquals(signature.device!!.deviceId, cryptoTestData.firstSession.sessionParams.deviceId) stateObserver.stopAndCheckStates(null) cryptoTestData.cleanUp(mTestHelper) @@ -997,7 +997,7 @@ class KeysBackupTest : InstrumentedTest { keysBackup.backupAllGroupSessions(null, it) } - val oldDeviceId = cryptoTestData.firstSession.sessionParams.credentials.deviceId!! + val oldDeviceId = cryptoTestData.firstSession.sessionParams.deviceId!! val oldKeyBackupVersion = keysBackup.currentBackupVersion val aliceUserId = cryptoTestData.firstSession.myUserId diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/SASTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/SASTest.kt index 1ac70d7f2b..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 5150420de2..effeae596a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/AuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/AuthenticationService.kt @@ -20,7 +20,6 @@ import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.LoginFlowResult -import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.login.LoginWizard import im.vector.matrix.android.api.auth.registration.RegistrationWizard import im.vector.matrix.android.api.auth.wellknown.WellknownResult @@ -37,6 +36,11 @@ interface AuthenticationService { */ fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback): Cancelable + /** + * Request the supported login flows for the corresponding sessionId. + */ + fun getLoginFlowOfSession(sessionId: String, callback: MatrixCallback): Cancelable + /** * Return a LoginWizard, to login to the homeserver. The login flow has to be retrieved first. */ @@ -74,15 +78,6 @@ interface AuthenticationService { */ fun getLastAuthenticatedSession(): Session? - /** - * Get an authenticated session. You should at least call authenticate one time before. - * If you logout, this session will no longer be valid. - * - * @param sessionParams the sessionParams to open with. - * @return the associated session if any, or null - */ - fun getSession(sessionParams: SessionParams): Session? - /** * Create a session after a SSO successful login */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/SessionParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/SessionParams.kt index 2d65cac43d..1cbba50af7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/SessionParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/SessionParams.kt @@ -21,7 +21,48 @@ package im.vector.matrix.android.api.auth.data * You don't have to manually instantiate it. */ data class SessionParams( + /** + * Please consider using shortcuts instead + */ val credentials: Credentials, + + /** + * Please consider using shortcuts instead + */ val homeServerConnectionConfig: HomeServerConnectionConfig, + + /** + * Set to false if the current token is not valid anymore. Application should not have to use this info. + */ val isTokenValid: Boolean -) +) { + /* + * Shortcuts. Usually the application should only need to use these shortcuts + */ + + /** + * The userId of the session (Ex: "@user:domain.org") + */ + val userId = credentials.userId + + /** + * The deviceId of the session (Ex: "ABCDEFGH") + */ + val deviceId = credentials.deviceId + + /** + * The current homeserver Url. It can be different that the homeserver url entered + * during login phase, because a redirection may have occurred + */ + val homeServerUrl = homeServerConnectionConfig.homeServerUri.toString() + + /** + * The current homeserver host + */ + val homeServerHost = homeServerConnectionConfig.homeServerUri.host + + /** + * The default identity server url if any, returned by the homeserver during login phase + */ + val defaultIdentityServerUrl = homeServerConnectionConfig.identityServerUri?.toString() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/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 3ba275fd5f..1b9544bed6 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 import im.vector.matrix.android.api.session.widgets.WidgetService @@ -55,6 +57,7 @@ interface Session : SignOutService, FilterService, FileService, + TermsService, ProfileService, PushRuleService, PushersService, @@ -79,7 +82,7 @@ interface Session : * Useful shortcut to get access to the userId */ val myUserId: String - get() = sessionParams.credentials.userId + get() = sessionParams.userId /** * The sessionId @@ -147,6 +150,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/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 232cb3f541..68f404cb71 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt @@ -25,16 +25,15 @@ import im.vector.matrix.android.internal.auth.db.AuthRealmMigration import im.vector.matrix.android.internal.auth.db.AuthRealmModule import im.vector.matrix.android.internal.auth.db.RealmPendingSessionStore import im.vector.matrix.android.internal.auth.db.RealmSessionParamsStore -import im.vector.matrix.android.internal.auth.wellknown.DefaultDirectLoginTask -import im.vector.matrix.android.internal.auth.wellknown.DefaultGetWellknownTask -import im.vector.matrix.android.internal.auth.wellknown.DirectLoginTask -import im.vector.matrix.android.internal.auth.wellknown.GetWellknownTask +import im.vector.matrix.android.internal.auth.login.DefaultDirectLoginTask +import im.vector.matrix.android.internal.auth.login.DirectLoginTask import im.vector.matrix.android.internal.database.RealmKeysUtils import im.vector.matrix.android.internal.di.AuthDatabase +import im.vector.matrix.android.internal.wellknown.WellknownModule import io.realm.RealmConfiguration import java.io.File -@Module +@Module(includes = [WellknownModule::class]) internal abstract class AuthModule { @Module @@ -74,9 +73,6 @@ internal abstract class AuthModule { @Binds abstract fun bindSessionCreator(creator: DefaultSessionCreator): SessionCreator - @Binds - abstract fun bindGetWellknownTask(task: DefaultGetWellknownTask): GetWellknownTask - @Binds abstract fun bindDirectLoginTask(task: DefaultDirectLoginTask): DirectLoginTask } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt index 997cf70e5a..b543fa7507 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt @@ -23,7 +23,6 @@ import im.vector.matrix.android.api.auth.AuthenticationService import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.LoginFlowResult -import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.data.Versions import im.vector.matrix.android.api.auth.data.isLoginAndRegistrationSupportedBySdk import im.vector.matrix.android.api.auth.data.isSupportedBySdk @@ -33,14 +32,14 @@ import im.vector.matrix.android.api.auth.wellknown.WellknownResult import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.api.util.NoOpCancellable import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.auth.data.LoginFlowResponse import im.vector.matrix.android.internal.auth.data.RiotConfig import im.vector.matrix.android.internal.auth.db.PendingSessionData import im.vector.matrix.android.internal.auth.login.DefaultLoginWizard +import im.vector.matrix.android.internal.auth.login.DirectLoginTask import im.vector.matrix.android.internal.auth.registration.DefaultRegistrationWizard -import im.vector.matrix.android.internal.auth.wellknown.DirectLoginTask -import im.vector.matrix.android.internal.auth.wellknown.GetWellknownTask import im.vector.matrix.android.internal.di.Unauthenticated import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.network.executeRequest @@ -50,7 +49,7 @@ import im.vector.matrix.android.internal.task.launchToCallback import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.exhaustive import im.vector.matrix.android.internal.util.toCancelable -import kotlinx.coroutines.GlobalScope +import im.vector.matrix.android.internal.wellknown.GetWellknownTask import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okhttp3.OkHttpClient @@ -87,14 +86,21 @@ internal class DefaultAuthenticationService @Inject constructor( } } - override fun getSession(sessionParams: SessionParams): Session? { - return sessionManager.getOrCreateSession(sessionParams) + override fun getLoginFlowOfSession(sessionId: String, callback: MatrixCallback): Cancelable { + val homeServerConnectionConfig = sessionParamsStore.get(sessionId)?.homeServerConnectionConfig + + return if (homeServerConnectionConfig == null) { + callback.onFailure(IllegalStateException("Session not found")) + NoOpCancellable + } else { + getLoginFlow(homeServerConnectionConfig, callback) + } } override fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback): Cancelable { pendingSessionData = null - return GlobalScope.launch(coroutineDispatchers.main) { + return taskExecutor.executorScope.launch(coroutineDispatchers.main) { pendingSessionStore.delete() val result = runCatching { @@ -246,7 +252,8 @@ internal class DefaultAuthenticationService @Inject constructor( retrofitFactory, coroutineDispatchers, sessionCreator, - pendingSessionStore + pendingSessionStore, + taskExecutor.executorScope ).also { currentRegistrationWizard = it } @@ -266,7 +273,8 @@ internal class DefaultAuthenticationService @Inject constructor( retrofitFactory, coroutineDispatchers, sessionCreator, - pendingSessionStore + pendingSessionStore, + taskExecutor.executorScope ).also { currentLoginWizard = it } @@ -283,7 +291,7 @@ internal class DefaultAuthenticationService @Inject constructor( pendingSessionData = pendingSessionData?.homeServerConnectionConfig ?.let { PendingSessionData(it) } .also { - GlobalScope.launch(coroutineDispatchers.main) { + taskExecutor.executorScope.launch(coroutineDispatchers.main) { if (it == null) { // Should not happen pendingSessionStore.delete() @@ -300,7 +308,7 @@ internal class DefaultAuthenticationService @Inject constructor( pendingSessionData = null - GlobalScope.launch(coroutineDispatchers.main) { + taskExecutor.executorScope.launch(coroutineDispatchers.main) { pendingSessionStore.delete() } } @@ -308,7 +316,7 @@ internal class DefaultAuthenticationService @Inject constructor( override fun createSessionFromSso(homeServerConnectionConfig: HomeServerConnectionConfig, credentials: Credentials, callback: MatrixCallback): Cancelable { - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { createSessionFromSso(credentials, homeServerConnectionConfig) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt index ebd50a6924..a4db0e84f7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt @@ -51,7 +51,7 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) { } return SessionParamsEntity( sessionParams.credentials.sessionId(), - sessionParams.credentials.userId, + sessionParams.userId, credentialsJson, homeServerConnectionConfigJson, sessionParams.isTokenValid) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DefaultLoginWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DefaultLoginWizard.kt index 4d98ddcf08..132073b340 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DefaultLoginWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DefaultLoginWizard.kt @@ -38,7 +38,7 @@ import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.launchToCallback import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers -import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.withContext import okhttp3.OkHttpClient @@ -47,7 +47,8 @@ internal class DefaultLoginWizard( retrofitFactory: RetrofitFactory, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val sessionCreator: SessionCreator, - private val pendingSessionStore: PendingSessionStore + private val pendingSessionStore: PendingSessionStore, + private val coroutineScope: CoroutineScope ) : LoginWizard { private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here") @@ -59,7 +60,7 @@ internal class DefaultLoginWizard( password: String, deviceName: String, callback: MatrixCallback): Cancelable { - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { loginInternal(login, password, deviceName) } } @@ -80,7 +81,7 @@ internal class DefaultLoginWizard( } override fun resetPassword(email: String, newPassword: String, callback: MatrixCallback): Cancelable { - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { resetPasswordInternal(email, newPassword) } } @@ -108,7 +109,7 @@ internal class DefaultLoginWizard( callback.onFailure(IllegalStateException("developer error, no reset password in progress")) return NoOpCancellable } - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { resetPasswordMailConfirmedInternal(safeResetPasswordData) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/DirectLoginTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DirectLoginTask.kt similarity index 97% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/DirectLoginTask.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DirectLoginTask.kt index 01a3ab192d..90eddf2e14 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/DirectLoginTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DirectLoginTask.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.auth.wellknown +package im.vector.matrix.android.internal.auth.login import dagger.Lazy import im.vector.matrix.android.api.auth.data.Credentials diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt index 29970b6c0c..5a39de72ca 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt @@ -33,7 +33,7 @@ import im.vector.matrix.android.internal.auth.db.PendingSessionData import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.task.launchToCallback import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers -import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import okhttp3.OkHttpClient @@ -45,7 +45,8 @@ internal class DefaultRegistrationWizard( private val retrofitFactory: RetrofitFactory, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val sessionCreator: SessionCreator, - private val pendingSessionStore: PendingSessionStore + private val pendingSessionStore: PendingSessionStore, + private val coroutineScope: CoroutineScope ) : RegistrationWizard { private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here") @@ -72,7 +73,7 @@ internal class DefaultRegistrationWizard( override fun getRegistrationFlow(callback: MatrixCallback): Cancelable { val params = RegistrationParams() - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { performRegistrationRequest(params) } } @@ -86,7 +87,7 @@ internal class DefaultRegistrationWizard( password = password, initialDeviceDisplayName = initialDeviceDisplayName ) - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { performRegistrationRequest(params) .also { pendingSessionData = pendingSessionData.copy(isRegistrationStarted = true) @@ -101,7 +102,7 @@ internal class DefaultRegistrationWizard( return NoOpCancellable } val params = RegistrationParams(auth = AuthParams.createForCaptcha(safeSession, response)) - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { performRegistrationRequest(params) } } @@ -112,13 +113,13 @@ internal class DefaultRegistrationWizard( return NoOpCancellable } val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.TERMS, session = safeSession)) - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { performRegistrationRequest(params) } } override fun addThreePid(threePid: RegisterThreePid, callback: MatrixCallback): Cancelable { - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { pendingSessionData = pendingSessionData.copy(currentThreePidData = null) .also { pendingSessionStore.savePendingSessionData(it) } @@ -131,7 +132,7 @@ internal class DefaultRegistrationWizard( callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) return NoOpCancellable } - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { sendThreePid(safeCurrentThreePid) } } @@ -177,13 +178,13 @@ internal class DefaultRegistrationWizard( callback.onFailure(IllegalStateException("developer error, no pending three pid")) return NoOpCancellable } - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { performRegistrationRequest(safeParam, delayMillis) } } override fun handleValidateThreePid(code: String, callback: MatrixCallback): Cancelable { - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { validateThreePid(code) } } @@ -199,7 +200,7 @@ internal class DefaultRegistrationWizard( code = code ) val validationResponse = validateCodeTask.execute(ValidateCodeTask.Params(url, validationBody)) - if (validationResponse.success == true) { + if (validationResponse.isSuccess()) { // The entered code is correct // Same than validate email return performRegistrationRequest(registrationParams, 3_000) @@ -214,7 +215,7 @@ internal class DefaultRegistrationWizard( callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) return NoOpCancellable } - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.DUMMY, session = safeSession)) performRegistrationRequest(params) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt index 2cd52f702e..5bdc9579e0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.matrix.androidsdk.rest.model.login +package im.vector.matrix.android.internal.auth.registration import android.os.Parcelable import kotlinx.android.parcel.Parcelize diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/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/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/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 47ae0199cf..03cdbfdf4e 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 5168d0728e..f798dbcf41 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.findLastLiveChunkFromRoom(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 ab6745148f..56e6ee0953 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt @@ -28,8 +28,9 @@ internal object NetworkConstants { const val URI_API_MEDIA_PREFIX_PATH_R0 = "$URI_API_MEDIA_PREFIX_PATH/r0/" // Identity server - const val URI_IDENTITY_PATH = "_matrix/identity/api/v1/" - const val URI_IDENTITY_PATH_V2 = "_matrix/identity/v2/" + const val URI_IDENTITY_PREFIX_PATH = "_matrix/identity/v2" + const val URI_IDENTITY_PATH_V2 = "$URI_IDENTITY_PREFIX_PATH/" - const val URI_API_PREFIX_IDENTITY = "_matrix/identity/api/v1" + // TODO Ganfra, use correct value + const val URI_INTEGRATION_MANAGER_PATH = "TODO/" } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/httpclient/OkHttpClientUtil.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/httpclient/OkHttpClientUtil.kt new file mode 100644 index 0000000000..8ffa0553e9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/httpclient/OkHttpClientUtil.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.network.httpclient + +import im.vector.matrix.android.internal.network.AccessTokenInterceptor +import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor +import im.vector.matrix.android.internal.network.token.AccessTokenProvider +import okhttp3.OkHttpClient + +internal fun OkHttpClient.addAccessTokenInterceptor(accessTokenProvider: AccessTokenProvider): OkHttpClient { + return newBuilder() + .apply { + // Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor + val existingCurlInterceptors = interceptors().filterIsInstance() + interceptors().removeAll(existingCurlInterceptors) + + addInterceptor(AccessTokenInterceptor(accessTokenProvider)) + + // Re add eventually the curl logging interceptors + existingCurlInterceptors.forEach { + addInterceptor(it) + } + } + .build() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/token/AccessTokenProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/token/AccessTokenProvider.kt new file mode 100644 index 0000000000..08176392fa --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/token/AccessTokenProvider.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.network.token + +internal interface AccessTokenProvider { + fun getToken(): String? +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/token/HomeserverAccessTokenProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/token/HomeserverAccessTokenProvider.kt new file mode 100644 index 0000000000..b570cb362e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/token/HomeserverAccessTokenProvider.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.network.token + +import im.vector.matrix.android.internal.auth.SessionParamsStore +import im.vector.matrix.android.internal.di.SessionId +import javax.inject.Inject + +internal class HomeserverAccessTokenProvider @Inject constructor( + @SessionId private val sessionId: String, + private val sessionParamsStore: SessionParamsStore +) : AccessTokenProvider { + override fun getToken() = sessionParamsStore.get(sessionId)?.credentials?.accessToken +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt index cfb0d23f2b..0cdd39f117 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt @@ -28,10 +28,10 @@ import im.vector.matrix.android.internal.di.ExternalFilesDirectory import im.vector.matrix.android.internal.di.SessionCacheDirectory import im.vector.matrix.android.internal.di.Unauthenticated import im.vector.matrix.android.internal.extensions.foldToCallback +import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.toCancelable import im.vector.matrix.android.internal.util.writeToFile -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okhttp3.OkHttpClient @@ -51,7 +51,9 @@ internal class DefaultFileService @Inject constructor( private val contentUrlResolver: ContentUrlResolver, @Unauthenticated private val okHttpClient: OkHttpClient, - private val coroutineDispatchers: MatrixCoroutineDispatchers) : FileService { + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val taskExecutor: TaskExecutor +) : FileService { /** * Download file in the cache folder, and eventually decrypt it @@ -63,7 +65,7 @@ internal class DefaultFileService @Inject constructor( url: String?, elementToDecrypt: ElementToDecrypt?, callback: MatrixCallback): Cancelable { - return GlobalScope.launch(coroutineDispatchers.main) { + return taskExecutor.executorScope.launch(coroutineDispatchers.main) { withContext(coroutineDispatchers.io) { Try { val folder = File(sessionCacheDirectory, "MF") diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index aa9df9c496..0052c1d7a2 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.api.session.widgets.WidgetService import im.vector.matrix.android.internal.auth.SessionParamsStore @@ -51,14 +52,16 @@ 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.integrationmanager.IntegrationManager 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.session.widgets.WidgetManager +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 @@ -85,6 +88,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, @@ -103,8 +107,11 @@ internal class DefaultSession @Inject constructor( private val timelineEventDecryptor: TimelineEventDecryptor, private val integrationManager: IntegrationManager, private val widgetManager: WidgetManager, - 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(), @@ -114,6 +121,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(), @@ -142,6 +150,7 @@ internal class DefaultSession @Inject constructor( shieldTrustUpdater.start() integrationManager.start() widgetManager.start() + defaultIdentityService.start() } override fun requireBackgroundSync() { @@ -186,6 +195,10 @@ internal class DefaultSession @Inject constructor( shieldTrustUpdater.stop() integrationManager.stop() widgetManager.stop() + taskExecutor.executorScope.launch(coroutineDispatchers.main) { + // This has to be done on main thread + defaultIdentityService.stop() + } } override fun getSyncStateLive(): LiveData { @@ -215,7 +228,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) } } @@ -229,6 +242,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) } @@ -239,6 +254,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 a99c2ba0aa..3fd8472edf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt @@ -36,6 +36,7 @@ import im.vector.matrix.android.internal.session.filter.FilterModule import im.vector.matrix.android.internal.session.group.GetGroupDataWorker import im.vector.matrix.android.internal.session.group.GroupModule import im.vector.matrix.android.internal.session.homeserver.HomeServerCapabilitiesModule +import im.vector.matrix.android.internal.session.identity.IdentityModule import im.vector.matrix.android.internal.session.openid.OpenIdModule import im.vector.matrix.android.internal.session.profile.ProfileModule import im.vector.matrix.android.internal.session.pushers.AddHttpPusherWorker @@ -51,6 +52,7 @@ import im.vector.matrix.android.internal.session.sync.SyncModule import im.vector.matrix.android.internal.session.sync.SyncTask import im.vector.matrix.android.internal.session.sync.SyncTokenStore import im.vector.matrix.android.internal.session.sync.job.SyncWorker +import im.vector.matrix.android.internal.session.terms.TermsModule import im.vector.matrix.android.internal.session.user.UserModule import im.vector.matrix.android.internal.session.user.accountdata.AccountDataModule import im.vector.matrix.android.internal.session.widgets.WidgetModule @@ -74,6 +76,8 @@ import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers PushersModule::class, OpenIdModule::class, WidgetModule::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/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityApiProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityApiProvider.kt new file mode 100644 index 0000000000..3262a56398 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityApiProvider.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity + +import im.vector.matrix.android.internal.session.SessionScope +import javax.inject.Inject + +@SessionScope +internal class IdentityApiProvider @Inject constructor() { + + var identityApi: IdentityAPI? = null +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAuthAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAuthAPI.kt new file mode 100644 index 0000000000..04abf5fe6c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAuthAPI.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity + +import im.vector.matrix.android.internal.network.NetworkConstants +import im.vector.matrix.android.internal.session.identity.model.IdentityRegisterResponse +import im.vector.matrix.android.internal.session.openid.RequestOpenIdTokenResponse +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST + +/** + * Ref: https://matrix.org/docs/spec/identity_service/latest + * This contain the requests which do not need an identity server token + */ +internal interface IdentityAuthAPI { + + /** + * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery + * Simple ping call to check if server exists and is alive + * + * Ref: https://matrix.org/docs/spec/identity_service/unstable#status-check + * https://matrix.org/docs/spec/identity_service/latest#get-matrix-identity-v2 + * + * @return 200 in case of success + */ + @GET(NetworkConstants.URI_IDENTITY_PREFIX_PATH) + fun ping(): Call + + /** + * Ping v1 will be used to check outdated Identity server + */ + @GET("_matrix/identity/api/v1") + fun pingV1(): Call + + /** + * Exchanges an OpenID token from the homeserver for an access token to access the identity server. + * The request body is the same as the values returned by /openid/request_token in the Client-Server API. + */ + @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "account/register") + fun register(@Body openIdToken: RequestOpenIdTokenResponse): Call +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityBulkLookupTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityBulkLookupTask.kt new file mode 100644 index 0000000000..9f1579af60 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityBulkLookupTask.kt @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity + +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.failure.MatrixError +import im.vector.matrix.android.api.session.identity.FoundThreePid +import im.vector.matrix.android.api.session.identity.IdentityServiceError +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.matrix.android.api.session.identity.toMedium +import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments.base64ToBase64Url +import im.vector.matrix.android.internal.crypto.tools.withOlmUtility +import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.identity.data.IdentityStore +import im.vector.matrix.android.internal.session.identity.model.IdentityHashDetailResponse +import im.vector.matrix.android.internal.session.identity.model.IdentityLookUpParams +import im.vector.matrix.android.internal.session.identity.model.IdentityLookUpResponse +import im.vector.matrix.android.internal.task.Task +import java.util.Locale +import javax.inject.Inject + +internal interface IdentityBulkLookupTask : Task> { + data class Params( + val threePids: List + ) +} + +internal class DefaultIdentityBulkLookupTask @Inject constructor( + private val identityApiProvider: IdentityApiProvider, + private val identityStore: IdentityStore, + @UserId private val userId: String +) : IdentityBulkLookupTask { + + override suspend fun execute(params: IdentityBulkLookupTask.Params): List { + val identityAPI = getIdentityApiAndEnsureTerms(identityApiProvider, userId) + val identityData = identityStore.getIdentityData() ?: throw IdentityServiceError.NoIdentityServerConfigured + val pepper = identityData.hashLookupPepper + val hashDetailResponse = if (pepper == null) { + // We need to fetch the hash details first + fetchAndStoreHashDetails(identityAPI) + } else { + IdentityHashDetailResponse(pepper, identityData.hashLookupAlgorithm) + } + + if (hashDetailResponse.algorithms.contains("sha256").not()) { + // TODO We should ask the user if he is ok to send their 3Pid in clear, but for the moment we do not do it + // Also, what we have in cache could be outdated, the identity server maybe now supports sha256 + throw IdentityServiceError.BulkLookupSha256NotSupported + } + + val hashedAddresses = withOlmUtility { olmUtility -> + params.threePids.map { threePid -> + base64ToBase64Url( + olmUtility.sha256(threePid.value.toLowerCase(Locale.ROOT) + + " " + threePid.toMedium() + " " + hashDetailResponse.pepper) + ) + } + } + + val identityLookUpV2Response = lookUpInternal(identityAPI, hashedAddresses, hashDetailResponse, true) + + // Convert back to List + return handleSuccess(params.threePids, hashedAddresses, identityLookUpV2Response) + } + + private suspend fun lookUpInternal(identityAPI: IdentityAPI, + hashedAddresses: List, + hashDetailResponse: IdentityHashDetailResponse, + canRetry: Boolean): IdentityLookUpResponse { + return try { + executeRequest(null) { + apiCall = identityAPI.lookup(IdentityLookUpParams( + hashedAddresses, + IdentityHashDetailResponse.ALGORITHM_SHA256, + hashDetailResponse.pepper + )) + } + } catch (failure: Throwable) { + // Catch invalid hash pepper and retry + if (canRetry && failure is Failure.ServerError && failure.error.code == MatrixError.M_INVALID_PEPPER) { + // This is not documented, by the error can contain the new pepper! + if (!failure.error.newLookupPepper.isNullOrEmpty()) { + // Store it and use it right now + hashDetailResponse.copy(pepper = failure.error.newLookupPepper) + .also { identityStore.setHashDetails(it) } + .let { lookUpInternal(identityAPI, hashedAddresses, it, false /* Avoid infinite loop */) } + } else { + // Retrieve the new hash details + val newHashDetailResponse = fetchAndStoreHashDetails(identityAPI) + + if (hashDetailResponse.algorithms.contains(IdentityHashDetailResponse.ALGORITHM_SHA256).not()) { + // TODO We should ask the user if he is ok to send their 3Pid in clear, but for the moment we do not do it + // Also, what we have in cache is maybe outdated, the identity server maybe now support sha256 + throw IdentityServiceError.BulkLookupSha256NotSupported + } + + lookUpInternal(identityAPI, hashedAddresses, newHashDetailResponse, false /* Avoid infinite loop */) + } + } else { + // Other error + throw failure + } + } + } + + private suspend fun fetchAndStoreHashDetails(identityAPI: IdentityAPI): IdentityHashDetailResponse { + return executeRequest(null) { + apiCall = identityAPI.hashDetails() + } + .also { identityStore.setHashDetails(it) } + } + + private fun handleSuccess(threePids: List, hashedAddresses: List, identityLookUpResponse: IdentityLookUpResponse): List { + return identityLookUpResponse.mappings.keys.map { hashedAddress -> + FoundThreePid(threePids[hashedAddresses.indexOf(hashedAddress)], identityLookUpResponse.mappings[hashedAddress] ?: error("")) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityDisconnectTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityDisconnectTask.kt new file mode 100644 index 0000000000..abed3962bc --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityDisconnectTask.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity + +import im.vector.matrix.android.api.session.identity.IdentityServiceError +import im.vector.matrix.android.internal.di.AuthenticatedIdentity +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.network.token.AccessTokenProvider +import im.vector.matrix.android.internal.task.Task +import timber.log.Timber +import javax.inject.Inject + +internal interface IdentityDisconnectTask : Task + +internal class DefaultIdentityDisconnectTask @Inject constructor( + private val identityApiProvider: IdentityApiProvider, + @AuthenticatedIdentity + private val accessTokenProvider: AccessTokenProvider +) : IdentityDisconnectTask { + + override suspend fun execute(params: Unit) { + val identityAPI = identityApiProvider.identityApi ?: throw IdentityServiceError.NoIdentityServerConfigured + + // Ensure we have a token. + // We can have an identity server configured, but no token yet. + if (accessTokenProvider.getToken() == null) { + Timber.d("No token to disconnect identity server.") + return + } + + executeRequest(null) { + apiCall = identityAPI.logout() + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityModule.kt new file mode 100644 index 0000000000..7a5790788b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityModule.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity + +import dagger.Binds +import dagger.Module +import dagger.Provides +import im.vector.matrix.android.internal.database.RealmKeysUtils +import im.vector.matrix.android.internal.di.AuthenticatedIdentity +import im.vector.matrix.android.internal.di.IdentityDatabase +import im.vector.matrix.android.internal.di.SessionFilesDirectory +import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.di.UserMd5 +import im.vector.matrix.android.internal.network.httpclient.addAccessTokenInterceptor +import im.vector.matrix.android.internal.network.token.AccessTokenProvider +import im.vector.matrix.android.internal.session.SessionModule +import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.session.identity.data.IdentityStore +import im.vector.matrix.android.internal.session.identity.db.IdentityRealmModule +import im.vector.matrix.android.internal.session.identity.db.RealmIdentityStore +import io.realm.RealmConfiguration +import okhttp3.OkHttpClient +import java.io.File + +@Module +internal abstract class IdentityModule { + + @Module + companion object { + @JvmStatic + @Provides + @SessionScope + @AuthenticatedIdentity + fun providesOkHttpClient(@Unauthenticated okHttpClient: OkHttpClient, + @AuthenticatedIdentity accessTokenProvider: AccessTokenProvider): OkHttpClient { + return okHttpClient.addAccessTokenInterceptor(accessTokenProvider) + } + + @JvmStatic + @Provides + @IdentityDatabase + @SessionScope + fun providesIdentityRealmConfiguration(realmKeysUtils: RealmKeysUtils, + @SessionFilesDirectory directory: File, + @UserMd5 userMd5: String): RealmConfiguration { + return RealmConfiguration.Builder() + .directory(directory) + .name("matrix-sdk-identity.realm") + .apply { + realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5)) + } + .modules(IdentityRealmModule()) + .build() + } + } + + @Binds + @AuthenticatedIdentity + abstract fun bindAccessTokenProvider(provider: IdentityAccessTokenProvider): AccessTokenProvider + + @Binds + abstract fun bindIdentityStore(store: RealmIdentityStore): IdentityStore + + @Binds + abstract fun bindIdentityPingTask(task: DefaultIdentityPingTask): IdentityPingTask + + @Binds + abstract fun bindIdentityRegisterTask(task: DefaultIdentityRegisterTask): IdentityRegisterTask + + @Binds + abstract fun bindIdentityRequestTokenForBindingTask(task: DefaultIdentityRequestTokenForBindingTask): IdentityRequestTokenForBindingTask + + @Binds + abstract fun bindIdentitySubmitTokenForBindingTask(task: DefaultIdentitySubmitTokenForBindingTask): IdentitySubmitTokenForBindingTask + + @Binds + abstract fun bindIdentityBulkLookupTask(task: DefaultIdentityBulkLookupTask): IdentityBulkLookupTask + + @Binds + abstract fun bindIdentityDisconnectTask(task: DefaultIdentityDisconnectTask): IdentityDisconnectTask +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityPingTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityPingTask.kt new file mode 100644 index 0000000000..50a36097a1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityPingTask.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity + +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.session.identity.IdentityServiceError +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task +import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection + +internal interface IdentityPingTask : Task { + data class Params( + val identityAuthAPI: IdentityAuthAPI + ) +} + +internal class DefaultIdentityPingTask @Inject constructor() : IdentityPingTask { + + override suspend fun execute(params: IdentityPingTask.Params) { + try { + executeRequest(null) { + apiCall = params.identityAuthAPI.ping() + } + } catch (throwable: Throwable) { + if (throwable is Failure.ServerError && throwable.httpCode == HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) { + // Check if API v1 is available + executeRequest(null) { + apiCall = params.identityAuthAPI.pingV1() + } + // API V1 is responding, but not V2 -> Outdated + throw IdentityServiceError.OutdatedIdentityServer + } else { + throw throwable + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityRegisterTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityRegisterTask.kt new file mode 100644 index 0000000000..c72e364ef8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityRegisterTask.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity + +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.identity.model.IdentityRegisterResponse +import im.vector.matrix.android.internal.session.openid.RequestOpenIdTokenResponse +import im.vector.matrix.android.internal.task.Task +import javax.inject.Inject + +internal interface IdentityRegisterTask : Task { + data class Params( + val identityAuthAPI: IdentityAuthAPI, + val openIdTokenResponse: RequestOpenIdTokenResponse + ) +} + +internal class DefaultIdentityRegisterTask @Inject constructor() : IdentityRegisterTask { + + override suspend fun execute(params: IdentityRegisterTask.Params): IdentityRegisterResponse { + return executeRequest(null) { + apiCall = params.identityAuthAPI.register(params.openIdTokenResponse) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityRequestTokenForBindingTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityRequestTokenForBindingTask.kt new file mode 100644 index 0000000000..313f5f6662 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityRequestTokenForBindingTask.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity + +import im.vector.matrix.android.api.session.identity.IdentityServiceError +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.matrix.android.api.session.identity.getCountryCode +import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.identity.data.IdentityPendingBinding +import im.vector.matrix.android.internal.session.identity.data.IdentityStore +import im.vector.matrix.android.internal.session.identity.model.IdentityRequestTokenForEmailBody +import im.vector.matrix.android.internal.session.identity.model.IdentityRequestTokenForMsisdnBody +import im.vector.matrix.android.internal.session.identity.model.IdentityRequestTokenResponse +import im.vector.matrix.android.internal.task.Task +import java.util.UUID +import javax.inject.Inject + +internal interface IdentityRequestTokenForBindingTask : Task { + data class Params( + val threePid: ThreePid, + // True to request the identity server to send again the email or the SMS + val sendAgain: Boolean + ) +} + +internal class DefaultIdentityRequestTokenForBindingTask @Inject constructor( + private val identityApiProvider: IdentityApiProvider, + private val identityStore: IdentityStore, + @UserId private val userId: String +) : IdentityRequestTokenForBindingTask { + + override suspend fun execute(params: IdentityRequestTokenForBindingTask.Params) { + val identityAPI = getIdentityApiAndEnsureTerms(identityApiProvider, userId) + + val identityPendingBinding = identityStore.getPendingBinding(params.threePid) + + if (params.sendAgain && identityPendingBinding == null) { + throw IdentityServiceError.NoCurrentBindingError + } + + val clientSecret = identityPendingBinding?.clientSecret ?: UUID.randomUUID().toString() + val sendAttempt = identityPendingBinding?.sendAttempt?.inc() ?: 1 + + val tokenResponse = executeRequest(null) { + apiCall = when (params.threePid) { + is ThreePid.Email -> identityAPI.requestTokenToBindEmail(IdentityRequestTokenForEmailBody( + clientSecret = clientSecret, + sendAttempt = sendAttempt, + email = params.threePid.email + )) + is ThreePid.Msisdn -> { + identityAPI.requestTokenToBindMsisdn(IdentityRequestTokenForMsisdnBody( + clientSecret = clientSecret, + sendAttempt = sendAttempt, + phoneNumber = params.threePid.msisdn, + countryCode = params.threePid.getCountryCode() + )) + } + } + } + + // Store client secret, send attempt and sid + identityStore.storePendingBinding( + params.threePid, + IdentityPendingBinding( + clientSecret = clientSecret, + sendAttempt = sendAttempt, + sid = tokenResponse.sid + ) + ) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentitySubmitTokenForBindingTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentitySubmitTokenForBindingTask.kt new file mode 100644 index 0000000000..fae1dd1eba --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentitySubmitTokenForBindingTask.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity + +import im.vector.matrix.android.api.session.identity.IdentityServiceError +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.matrix.android.api.session.identity.toMedium +import im.vector.matrix.android.internal.auth.registration.SuccessResult +import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.identity.data.IdentityStore +import im.vector.matrix.android.internal.session.identity.model.IdentityRequestOwnershipParams +import im.vector.matrix.android.internal.task.Task +import javax.inject.Inject + +internal interface IdentitySubmitTokenForBindingTask : Task { + data class Params( + val threePid: ThreePid, + val token: String + ) +} + +internal class DefaultIdentitySubmitTokenForBindingTask @Inject constructor( + private val identityApiProvider: IdentityApiProvider, + private val identityStore: IdentityStore, + @UserId private val userId: String +) : IdentitySubmitTokenForBindingTask { + + override suspend fun execute(params: IdentitySubmitTokenForBindingTask.Params) { + val identityAPI = getIdentityApiAndEnsureTerms(identityApiProvider, userId) + val identityPendingBinding = identityStore.getPendingBinding(params.threePid) ?: throw IdentityServiceError.NoCurrentBindingError + + val tokenResponse = executeRequest(null) { + apiCall = identityAPI.submitToken( + params.threePid.toMedium(), + IdentityRequestOwnershipParams( + clientSecret = identityPendingBinding.clientSecret, + sid = identityPendingBinding.sid, + token = params.token + )) + } + + if (!tokenResponse.isSuccess()) { + throw IdentityServiceError.BindingError + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityTaskHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityTaskHelper.kt new file mode 100644 index 0000000000..bd97a0af2b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityTaskHelper.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity + +import im.vector.matrix.android.api.session.identity.IdentityServiceError +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.identity.model.IdentityAccountResponse + +internal suspend fun getIdentityApiAndEnsureTerms(identityApiProvider: IdentityApiProvider, userId: String): IdentityAPI { + val identityAPI = identityApiProvider.identityApi ?: throw IdentityServiceError.NoIdentityServerConfigured + + // Always check that we have access to the service (regarding terms) + val identityAccountResponse = executeRequest(null) { + apiCall = identityAPI.getAccount() + } + + assert(userId == identityAccountResponse.userId) + + return identityAPI +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/data/IdentityData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/data/IdentityData.kt new file mode 100644 index 0000000000..f1e57e1ed5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/data/IdentityData.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.data + +internal data class IdentityData( + val identityServerUrl: String?, + val token: String?, + val hashLookupPepper: String?, + val hashLookupAlgorithm: List +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/data/IdentityPendingBinding.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/data/IdentityPendingBinding.kt new file mode 100644 index 0000000000..b7f405cb0a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/data/IdentityPendingBinding.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.data + +internal data class IdentityPendingBinding( + /* Managed by Riot */ + val clientSecret: String, + /* Managed by Riot */ + val sendAttempt: Int, + /* Provided by the identity server */ + val sid: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/data/IdentityStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/data/IdentityStore.kt new file mode 100644 index 0000000000..d5cd3277ec --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/data/IdentityStore.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.data + +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.matrix.android.internal.session.identity.model.IdentityHashDetailResponse + +internal interface IdentityStore { + + fun getIdentityData(): IdentityData? + + fun setUrl(url: String?) + + fun setToken(token: String?) + + fun setHashDetails(hashDetailResponse: IdentityHashDetailResponse) + + /** + * Store details about a current binding + */ + fun storePendingBinding(threePid: ThreePid, data: IdentityPendingBinding) + + fun getPendingBinding(threePid: ThreePid): IdentityPendingBinding? + + fun deletePendingBinding(threePid: ThreePid) +} + +internal fun IdentityStore.getIdentityServerUrlWithoutProtocol(): String? { + return getIdentityData()?.identityServerUrl?.substringAfter("://") +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityDataEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityDataEntity.kt new file mode 100644 index 0000000000..76e480bdc9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityDataEntity.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.db + +import io.realm.RealmList +import io.realm.RealmObject + +internal open class IdentityDataEntity( + var identityServerUrl: String? = null, + var token: String? = null, + var hashLookupPepper: String? = null, + var hashLookupAlgorithm: RealmList = RealmList() +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityDataEntityQuery.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityDataEntityQuery.kt new file mode 100644 index 0000000000..0a07359642 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityDataEntityQuery.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.db + +import io.realm.Realm +import io.realm.RealmList +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +/** + * Only one object can be stored at a time + */ +internal fun IdentityDataEntity.Companion.get(realm: Realm): IdentityDataEntity? { + return realm.where().findFirst() +} + +private fun IdentityDataEntity.Companion.getOrCreate(realm: Realm): IdentityDataEntity { + return get(realm) ?: realm.createObject() +} + +internal fun IdentityDataEntity.Companion.setUrl(realm: Realm, + url: String?) { + realm.where().findAll().deleteAllFromRealm() + // Delete all pending binding if any + IdentityPendingBindingEntity.deleteAll(realm) + + if (url != null) { + getOrCreate(realm).apply { + identityServerUrl = url + } + } +} + +internal fun IdentityDataEntity.Companion.setToken(realm: Realm, + newToken: String?) { + get(realm)?.apply { + token = newToken + } +} + +internal fun IdentityDataEntity.Companion.setHashDetails(realm: Realm, + pepper: String, + algorithms: List) { + get(realm)?.apply { + hashLookupPepper = pepper + hashLookupAlgorithm = RealmList().apply { addAll(algorithms) } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityMapper.kt new file mode 100644 index 0000000000..1335e38565 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityMapper.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.db + +import im.vector.matrix.android.internal.session.identity.data.IdentityData +import im.vector.matrix.android.internal.session.identity.data.IdentityPendingBinding + +internal object IdentityMapper { + + fun map(entity: IdentityDataEntity): IdentityData { + return IdentityData( + identityServerUrl = entity.identityServerUrl, + token = entity.token, + hashLookupPepper = entity.hashLookupPepper, + hashLookupAlgorithm = entity.hashLookupAlgorithm.toList() + ) + } + + fun map(entity: IdentityPendingBindingEntity): IdentityPendingBinding { + return IdentityPendingBinding( + clientSecret = entity.clientSecret, + sendAttempt = entity.sendAttempt, + sid = entity.sid + ) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityPendingBindingEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityPendingBindingEntity.kt new file mode 100644 index 0000000000..51b195a7fc --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityPendingBindingEntity.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.db + +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.matrix.android.api.session.identity.toMedium +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class IdentityPendingBindingEntity( + @PrimaryKey var threePid: String = "", + /* Managed by Riot */ + var clientSecret: String = "", + /* Managed by Riot */ + var sendAttempt: Int = 0, + /* Provided by the identity server */ + var sid: String = "" +) : RealmObject() { + + companion object { + fun ThreePid.toPrimaryKey() = "${toMedium()}_$value" + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityPendingBindingEntityQuery.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityPendingBindingEntityQuery.kt new file mode 100644 index 0000000000..e358be6bbb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityPendingBindingEntityQuery.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.db + +import im.vector.matrix.android.api.session.identity.ThreePid +import io.realm.Realm +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +internal fun IdentityPendingBindingEntity.Companion.get(realm: Realm, threePid: ThreePid): IdentityPendingBindingEntity? { + return realm.where() + .equalTo(IdentityPendingBindingEntityFields.THREE_PID, threePid.toPrimaryKey()) + .findFirst() +} + +internal fun IdentityPendingBindingEntity.Companion.getOrCreate(realm: Realm, threePid: ThreePid): IdentityPendingBindingEntity { + return get(realm, threePid) ?: realm.createObject(threePid.toPrimaryKey()) +} + +internal fun IdentityPendingBindingEntity.Companion.delete(realm: Realm, threePid: ThreePid) { + get(realm, threePid)?.deleteFromRealm() +} + +internal fun IdentityPendingBindingEntity.Companion.deleteAll(realm: Realm) { + realm.where() + .findAll() + .deleteAllFromRealm() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityRealmModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityRealmModule.kt new file mode 100644 index 0000000000..19bd90ee1f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityRealmModule.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.db + +import io.realm.annotations.RealmModule + +/** + * Realm module for identity server classes + */ +@RealmModule(library = true, + classes = [ + IdentityDataEntity::class, + IdentityPendingBindingEntity::class + ]) +internal class IdentityRealmModule diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/RealmIdentityStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/RealmIdentityStore.kt new file mode 100644 index 0000000000..c294fbbf4b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/RealmIdentityStore.kt @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.db + +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.matrix.android.internal.di.IdentityDatabase +import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.session.identity.data.IdentityPendingBinding +import im.vector.matrix.android.internal.session.identity.data.IdentityData +import im.vector.matrix.android.internal.session.identity.data.IdentityStore +import im.vector.matrix.android.internal.session.identity.model.IdentityHashDetailResponse +import io.realm.Realm +import io.realm.RealmConfiguration +import javax.inject.Inject + +@SessionScope +internal class RealmIdentityStore @Inject constructor( + @IdentityDatabase + private val realmConfiguration: RealmConfiguration +) : IdentityStore { + + override fun getIdentityData(): IdentityData? { + return Realm.getInstance(realmConfiguration).use { realm -> + IdentityDataEntity.get(realm)?.let { IdentityMapper.map(it) } + } + } + + override fun setUrl(url: String?) { + Realm.getInstance(realmConfiguration).use { + it.executeTransaction { realm -> + IdentityDataEntity.setUrl(realm, url) + } + } + } + + override fun setToken(token: String?) { + Realm.getInstance(realmConfiguration).use { + it.executeTransaction { realm -> + IdentityDataEntity.setToken(realm, token) + } + } + } + + override fun setHashDetails(hashDetailResponse: IdentityHashDetailResponse) { + Realm.getInstance(realmConfiguration).use { + it.executeTransaction { realm -> + IdentityDataEntity.setHashDetails(realm, hashDetailResponse.pepper, hashDetailResponse.algorithms) + } + } + } + + override fun storePendingBinding(threePid: ThreePid, data: IdentityPendingBinding) { + Realm.getInstance(realmConfiguration).use { + it.executeTransaction { realm -> + IdentityPendingBindingEntity.getOrCreate(realm, threePid).let { entity -> + entity.clientSecret = data.clientSecret + entity.sendAttempt = data.sendAttempt + entity.sid = data.sid + } + } + } + } + + override fun getPendingBinding(threePid: ThreePid): IdentityPendingBinding? { + return Realm.getInstance(realmConfiguration).use { realm -> + IdentityPendingBindingEntity.get(realm, threePid)?.let { IdentityMapper.map(it) } + } + } + + override fun deletePendingBinding(threePid: ThreePid) { + Realm.getInstance(realmConfiguration).use { + it.executeTransaction { realm -> + IdentityPendingBindingEntity.delete(realm, threePid) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityAccountResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityAccountResponse.kt new file mode 100644 index 0000000000..a72eb75537 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityAccountResponse.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class IdentityAccountResponse( + /** + * Required. The user ID which registered the token. + */ + @Json(name = "user_id") + val userId: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityHashDetailResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityHashDetailResponse.kt new file mode 100644 index 0000000000..16a4e1fc71 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityHashDetailResponse.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Ref: https://github.com/matrix-org/matrix-doc/blob/hs/hash-identity/proposals/2134-identity-hash-lookup.md + */ +@JsonClass(generateAdapter = true) +internal data class IdentityHashDetailResponse( + /** + * Required. The pepper the client MUST use in hashing identifiers, and MUST supply to the /lookup endpoint when performing lookups. + * Servers SHOULD rotate this string often. + */ + @Json(name = "lookup_pepper") + val pepper: String, + + /** + * Required. The algorithms the server supports. Must contain at least "sha256". + * "none" can be another possible value. + */ + @Json(name = "algorithms") + val algorithms: List +) { + companion object { + const val ALGORITHM_SHA256 = "sha256" + const val ALGORITHM_NONE = "none" + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityLookUpParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityLookUpParams.kt new file mode 100644 index 0000000000..f87d14e1fe --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityLookUpParams.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Ref: https://github.com/matrix-org/matrix-doc/blob/hs/hash-identity/proposals/2134-identity-hash-lookup.md + */ +@JsonClass(generateAdapter = true) +internal data class IdentityLookUpParams( + /** + * Required. The addresses to look up. The format of the entries here depend on the algorithm used. + * Note that queries which have been incorrectly hashed or formatted will lead to no matches. + */ + @Json(name = "addresses") + val hashedAddresses: List, + + /** + * Required. The algorithm the client is using to encode the addresses. This should be one of the available options from /hash_details. + */ + @Json(name = "algorithm") + val algorithm: String, + + /** + * Required. The pepper from /hash_details. This is required even when the algorithm does not make use of it. + */ + @Json(name = "pepper") + val pepper: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityLookUpResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityLookUpResponse.kt new file mode 100644 index 0000000000..a71e2f7366 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityLookUpResponse.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Ref: https://github.com/matrix-org/matrix-doc/blob/hs/hash-identity/proposals/2134-identity-hash-lookup.md + */ +@JsonClass(generateAdapter = true) +internal data class IdentityLookUpResponse( + /** + * Required. Any applicable mappings of addresses to Matrix User IDs. Addresses which do not have associations will + * not be included, which can make this property be an empty object. + */ + @Json(name = "mappings") + val mappings: Map +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRegisterResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRegisterResponse.kt new file mode 100644 index 0000000000..86999d570d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRegisterResponse.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class IdentityRegisterResponse( + /** + * Required. An opaque string representing the token to authenticate future requests to the identity server with. + */ + @Json(name = "token") + val token: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRequestOwnershipParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRequestOwnershipParams.kt new file mode 100644 index 0000000000..9da86cbc48 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRequestOwnershipParams.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class IdentityRequestOwnershipParams( + /** + * Required. The client secret that was supplied to the requestToken call. + */ + @Json(name = "client_secret") + val clientSecret: String, + + /** + * Required. The session ID, generated by the requestToken call. + */ + @Json(name = "sid") + val sid: String, + + /** + * Required. The token generated by the requestToken call and sent to the user. + */ + @Json(name = "token") + val token: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRequestTokenBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRequestTokenBody.kt new file mode 100644 index 0000000000..3e92ebb1d2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRequestTokenBody.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +// Just to consider common parameters +private interface IdentityRequestTokenBody { + /** + * Required. A unique string generated by the client, and used to identify the validation attempt. + * It must be a string consisting of the characters [0-9a-zA-Z.=_-]. + * Its length must not exceed 255 characters and it must not be empty. + */ + val clientSecret: String + + val sendAttempt: Int +} + +@JsonClass(generateAdapter = true) +internal data class IdentityRequestTokenForEmailBody( + @Json(name = "client_secret") + override val clientSecret: String, + + /** + * Required. The server will only send an email if the send_attempt is a number greater than the most + * recent one which it has seen, scoped to that email + client_secret pair. This is to avoid repeatedly + * sending the same email in the case of request retries between the POSTing user and the identity server. + * The client should increment this value if they desire a new email (e.g. a reminder) to be sent. + * If they do not, the server should respond with success but not resend the email. + */ + @Json(name = "send_attempt") + override val sendAttempt: Int, + + /** + * Required. The email address to validate. + */ + @Json(name = "email") + val email: String +) : IdentityRequestTokenBody + +@JsonClass(generateAdapter = true) +internal data class IdentityRequestTokenForMsisdnBody( + @Json(name = "client_secret") + override val clientSecret: String, + + /** + * Required. The server will only send an SMS if the send_attempt is a number greater than the most recent one + * which it has seen, scoped to that country + phone_number + client_secret triple. This is to avoid repeatedly + * sending the same SMS in the case of request retries between the POSTing user and the identity server. + * The client should increment this value if they desire a new SMS (e.g. a reminder) to be sent. + */ + @Json(name = "send_attempt") + override val sendAttempt: Int, + + /** + * Required. The phone number to validate. + */ + @Json(name = "phone_number") + val phoneNumber: String, + + /** + * Required. The two-letter uppercase ISO-3166-1 alpha-2 country code that the number in phone_number + * should be parsed as if it were dialled from. + */ + @Json(name = "country") + val countryCode: String +) : IdentityRequestTokenBody diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRequestTokenResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRequestTokenResponse.kt new file mode 100644 index 0000000000..cb3c257ddb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRequestTokenResponse.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class IdentityRequestTokenResponse( + /** + * Required. The session ID. Session IDs are opaque strings generated by the identity server. + * They must consist entirely of the characters [0-9a-zA-Z.=_-]. + * Their length must not exceed 255 characters and they must not be empty. + */ + @Json(name = "sid") + val sid: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/AccountDataDataSource.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/AccountDataDataSource.kt new file mode 100644 index 0000000000..37b0da9101 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/AccountDataDataSource.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.identity.todelete + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.util.Optional +import im.vector.matrix.android.api.util.toOptional +import im.vector.matrix.android.internal.database.model.UserAccountDataEntity +import im.vector.matrix.android.internal.database.model.UserAccountDataEntityFields +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent +import io.realm.Realm +import io.realm.RealmQuery +import javax.inject.Inject + +// There will be a duplicated class when Integration manager will be merged, so delete this one +internal class AccountDataDataSource @Inject constructor(private val monarchy: Monarchy, + private val accountDataMapper: AccountDataMapper) { + + fun getAccountDataEvent(type: String): UserAccountDataEvent? { + return getAccountDataEvents(setOf(type)).firstOrNull() + } + + fun getLiveAccountDataEvent(type: String): LiveData> { + return Transformations.map(getLiveAccountDataEvents(setOf(type))) { + it.firstOrNull()?.toOptional() + } + } + + fun getAccountDataEvents(types: Set): List { + return monarchy.fetchAllMappedSync( + { accountDataEventsQuery(it, types) }, + accountDataMapper::map + ) + } + + fun getLiveAccountDataEvents(types: Set): LiveData> { + return monarchy.findAllMappedWithChanges( + { accountDataEventsQuery(it, types) }, + accountDataMapper::map + ) + } + + private fun accountDataEventsQuery(realm: Realm, types: Set): RealmQuery { + val query = realm.where(UserAccountDataEntity::class.java) + if (types.isNotEmpty()) { + query.`in`(UserAccountDataEntityFields.TYPE, types.toTypedArray()) + } + return query + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/AccountDataMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/AccountDataMapper.kt new file mode 100644 index 0000000000..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/integrationmanager/IntegrationManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/integrationmanager/IntegrationManager.kt index f3a06af929..931d1b9bb8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/integrationmanager/IntegrationManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/integrationmanager/IntegrationManager.kt @@ -91,7 +91,7 @@ internal class IntegrationManager @Inject constructor(private val taskExecutor: fun start() { lifecycleRegistry.currentState = Lifecycle.State.STARTED accountDataDataSource - .getLiveAccountDataEvent(UserAccountData.ACCOUNT_DATA_TYPE_ALLOWED_WIDGETS) + .getLiveAccountDataEvent(UserAccountData.TYPE_ALLOWED_WIDGETS) .observeNotNull(lifecycleOwner) { val allowedWidgetsContent = it.getOrNull()?.content?.toModel() if (allowedWidgetsContent != null) { @@ -99,7 +99,7 @@ internal class IntegrationManager @Inject constructor(private val taskExecutor: } } accountDataDataSource - .getLiveAccountDataEvent(UserAccountData.ACCOUNT_DATA_TYPE_INTEGRATION_PROVISIONING) + .getLiveAccountDataEvent(UserAccountData.TYPE_INTEGRATION_PROVISIONING) .observeNotNull(lifecycleOwner) { val integrationProvisioningContent = it.getOrNull()?.content?.toModel() if (integrationProvisioningContent != null) { @@ -142,7 +142,7 @@ internal class IntegrationManager @Inject constructor(private val taskExecutor: * Returns false if the user as disabled integration manager feature */ fun isIntegrationEnabled(): Boolean { - val integrationProvisioningData = accountDataDataSource.getAccountDataEvent(UserAccountData.ACCOUNT_DATA_TYPE_INTEGRATION_PROVISIONING) + val integrationProvisioningData = accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_INTEGRATION_PROVISIONING) val integrationProvisioningContent = integrationProvisioningData?.content?.toModel() return integrationProvisioningContent?.enabled ?: false } @@ -163,7 +163,7 @@ internal class IntegrationManager @Inject constructor(private val taskExecutor: } fun setWidgetAllowed(stateEventId: String, allowed: Boolean, callback: MatrixCallback): Cancelable { - val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.ACCOUNT_DATA_TYPE_ALLOWED_WIDGETS) + val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_ALLOWED_WIDGETS) val currentContent = currentAllowedWidgets?.content?.toModel() val newContent = if (currentContent == null) { val allowedWidget = mapOf(stateEventId to allowed) @@ -183,13 +183,13 @@ internal class IntegrationManager @Inject constructor(private val taskExecutor: } fun isWidgetAllowed(stateEventId: String): Boolean { - val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.ACCOUNT_DATA_TYPE_ALLOWED_WIDGETS) + val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_ALLOWED_WIDGETS) val currentContent = currentAllowedWidgets?.content?.toModel() return currentContent?.widgets?.get(stateEventId) ?: false } fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean, callback: MatrixCallback): Cancelable { - val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.ACCOUNT_DATA_TYPE_ALLOWED_WIDGETS) + val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_ALLOWED_WIDGETS) val currentContent = currentAllowedWidgets?.content?.toModel() val newContent = if (currentContent == null) { val nativeAllowedWidgets = mapOf(widgetType to mapOf(domain to allowed)) @@ -213,7 +213,7 @@ internal class IntegrationManager @Inject constructor(private val taskExecutor: } fun isNativeWidgetAllowed(widgetType: String, domain: String?): Boolean { - val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.ACCOUNT_DATA_TYPE_ALLOWED_WIDGETS) + val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_ALLOWED_WIDGETS) val currentContent = currentAllowedWidgets?.content?.toModel() return currentContent?.native?.get(widgetType)?.get(domain) ?: false } 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/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index f2bee734ce..bf6b81b57c 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 @@ -35,7 +35,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 @@ -724,8 +724,11 @@ internal class DefaultTimeline( `in`(TimelineEventEntityFields.ROOT.TYPE, settings.allowedTypes.toTypedArray()) } if (settings.filterEdits) { - not().like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.EDIT_TYPE) - not().like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.RESPONSE_TYPE) + not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT) + not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE) + } + if (settings.filterRedacted) { + not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED) } return this } @@ -737,13 +740,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 43a5d8a5cb..d758110e09 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,9 +30,9 @@ 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 ACCOUNT_DATA_TYPE_INTEGRATION_PROVISIONING = "im.vector.setting.integration_provisioning" - const val ACCOUNT_DATA_TYPE_ALLOWED_WIDGETS = "im.vector.setting.allowed_widgets" - + const val TYPE_INTEGRATION_PROVISIONING = "im.vector.setting.integration_provisioning" + const val TYPE_ALLOWED_WIDGETS = "im.vector.setting.allowed_widgets" + 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/UserAccountDataAllowedWidgets.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataAllowedWidgets.kt index ae6ca686ed..43f3b3b32a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataAllowedWidgets.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataAllowedWidgets.kt @@ -22,6 +22,6 @@ import im.vector.matrix.android.internal.session.integrationmanager.AllowedWidge @JsonClass(generateAdapter = true) internal data class UserAccountDataAllowedWidgets( - @Json(name = "type") override val type: String = ACCOUNT_DATA_TYPE_ALLOWED_WIDGETS, + @Json(name = "type") override val type: String = TYPE_ALLOWED_WIDGETS, @Json(name = "content") val content: AllowedWidgetsContent ) : UserAccountData() 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/sync/model/accountdata/UserAccountDataIntegrationProvisioning.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataIntegrationProvisioning.kt index c65bbd82c1..a47bb761cd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataIntegrationProvisioning.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataIntegrationProvisioning.kt @@ -22,6 +22,6 @@ import im.vector.matrix.android.internal.session.integrationmanager.IntegrationP @JsonClass(generateAdapter = true) internal data class UserAccountDataIntegrationProvisioning( - @Json(name = "type") override val type: String = ACCOUNT_DATA_TYPE_INTEGRATION_PROVISIONING, + @Json(name = "type") override val type: String = TYPE_INTEGRATION_PROVISIONING, @Json(name = "content") val content: IntegrationProvisioningContent ) : UserAccountData() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/AcceptTermsBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/AcceptTermsBody.kt new file mode 100644 index 0000000000..c5827b822f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/AcceptTermsBody.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.terms + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class represent a list of urls of terms the user wants to accept + */ +@JsonClass(generateAdapter = true) +internal data class AcceptTermsBody( + @Json(name = "user_accepts") + val acceptedTermUrls: List +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/DefaultTermsService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/DefaultTermsService.kt new file mode 100644 index 0000000000..6d5e597da8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/DefaultTermsService.kt @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.terms + +import dagger.Lazy +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.terms.GetTermsResponse +import im.vector.matrix.android.api.session.terms.TermsService +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.network.NetworkConstants +import im.vector.matrix.android.internal.network.RetrofitFactory +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.identity.IdentityAuthAPI +import im.vector.matrix.android.internal.session.identity.IdentityRegisterTask +import im.vector.matrix.android.internal.session.identity.todelete.AccountDataDataSource +import im.vector.matrix.android.internal.session.openid.GetOpenIdTokenTask +import im.vector.matrix.android.internal.session.sync.model.accountdata.AcceptedTermsContent +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData +import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.launchToCallback +import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers +import okhttp3.OkHttpClient +import javax.inject.Inject + +internal class DefaultTermsService @Inject constructor( + @Unauthenticated + private val unauthenticatedOkHttpClient: Lazy, + private val accountDataDataSource: AccountDataDataSource, + private val termsAPI: TermsAPI, + private val retrofitFactory: RetrofitFactory, + private val getOpenIdTokenTask: GetOpenIdTokenTask, + private val identityRegisterTask: IdentityRegisterTask, + private val updateUserAccountDataTask: UpdateUserAccountDataTask, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val taskExecutor: TaskExecutor +) : TermsService { + override fun getTerms(serviceType: TermsService.ServiceType, + baseUrl: String, + callback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + val sep = if (baseUrl.endsWith("/")) "" else "/" + + val url = when (serviceType) { + TermsService.ServiceType.IntegrationManager -> "$baseUrl$sep${NetworkConstants.URI_INTEGRATION_MANAGER_PATH}" + TermsService.ServiceType.IdentityService -> "$baseUrl$sep${NetworkConstants.URI_IDENTITY_PATH_V2}" + } + + val termsResponse = executeRequest(null) { + apiCall = termsAPI.getTerms("${url}terms") + } + + GetTermsResponse(termsResponse, getAlreadyAcceptedTermUrlsFromAccountData()) + } + } + + override fun agreeToTerms(serviceType: TermsService.ServiceType, + baseUrl: String, + agreedUrls: List, + token: String?, + callback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + val sep = if (baseUrl.endsWith("/")) "" else "/" + + val url = when (serviceType) { + TermsService.ServiceType.IntegrationManager -> "$baseUrl$sep${NetworkConstants.URI_INTEGRATION_MANAGER_PATH}" + TermsService.ServiceType.IdentityService -> "$baseUrl$sep${NetworkConstants.URI_IDENTITY_PATH_V2}" + } + + val tokenToUse = token?.takeIf { it.isNotEmpty() } ?: getToken(baseUrl) + + executeRequest(null) { + apiCall = termsAPI.agreeToTerms("${url}terms", AcceptTermsBody(agreedUrls), "Bearer $tokenToUse") + } + + // client SHOULD update this account data section adding any the URLs + // of any additional documents that the user agreed to this list. + // Get current m.accepted_terms append new ones and update account data + val listOfAcceptedTerms = getAlreadyAcceptedTermUrlsFromAccountData() + + val newList = listOfAcceptedTerms.toMutableSet().apply { addAll(agreedUrls) }.toList() + + updateUserAccountDataTask.execute(UpdateUserAccountDataTask.AcceptedTermsParams( + acceptedTermsContent = AcceptedTermsContent(newList) + )) + } + } + + private suspend fun getToken(url: String): String { + // TODO This is duplicated code see DefaultIdentityService + val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java) + + val openIdToken = getOpenIdTokenTask.execute(Unit) + val token = identityRegisterTask.execute(IdentityRegisterTask.Params(api, openIdToken)) + + return token.token + } + + private fun getAlreadyAcceptedTermUrlsFromAccountData(): Set { + return accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_ACCEPTED_TERMS) + ?.content + ?.toModel() + ?.acceptedTerms + ?.toSet() + .orEmpty() + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/identity/IdentityPingApi.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsAPI.kt similarity index 53% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/identity/IdentityPingApi.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsAPI.kt index 2a0e00704c..03b745f8d7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/identity/IdentityPingApi.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsAPI.kt @@ -13,22 +13,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package im.vector.matrix.android.internal.identity -import im.vector.matrix.android.internal.network.NetworkConstants +package im.vector.matrix.android.internal.session.terms + +import im.vector.matrix.android.internal.network.HttpHeaders import retrofit2.Call +import retrofit2.http.Body import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.POST +import retrofit2.http.Url -internal interface IdentityPingApi { +internal interface TermsAPI { + /** + * This request does not require authentication + */ + @GET + fun getTerms(@Url url: String): Call /** - * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery - * Simple ping call to check if server alive - * - * Ref: https://matrix.org/docs/spec/identity_service/unstable#status-check - * - * @return 200 in case of success + * This request requires authentication */ - @GET(NetworkConstants.URI_API_PREFIX_IDENTITY) - fun ping(): Call + @POST + fun agreeToTerms(@Url url: String, @Body params: AcceptTermsBody, @Header(HttpHeaders.Authorization) token: String): Call } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsModule.kt new file mode 100644 index 0000000000..eee7e22134 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsModule.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.terms + +import dagger.Binds +import dagger.Lazy +import dagger.Module +import dagger.Provides +import im.vector.matrix.android.api.session.terms.TermsService +import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.network.RetrofitFactory +import im.vector.matrix.android.internal.session.SessionScope +import okhttp3.OkHttpClient + +@Module +internal abstract class TermsModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesTermsAPI(@Unauthenticated unauthenticatedOkHttpClient: Lazy, + retrofitFactory: RetrofitFactory): TermsAPI { + val retrofit = retrofitFactory.create(unauthenticatedOkHttpClient, "https://foo.bar") + return retrofit.create(TermsAPI::class.java) + } + } + + @Binds + abstract fun bindTermsService(service: DefaultTermsService): TermsService +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsResponse.kt new file mode 100644 index 0000000000..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/accountdata/UpdateUserAccountDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt index 22ab61c93a..8db2ad9781 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 @@ -20,7 +20,9 @@ import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.integrationmanager.AllowedWidgetsContent import im.vector.matrix.android.internal.session.integrationmanager.IntegrationProvisioningContent +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 @@ -33,6 +35,24 @@ internal interface UpdateUserAccountDataTask : Task> @@ -52,7 +72,7 @@ internal interface UpdateUserAccountDataTask : Task this + !startsWith("http") -> "https://$this" + else -> this + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/GetWellknownTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/GetWellknownTask.kt similarity index 96% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/GetWellknownTask.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/GetWellknownTask.kt index 8ed2cb3b0f..c6f6b8752d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/GetWellknownTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/GetWellknownTask.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.auth.wellknown +package im.vector.matrix.android.internal.wellknown import android.util.MalformedJsonException import dagger.Lazy @@ -23,10 +23,10 @@ import im.vector.matrix.android.api.auth.data.WellKnown import im.vector.matrix.android.api.auth.wellknown.WellknownResult import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.internal.di.Unauthenticated -import im.vector.matrix.android.internal.identity.IdentityPingApi import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.homeserver.CapabilitiesAPI +import im.vector.matrix.android.internal.session.identity.IdentityAuthAPI import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.isValidUrl import okhttp3.OkHttpClient @@ -119,7 +119,7 @@ internal class DefaultGetWellknownTask @Inject constructor( try { executeRequest(null) { - apiCall = capabilitiesAPI.getVersions() + apiCall = capabilitiesAPI.ping() } } catch (throwable: Throwable) { return WellknownResult.FailError @@ -153,7 +153,7 @@ internal class DefaultGetWellknownTask @Inject constructor( */ private suspend fun validateIdentityServer(identityServerBaseUrl: String): Boolean { val identityPingApi = retrofitFactory.create(okHttpClient, identityServerBaseUrl) - .create(IdentityPingApi::class.java) + .create(IdentityAuthAPI::class.java) return try { executeRequest(null) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/WellKnownAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/WellKnownAPI.kt similarity index 94% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/WellKnownAPI.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/WellKnownAPI.kt index 71928123bf..ec62707db7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/WellKnownAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/WellKnownAPI.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package im.vector.matrix.android.internal.auth.wellknown +package im.vector.matrix.android.internal.wellknown import im.vector.matrix.android.api.auth.data.WellKnown import retrofit2.Call diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/WellknownModule.kt similarity index 60% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/WellknownModule.kt index 6e89a28b7d..2705803fec 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/WellknownModule.kt @@ -1,11 +1,11 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright (c) 2020 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,10 +14,14 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.database.query +package im.vector.matrix.android.internal.wellknown -internal object FilterContent { +import dagger.Binds +import dagger.Module - internal const val EDIT_TYPE = """{*"m.relates_to"*"rel_type":*"m.replace"*}""" - internal const val RESPONSE_TYPE = """{*"m.relates_to"*"rel_type":*"m.response"*}""" +@Module +internal abstract class WellknownModule { + + @Binds + abstract fun bindGetWellknownTask(task: DefaultGetWellknownTask): GetWellknownTask } diff --git a/matrix-sdk-android/src/main/res/values-de/strings.xml b/matrix-sdk-android/src/main/res/values-de/strings.xml index 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/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 ae0ffa1f91..7c2939707f 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -161,6 +161,8 @@ android:name=".features.attachments.preview.AttachmentsPreviewActivity" android:theme="@style/AppTheme.AttachmentsPreview" /> + + 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/SimpleFragmentActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt index e8e8f21259..58ec4b22c6 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt @@ -26,7 +26,6 @@ import im.vector.matrix.android.api.session.Session import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.hideKeyboard -import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.activity.* import javax.inject.Inject @@ -108,15 +107,4 @@ abstract class SimpleFragmentActivity : VectorBaseActivity() { } super.onBackPressed() } - - protected fun VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) { - viewEvents - .observe() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - hideWaitingView() - observer(it) - } - .disposeOnDestroy() - } } diff --git a/vector/src/main/java/im/vector/riotx/core/platform/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/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/settings/KeysBackupSettingsRecyclerViewController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt index 1fec404f7d..20046fa115 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt @@ -197,7 +197,7 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(private val s endIconResourceId(R.drawable.e2e_warning) } else { if (isSignatureValid) { - if (session.sessionParams.credentials.deviceId == it.deviceId) { + if (session.sessionParams.deviceId == it.deviceId) { description(stringProvider.getString(R.string.keys_backup_settings_valid_signature_from_this_device)) endIconResourceId(R.drawable.e2e_verified) } else { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt index 3522c5a752..93d6f43763 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt @@ -164,16 +164,16 @@ class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment() @OnClick(R.id.keys_backup_setup_step2_button) fun doNext() { when { - viewModel.passphrase.value.isNullOrEmpty() -> { + viewModel.passphrase.value.isNullOrEmpty() -> { viewModel.passphraseError.value = context?.getString(R.string.passphrase_empty_error_message) } viewModel.passphrase.value != viewModel.confirmPassphrase.value -> { viewModel.confirmPassphraseError.value = context?.getString(R.string.passphrase_passphrase_does_not_match) } - viewModel.passwordStrength.value?.score ?: 0 < 4 -> { + viewModel.passwordStrength.value?.score ?: 0 < 4 -> { viewModel.passphraseError.value = context?.getString(R.string.passphrase_passphrase_too_weak) } - else -> { + else -> { viewModel.megolmBackupCreationInfo = null viewModel.prepareRecoveryKey(activity!!, viewModel.passphrase.value) @@ -190,7 +190,7 @@ class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment() viewModel.prepareRecoveryKey(activity!!, null) } - else -> { + else -> { // User has entered a passphrase but want to skip this step. viewModel.passphraseError.value = context?.getString(R.string.keys_backup_passphrase_not_empty_error_message) } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt index 9175d6c081..1478b99d3b 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt @@ -123,7 +123,7 @@ class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment() .joinToString(" ") } - it.setOnClickListener { + it.debouncedClicks { copyToClipboard(activity!!, recoveryKey) } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStorageKeyFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStorageKeyFragment.kt index db4c5230fd..848166381e 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStorageKeyFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStorageKeyFragment.kt @@ -23,7 +23,6 @@ import android.view.View import android.view.inputmethod.EditorInfo import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.view.clicks import com.jakewharton.rxbinding3.widget.editorActionEvents import com.jakewharton.rxbinding3.widget.textChanges import im.vector.matrix.android.api.extensions.tryThis @@ -33,7 +32,6 @@ import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.utils.startImportTextFromFileIntent import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.fragment_ssss_access_from_key.* -import kotlinx.android.synthetic.main.fragment_ssss_access_from_passphrase.* import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -67,13 +65,7 @@ class SharedSecuredStorageKeyFragment @Inject constructor( } .disposeOnDestroyView() - ssss_key_use_file.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - startImportTextFromFileIntent(this, IMPORT_FILE_REQ) - } - .disposeOnDestroyView() + ssss_key_use_file.debouncedClicks { startImportTextFromFileIntent(this, IMPORT_FILE_REQ) } sharedViewModel.observeViewEvents { when (it) { @@ -83,13 +75,7 @@ class SharedSecuredStorageKeyFragment @Inject constructor( } } - ssss_key_submit.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - submit() - } - .disposeOnDestroyView() + ssss_key_submit.debouncedClicks { submit() } } fun submit() { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt index 82d27aea1b..f5eb450fe1 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt @@ -22,7 +22,6 @@ import android.view.inputmethod.EditorInfo import androidx.core.text.toSpannable import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.view.clicks import com.jakewharton.rxbinding3.widget.editorActionEvents import com.jakewharton.rxbinding3.widget.textChanges import im.vector.riotx.R @@ -37,7 +36,7 @@ import javax.inject.Inject class SharedSecuredStoragePassphraseFragment @Inject constructor( private val colorProvider: ColorProvider -): VectorBaseFragment() { +) : VectorBaseFragment() { override fun getLayoutResId() = R.layout.fragment_ssss_access_from_passphrase @@ -83,29 +82,9 @@ class SharedSecuredStoragePassphraseFragment @Inject constructor( } } - ssss_passphrase_submit.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - submit() - } - .disposeOnDestroyView() - - ssss_passphrase_use_key.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - sharedViewModel.handle(SharedSecureStorageAction.UseKey) - } - .disposeOnDestroyView() - - ssss_view_show_password.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - sharedViewModel.handle(SharedSecureStorageAction.TogglePasswordVisibility) - } - .disposeOnDestroyView() + ssss_passphrase_submit.debouncedClicks { submit() } + ssss_passphrase_use_key.debouncedClicks { sharedViewModel.handle(SharedSecureStorageAction.UseKey) } + ssss_view_show_password.debouncedClicks { sharedViewModel.handle(SharedSecureStorageAction.TogglePasswordVisibility) } } fun submit() { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapAccountPasswordFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapAccountPasswordFragment.kt index abe6e54092..fcedd2926e 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapAccountPasswordFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapAccountPasswordFragment.kt @@ -22,7 +22,6 @@ import android.view.inputmethod.EditorInfo import androidx.core.text.toSpannable import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.view.clicks import com.jakewharton.rxbinding3.widget.editorActionEvents import com.jakewharton.rxbinding3.widget.textChanges import im.vector.riotx.R @@ -75,21 +74,8 @@ class BootstrapAccountPasswordFragment @Inject constructor( } .disposeOnDestroyView() - ssss_view_show_password.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) - } - .disposeOnDestroyView() - - bootstrapPasswordButton.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - submit() - } - .disposeOnDestroyView() + ssss_view_show_password.debouncedClicks { sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) } + bootstrapPasswordButton.debouncedClicks { submit() } withState(sharedViewModel) { state -> (state.step as? BootstrapStep.AccountPassword)?.failure?.let { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConclusionFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConclusionFragment.kt index d84283b14c..fd7ba2dbb7 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConclusionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConclusionFragment.kt @@ -21,14 +21,11 @@ import android.view.View import androidx.core.text.toSpannable import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.view.clicks import im.vector.riotx.R import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.utils.colorizeMatchingText -import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.fragment_bootstrap_conclusion.* -import java.util.concurrent.TimeUnit import javax.inject.Inject class BootstrapConclusionFragment @Inject constructor( @@ -42,13 +39,7 @@ class BootstrapConclusionFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - bootstrapConclusionContinue.clickableView.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - sharedViewModel.handle(BootstrapActions.Completed) - } - .disposeOnDestroyView() + bootstrapConclusionContinue.clickableView.debouncedClicks { sharedViewModel.handle(BootstrapActions.Completed) } } override fun invalidate() = withState(sharedViewModel) { state -> diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt index d09eafee58..df4d741bf1 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt @@ -23,7 +23,6 @@ import androidx.core.text.toSpannable import androidx.core.view.isGone import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.view.clicks import com.jakewharton.rxbinding3.widget.editorActionEvents import com.jakewharton.rxbinding3.widget.textChanges import im.vector.riotx.R @@ -88,21 +87,8 @@ class BootstrapConfirmPassphraseFragment @Inject constructor( // } } - ssss_view_show_password.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) - } - .disposeOnDestroyView() - - bootstrapSubmit.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - submit() - } - .disposeOnDestroyView() + ssss_view_show_password.debouncedClicks { sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) } + bootstrapSubmit.debouncedClicks { submit() } } private fun submit() = withState(sharedViewModel) { state -> diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapEnterPassphraseFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapEnterPassphraseFragment.kt index 952b0e5d03..d1eee9ff3f 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapEnterPassphraseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapEnterPassphraseFragment.kt @@ -22,7 +22,6 @@ import android.view.inputmethod.EditorInfo import androidx.core.text.toSpannable import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.view.clicks import com.jakewharton.rxbinding3.widget.editorActionEvents import com.jakewharton.rxbinding3.widget.textChanges import im.vector.riotx.R @@ -83,21 +82,8 @@ class BootstrapEnterPassphraseFragment @Inject constructor( // } } - ssss_view_show_password.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) - } - .disposeOnDestroyView() - - bootstrapSubmit.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - submit() - } - .disposeOnDestroyView() + ssss_view_show_password.debouncedClicks { sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) } + bootstrapSubmit.debouncedClicks { submit() } } private fun submit() = withState(sharedViewModel) { state -> diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapMigrateBackupFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapMigrateBackupFragment.kt index f1847e5ab5..caf43721a0 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapMigrateBackupFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapMigrateBackupFragment.kt @@ -28,7 +28,6 @@ import androidx.core.text.toSpannable import androidx.core.view.isVisible import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.view.clicks import com.jakewharton.rxbinding3.widget.editorActionEvents import com.jakewharton.rxbinding3.widget.textChanges import im.vector.matrix.android.api.extensions.tryThis @@ -80,37 +79,10 @@ class BootstrapMigrateBackupFragment @Inject constructor( .disposeOnDestroyView() // sharedViewModel.observeViewEvents {} - bootstrapMigrateContinueButton.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - submit() - } - .disposeOnDestroyView() - - bootstrapMigrateShowPassword.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) - } - .disposeOnDestroyView() - - bootstrapMigrateForgotPassphrase.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - sharedViewModel.handle(BootstrapActions.HandleForgotBackupPassphrase) - } - .disposeOnDestroyView() - - bootstrapMigrateUseFile.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - startImportTextFromFileIntent(this, IMPORT_FILE_REQ) - } - .disposeOnDestroyView() + bootstrapMigrateContinueButton.debouncedClicks { submit() } + bootstrapMigrateShowPassword.debouncedClicks { sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) } + bootstrapMigrateForgotPassphrase.debouncedClicks { sharedViewModel.handle(BootstrapActions.HandleForgotBackupPassphrase) } + bootstrapMigrateUseFile.debouncedClicks { startImportTextFromFileIntent(this, IMPORT_FILE_REQ) } } private fun submit() = withState(sharedViewModel) { state -> diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt index 05c6f7a53f..4faa4168b0 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt @@ -25,19 +25,16 @@ import androidx.core.text.toSpannable import androidx.core.view.isVisible import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.view.clicks import im.vector.riotx.R import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.utils.colorizeMatchingText import im.vector.riotx.core.utils.startSharePlainTextIntent import im.vector.riotx.core.utils.toast -import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.fragment_bootstrap_save_key.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -import java.util.concurrent.TimeUnit import javax.inject.Inject class BootstrapSaveRecoveryKeyFragment @Inject constructor( @@ -51,34 +48,17 @@ class BootstrapSaveRecoveryKeyFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - bootstrapSaveText.text = getString(R.string.bootstrap_save_key_description, getString(R.string.message_key), getString(R.string.recovery_passphrase)) + val messageKey = getString(R.string.message_key) + val recoveryPassphrase = getString(R.string.recovery_passphrase) + val color = colorProvider.getColorFromAttribute(R.attr.vctr_toolbar_link_text_color) + bootstrapSaveText.text = getString(R.string.bootstrap_save_key_description, messageKey, recoveryPassphrase) .toSpannable() - .colorizeMatchingText(getString(R.string.recovery_passphrase), colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) - .colorizeMatchingText(getString(R.string.message_key), colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) + .colorizeMatchingText(messageKey, color) + .colorizeMatchingText(recoveryPassphrase, color) - recoverySave.clickableView.clicks() - .debounce(600, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - downloadRecoveryKey() - } - .disposeOnDestroyView() - - recoveryCopy.clickableView.clicks() - .debounce(600, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - shareRecoveryKey() - } - .disposeOnDestroyView() - - recoveryContinue.clickableView.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - sharedViewModel.handle(BootstrapActions.GoToCompleted) - } - .disposeOnDestroyView() + recoverySave.clickableView.debouncedClicks { downloadRecoveryKey() } + recoveryCopy.clickableView.debouncedClicks { shareRecoveryKey() } + recoveryContinue.clickableView.debouncedClicks { sharedViewModel.handle(BootstrapActions.GoToCompleted) } } private fun downloadRecoveryKey() = withState(sharedViewModel) { _ -> diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt index 7a003c3722..dce33255ce 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt @@ -367,7 +367,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( ) if (trustResult.isVerified()) { // Sign this device and upload the signature - session.sessionParams.credentials.deviceId?.let { deviceId -> + session.sessionParams.deviceId?.let { deviceId -> session.cryptoService() .crossSigningService().trustDevice(deviceId, object : MatrixCallback { override fun onFailure(failure: Throwable) { diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsAction.kt b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsAction.kt new file mode 100644 index 0000000000..57b23d26d2 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsAction.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.discovery + +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.riotx.core.platform.VectorViewModelAction + +sealed class DiscoverySettingsAction : VectorViewModelAction { + object RetrieveBinding : DiscoverySettingsAction() + object Refresh : DiscoverySettingsAction() + + object DisconnectIdentityServer : DiscoverySettingsAction() + data class ChangeIdentityServer(val url: String) : DiscoverySettingsAction() + data class RevokeThreePid(val threePid: ThreePid) : DiscoverySettingsAction() + data class ShareThreePid(val threePid: ThreePid) : DiscoverySettingsAction() + data class FinalizeBind3pid(val threePid: ThreePid) : DiscoverySettingsAction() + data class SubmitMsisdnToken(val threePid: ThreePid.Msisdn, val code: String) : DiscoverySettingsAction() + data class CancelBinding(val threePid: ThreePid) : DiscoverySettingsAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsController.kt b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsController.kt new file mode 100644 index 0000000000..f92cb1a8bb --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsController.kt @@ -0,0 +1,377 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotx.features.discovery + +import android.view.View +import com.airbnb.epoxy.TypedEpoxyController +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Incomplete +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import com.google.i18n.phonenumbers.PhoneNumberUtil +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.session.identity.SharedState +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.attributes.ButtonStyle +import im.vector.riotx.core.epoxy.attributes.ButtonType +import im.vector.riotx.core.epoxy.attributes.IconMode +import im.vector.riotx.core.epoxy.loadingItem +import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.resources.StringProvider +import timber.log.Timber +import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection + +class DiscoverySettingsController @Inject constructor( + private val colorProvider: ColorProvider, + private val stringProvider: StringProvider, + private val errorFormatter: ErrorFormatter +) : TypedEpoxyController() { + + var listener: Listener? = null + + private val codes = mutableMapOf() + + override fun buildModels(data: DiscoverySettingsState) { + when (data.identityServer) { + is Loading -> { + loadingItem { + id("identityServerLoading") + } + } + is Fail -> { + settingsInfoItem { + id("identityServerError") + helperText(data.identityServer.error.message) + } + } + is Success -> { + buildIdentityServerSection(data) + val hasIdentityServer = data.identityServer().isNullOrBlank().not() + if (hasIdentityServer && !data.termsNotSigned) { + buildEmailsSection(data.emailList) + buildMsisdnSection(data.phoneNumbersList) + } + } + } + } + + private fun buildIdentityServerSection(data: DiscoverySettingsState) { + val identityServer = data.identityServer() ?: stringProvider.getString(R.string.none) + + settingsSectionTitleItem { + id("idServerTitle") + titleResId(R.string.identity_server) + } + + settingsItem { + id("idServer") + title(identityServer) + } + + if (data.identityServer() != null && data.termsNotSigned) { + settingsInfoItem { + id("idServerFooter") + helperText(stringProvider.getString(R.string.settings_agree_to_terms, identityServer)) + showCompoundDrawable(true) + itemClickListener(View.OnClickListener { listener?.openIdentityServerTerms() }) + } + settingsButtonItem { + id("seeTerms") + colorProvider(colorProvider) + buttonTitle(stringProvider.getString(R.string.open_terms_of, identityServer)) + buttonClickListener { listener?.openIdentityServerTerms() } + } + } else { + settingsInfoItem { + id("idServerFooter") + showCompoundDrawable(false) + if (data.identityServer() != null) { + helperText(stringProvider.getString(R.string.settings_discovery_identity_server_info, identityServer)) + } else { + helperTextResId(R.string.settings_discovery_identity_server_info_none) + } + } + } + + settingsButtonItem { + id("change") + colorProvider(colorProvider) + if (data.identityServer() == null) { + buttonTitleId(R.string.add_identity_server) + } else { + buttonTitleId(R.string.change_identity_server) + } + buttonClickListener { listener?.onTapChangeIdentityServer() } + } + + if (data.identityServer() != null) { + settingsInfoItem { + id("removeInfo") + helperTextResId(R.string.settings_discovery_disconnect_identity_server_info) + } + settingsButtonItem { + id("remove") + colorProvider(colorProvider) + buttonTitleId(R.string.disconnect_identity_server) + buttonStyle(ButtonStyle.DESTRUCTIVE) + buttonClickListener { listener?.onTapDisconnectIdentityServer() } + } + } + } + + private fun buildEmailsSection(emails: Async>) { + settingsSectionTitleItem { + id("emails") + titleResId(R.string.settings_discovery_emails_title) + } + when (emails) { + is Incomplete -> { + loadingItem { + id("emailsLoading") + } + } + is Fail -> { + settingsInfoItem { + id("emailsError") + helperText(emails.error.message) + } + } + is Success -> { + if (emails().isEmpty()) { + settingsInfoItem { + id("emailsEmpty") + helperText(stringProvider.getString(R.string.settings_discovery_no_mails)) + } + } else { + emails().forEach { buildEmail(it) } + } + } + } + } + + private fun buildEmail(pidInfo: PidInfo) { + buildThreePid(pidInfo) + + if (pidInfo.isShared is Fail) { + buildSharedFail(pidInfo) + } else if (pidInfo.isShared() == SharedState.BINDING_IN_PROGRESS) { + when (pidInfo.finalRequest) { + is Uninitialized, + is Loading -> + settingsInformationItem { + id("info${pidInfo.threePid.value}") + colorProvider(colorProvider) + message(stringProvider.getString(R.string.settings_discovery_confirm_mail, pidInfo.threePid.value)) + } + is Fail -> + settingsInformationItem { + id("info${pidInfo.threePid.value}") + colorProvider(colorProvider) + message(stringProvider.getString(R.string.settings_discovery_confirm_mail_not_clicked, pidInfo.threePid.value)) + textColorId(R.color.riotx_destructive_accent) + } + is Success -> Unit /* Cannot happen */ + } + when (pidInfo.finalRequest) { + is Uninitialized, + is Fail -> + buildContinueCancel(pidInfo.threePid) + is Loading -> + settingsProgressItem { + id("progress${pidInfo.threePid.value}") + } + is Success -> Unit /* Cannot happen */ + } + } + } + + private fun buildMsisdnSection(msisdns: Async>) { + settingsSectionTitleItem { + id("msisdn") + titleResId(R.string.settings_discovery_msisdn_title) + } + + when (msisdns) { + is Incomplete -> { + loadingItem { + id("msisdnLoading") + } + } + is Fail -> { + settingsInfoItem { + id("msisdnListError") + helperText(msisdns.error.message) + } + } + is Success -> { + if (msisdns().isEmpty()) { + settingsInfoItem { + id("no_msisdn") + helperText(stringProvider.getString(R.string.settings_discovery_no_msisdn)) + } + } else { + msisdns().forEach { buildMsisdn(it) } + } + } + } + } + + private fun buildMsisdn(pidInfo: PidInfo) { + val phoneNumber = try { + PhoneNumberUtil.getInstance().parse("+${pidInfo.threePid.value}", null) + } catch (t: Throwable) { + Timber.e(t, "Unable to parse the phone number") + null + } + ?.let { + PhoneNumberUtil.getInstance().format(it, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL) + } + ?: pidInfo.threePid.value + + buildThreePid(pidInfo, phoneNumber) + + if (pidInfo.isShared is Fail) { + buildSharedFail(pidInfo) + } else if (pidInfo.isShared() == SharedState.BINDING_IN_PROGRESS) { + val errorText = if (pidInfo.finalRequest is Fail) { + val error = pidInfo.finalRequest.error + // Deal with error 500 + // Ref: https://github.com/matrix-org/sydent/issues/292 + if (error is Failure.ServerError + && error.httpCode == HttpsURLConnection.HTTP_INTERNAL_ERROR /* 500 */) { + stringProvider.getString(R.string.settings_text_message_sent_wrong_code) + } else { + errorFormatter.toHumanReadable(error) + } + } else { + null + } + settingsEditTextItem { + id("msisdnVerification${pidInfo.threePid.value}") + descriptionText(stringProvider.getString(R.string.settings_text_message_sent, phoneNumber)) + errorText(errorText) + inProgress(pidInfo.finalRequest is Loading) + interactionListener(object : SettingsEditTextItem.Listener { + override fun onValidate() { + val code = codes[pidInfo.threePid] + if (pidInfo.threePid is ThreePid.Msisdn && code != null) { + listener?.sendMsisdnVerificationCode(pidInfo.threePid, code) + } + } + + override fun onCodeChange(code: String) { + codes[pidInfo.threePid] = code + } + }) + } + buildContinueCancel(pidInfo.threePid) + } + } + + private fun buildThreePid(pidInfo: PidInfo, title: String = pidInfo.threePid.value) { + settingsTextButtonSingleLineItem { + id(pidInfo.threePid.value) + title(title) + colorProvider(colorProvider) + stringProvider(stringProvider) + when (pidInfo.isShared) { + is Loading -> { + buttonIndeterminate(true) + } + is Fail -> { + buttonType(ButtonType.NORMAL) + buttonStyle(ButtonStyle.DESTRUCTIVE) + buttonTitle(stringProvider.getString(R.string.global_retry)) + iconMode(IconMode.ERROR) + buttonClickListener { listener?.onTapRetryToRetrieveBindings() } + } + is Success -> when (pidInfo.isShared()) { + SharedState.SHARED, + SharedState.NOT_SHARED -> { + buttonType(ButtonType.SWITCH) + checked(pidInfo.isShared() == SharedState.SHARED) + switchChangeListener { _, checked -> + if (checked) { + listener?.onTapShare(pidInfo.threePid) + } else { + listener?.onTapRevoke(pidInfo.threePid) + } + } + } + SharedState.BINDING_IN_PROGRESS -> { + buttonType(ButtonType.NO_BUTTON) + when (pidInfo.finalRequest) { + is Incomplete -> iconMode(IconMode.INFO) + is Fail -> iconMode(IconMode.ERROR) + else -> iconMode(IconMode.NONE) + } + } + } + } + } + } + + private fun buildSharedFail(pidInfo: PidInfo) { + settingsInformationItem { + id("info${pidInfo.threePid.value}") + colorProvider(colorProvider) + textColorId(R.color.vector_error_color) + message((pidInfo.isShared as? Fail)?.error?.message ?: "") + } + } + + private fun buildContinueCancel(threePid: ThreePid) { + settingsContinueCancelItem { + id("bottom${threePid.value}") + interactionListener(object : SettingsContinueCancelItem.Listener { + override fun onContinue() { + when (threePid) { + is ThreePid.Email -> { + listener?.checkEmailVerification(threePid) + } + is ThreePid.Msisdn -> { + val code = codes[threePid] + if (code != null) { + listener?.sendMsisdnVerificationCode(threePid, code) + } + } + } + } + + override fun onCancel() { + listener?.cancelBinding(threePid) + } + }) + } + } + + interface Listener { + fun openIdentityServerTerms() + fun onTapRevoke(threePid: ThreePid) + fun onTapShare(threePid: ThreePid) + fun checkEmailVerification(threePid: ThreePid.Email) + fun cancelBinding(threePid: ThreePid) + fun sendMsisdnVerificationCode(threePid: ThreePid.Msisdn, code: String) + fun onTapChangeIdentityServer() + fun onTapDisconnectIdentityServer() + fun onTapRetryToRetrieveBindings() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsFragment.kt b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsFragment.kt new file mode 100644 index 0000000000..b772db7322 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsFragment.kt @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotx.features.discovery + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AlertDialog +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.matrix.android.api.session.identity.SharedState +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.matrix.android.api.session.terms.TermsService +import im.vector.riotx.R +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.configureWith +import im.vector.riotx.core.extensions.exhaustive +import im.vector.riotx.core.extensions.observeEvent +import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.utils.ensureProtocol +import im.vector.riotx.features.discovery.change.SetIdentityServerFragment +import im.vector.riotx.features.terms.ReviewTermsActivity +import kotlinx.android.synthetic.main.fragment_generic_recycler.* +import javax.inject.Inject + +class DiscoverySettingsFragment @Inject constructor( + private val controller: DiscoverySettingsController, + val viewModelFactory: DiscoverySettingsViewModel.Factory +) : VectorBaseFragment(), DiscoverySettingsController.Listener { + + override fun getLayoutResId() = R.layout.fragment_generic_recycler + + private val viewModel by fragmentViewModel(DiscoverySettingsViewModel::class) + + lateinit var sharedViewModel: DiscoverySharedViewModel + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + sharedViewModel = activityViewModelProvider.get(DiscoverySharedViewModel::class.java) + + controller.listener = this + recyclerView.configureWith(controller) + + sharedViewModel.navigateEvent.observeEvent(this) { + when (it) { + is DiscoverySharedViewModelAction.ChangeIdentityServer -> + viewModel.handle(DiscoverySettingsAction.ChangeIdentityServer(it.newUrl)) + }.exhaustive + } + + viewModel.observeViewEvents { + when (it) { + is DiscoverySettingsViewEvents.Failure -> { + displayErrorDialog(it.throwable) + } + }.exhaustive + } + } + + override fun onDestroyView() { + recyclerView.cleanup() + controller.listener = null + super.onDestroyView() + } + + override fun invalidate() = withState(viewModel) { state -> + controller.setData(state) + } + + override fun onResume() { + super.onResume() + (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_discovery_category) + + // If some 3pids are pending, we can try to check if they have been verified here + viewModel.handle(DiscoverySettingsAction.Refresh) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == ReviewTermsActivity.TERMS_REQUEST_CODE) { + if (Activity.RESULT_OK == resultCode) { + viewModel.handle(DiscoverySettingsAction.RetrieveBinding) + } else { + // add some error? + } + } + + super.onActivityResult(requestCode, resultCode, data) + } + + override fun openIdentityServerTerms() = withState(viewModel) { state -> + if (state.termsNotSigned) { + navigator.openTerms( + this, + TermsService.ServiceType.IdentityService, + state.identityServer()?.ensureProtocol() ?: "", + null) + } + } + + override fun onTapRevoke(threePid: ThreePid) { + viewModel.handle(DiscoverySettingsAction.RevokeThreePid(threePid)) + } + + override fun onTapShare(threePid: ThreePid) { + viewModel.handle(DiscoverySettingsAction.ShareThreePid(threePid)) + } + + override fun checkEmailVerification(threePid: ThreePid.Email) { + viewModel.handle(DiscoverySettingsAction.FinalizeBind3pid(threePid)) + } + + override fun sendMsisdnVerificationCode(threePid: ThreePid.Msisdn, code: String) { + viewModel.handle(DiscoverySettingsAction.SubmitMsisdnToken(threePid, code)) + } + + override fun cancelBinding(threePid: ThreePid) { + viewModel.handle(DiscoverySettingsAction.CancelBinding(threePid)) + } + + override fun onTapChangeIdentityServer() = withState(viewModel) { state -> + // we should prompt if there are bound items with current is + val pidList = state.emailList().orEmpty() + state.phoneNumbersList().orEmpty() + val hasBoundIds = pidList.any { it.isShared() == SharedState.SHARED } + + if (hasBoundIds) { + // we should prompt + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.change_identity_server) + .setMessage(getString(R.string.settings_discovery_disconnect_with_bound_pid, state.identityServer(), state.identityServer())) + .setPositiveButton(R.string._continue) { _, _ -> navigateToChangeIdentityServerFragment() } + .setNegativeButton(R.string.cancel, null) + .show() + Unit + } else { + navigateToChangeIdentityServerFragment() + } + } + + override fun onTapDisconnectIdentityServer() { + // we should prompt if there are bound items with current is + withState(viewModel) { state -> + val pidList = state.emailList().orEmpty() + state.phoneNumbersList().orEmpty() + val hasBoundIds = pidList.any { it.isShared() == SharedState.SHARED } + + val message = if (hasBoundIds) { + getString(R.string.settings_discovery_disconnect_with_bound_pid, state.identityServer(), state.identityServer()) + } else { + getString(R.string.disconnect_identity_server_dialog_content, state.identityServer()) + } + + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.disconnect_identity_server) + .setMessage(message) + .setPositiveButton(R.string.disconnect) { _, _ -> viewModel.handle(DiscoverySettingsAction.DisconnectIdentityServer) } + .setNegativeButton(R.string.cancel, null) + .show() + } + } + + override fun onTapRetryToRetrieveBindings() { + viewModel.handle(DiscoverySettingsAction.RetrieveBinding) + } + + private fun navigateToChangeIdentityServerFragment() { + parentFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.anim_slide_in_bottom, R.anim.anim_slide_out_bottom, R.anim.anim_slide_in_bottom, R.anim.anim_slide_out_bottom) + .replace(R.id.vector_settings_page, SetIdentityServerFragment::class.java, null) + .addToBackStack(null) + .commit() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsState.kt b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsState.kt new file mode 100644 index 0000000000..5dc4b2354a --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsState.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.discovery + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized + +data class DiscoverySettingsState( + val identityServer: Async = Uninitialized, + val emailList: Async> = Uninitialized, + val phoneNumbersList: Async> = Uninitialized, + // Can be true if terms are updated + val termsNotSigned: Boolean = false +) : MvRxState diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsViewEvents.kt new file mode 100644 index 0000000000..6fd45394a2 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsViewEvents.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.discovery + +import im.vector.riotx.core.platform.VectorViewEvents + +sealed class DiscoverySettingsViewEvents : VectorViewEvents { + data class Failure(val throwable: Throwable) : DiscoverySettingsViewEvents() +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsViewModel.kt new file mode 100644 index 0000000000..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