diff --git a/CHANGES.md b/CHANGES.md index d3bca0eb06..7d2bd74c04 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,10 +8,17 @@ Features ✨: Improvements 🙌: - New wording for notice when current user is the sender - Hide "X made no changes" event by default in timeline (#1430) + - Hide left rooms in breadcrumbs (#766) + - Correctly handle SSO login redirection + - SSO login is now performed in the default browser, or in Chrome Custom tab if available (#1400) Bugfix 🐛: - Switch theme is not fully taken into account without restarting the app - Temporary fix to show error when user is creating an account on matrix.org with userId containing only digits (#1410) + - Reply composer overlay stays on screen too long after send (#1169) + - Fix navigation bar icon contrast on API in [21,27[ (#1342) + - Fix status bar icon contrast on API in [21,23[ + - Wrong /query request (#1444) - Make Credentials.homeServer optional because it is deprecated (#1443) Translations 🗣: diff --git a/docs/signin.md b/docs/signin.md index e7368137ae..7d3a4f819b 100644 --- a/docs/signin.md +++ b/docs/signin.md @@ -2,7 +2,7 @@ This document describes the flow of signin to a homeserver, and also the flow when user want to reset his password. Examples come from the `matrix.org` homeserver. -## Sign up flows +## Sign in flows ### Get the flow @@ -58,7 +58,7 @@ We get credential (200) ```json { "user_id": "@alice:matrix.org", - "access_token": "MDAxOGxvY2F0aW9uIG1hdHREDACTEDb2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gfnYrSypfdTtkNXIuNWx1KgowMDJmc2lnbmF0dXJlIOsh1XqeAkXexh4qcofl_aR4kHJoSOWYGOhE7-ubX-DZCg", + "access_token": "MDAxOGxvY2F0aW9uIG1hdHREDACTEDb2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lr", "home_server": "matrix.org", "device_id": "GTVREDALBF", "well_known": { @@ -117,7 +117,7 @@ We get the credentials (200) ```json { "user_id": "@alice:matrix.org", - "access_token": "MDAxOGxvY2F0aW9uIG1hdHJpeC5vcmREDACTEDZXJfaWQgPSBAYmVub2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gNjtDY0MwRlNPSFFoOC5wOgowMDJmc2lnbmF0dXJlIGiTRm1mYLLxQywxOh3qzQVT8HoEorSokEP2u-bAwtnYCg", + "access_token": "MDAxOGxvY2F0aW9uIG1hdHJpeC5vcmREDACTEDZXJfaWQgPSBAYmVub2l0MDgxNjptYXRyaXgub3Jnfrfdegfszsefddvf", "home_server": "matrix.org", "device_id": "WBSREDASND", "well_known": { @@ -145,12 +145,59 @@ Not supported yet in RiotX "flows": [ { "type": "m.login.sso" + }, + { + "type": "m.login.token" } ] } ``` -In this case, the user can click on "Sign in with SSO" and the web screen will be displayed on the page `https://homeserver.with.sso/_matrix/static/client/login/` and the credentials will be passed back to the native code through the JS bridge +In this case, the user can click on "Sign in with SSO" and the native web browser, or a ChromeCustomTab if the device supports it, will be launched on the page + +> https://homeserver.with.sso/_matrix/client/r0/login/sso/redirect?redirectUrl=riotx%3A%2F%2Friotx + +The parameter `redirectUrl` is set to `riotx://riotx`. + +ChromeCustomTabs are an intermediate way to display a WebPage, between a WebView and using the external browser. More info can be found [here](https://developer.chrome.com/multidevice/android/customtabs) + +The browser will then take care of the SSO login, which may include creating a third party account, entering an email, settings a display name, or any other possibilities. + +During the process, user may be asked to validate an email by clicking on a link it contains. The link has to be opened in the browser which initiates the authentication. This is why we cannot use WebView anymore. + +Once the process is finished, the web page will call the `redirectUrl` with an extra parameter `loginToken` + +> riotx://riotx?loginToken=MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy + +This navigation is intercepted by RiotX by the `LoginActivity`, which will then ask the homeserver to convert this `loginToken` to an access token + +> curl -X POST --data $'{"type":"m.login.token","token":"MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy"}' 'https://homeserver.with.sso/_matrix/client/r0/login' + +```json +{ + "type": "m.login.token", + "token": "MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy" +} +``` + +We get the credentials (200) + +```json +{ + "user_id": "@alice:homeserver.with.sso", + "access_token": "MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVyIGtleQowMDEwY2lkIGdlbiA9IDEKMDAyY2NpZCB1c2", + "home_server": "homeserver.with.sso", + "device_id": "DETBTVAHCH", + "well_known": { + "m.homeserver": { + "base_url": "https:\/\/homeserver.with.sso\/" + }, + "m.identity_server": { + "base_url": "https:\/\/vector.im" + } + } +} +``` ## Reset password 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 d2d9eb4d5a..e8fef1361d 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 @@ -29,6 +29,7 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.api.session.widgets.model.Widget import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.toOptional @@ -36,7 +37,6 @@ import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent -import im.vector.matrix.android.api.session.widgets.model.Widget import io.reactivex.Observable import io.reactivex.Single @@ -56,10 +56,10 @@ class RxSession(private val session: Session) { } } - fun liveBreadcrumbs(): Observable> { - return session.getBreadcrumbsLive().asObservable() + fun liveBreadcrumbs(queryParams: RoomSummaryQueryParams): Observable> { + return session.getBreadcrumbsLive(queryParams).asObservable() .startWithCallable { - session.getBreadcrumbs() + session.getBreadcrumbs(queryParams) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Constants.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Constants.kt index af6e2277f4..f4d6d2505e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Constants.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Constants.kt @@ -32,6 +32,6 @@ const val REGISTER_FALLBACK_PATH = "/_matrix/static/client/register/" * Path to use when the client want to connect using SSO * Ref: https://matrix.org/docs/spec/client_server/latest#sso-client-login */ -const val SSO_FALLBACK_PATH = "/_matrix/client/r0/login/sso/redirect" +const val SSO_REDIRECT_PATH = "/_matrix/client/r0/login/sso/redirect" const val SSO_REDIRECT_URL_PARAM = "redirectUrl" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/LoginFlowResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/LoginFlowResult.kt index dd0c93a41c..abc1b83c9f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/LoginFlowResult.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/LoginFlowResult.kt @@ -18,10 +18,10 @@ package im.vector.matrix.android.api.auth.data import im.vector.matrix.android.internal.auth.data.LoginFlowResponse -// Either a LoginFlowResponse, or an error if the homeserver is outdated +// Either a list of supported login types, or an error if the homeserver is outdated sealed class LoginFlowResult { data class Success( - val loginFlowResponse: LoginFlowResponse, + val supportedLoginTypes: List, val isLoginAndRegistrationSupported: Boolean, val homeServerUrl: String ) : LoginFlowResult() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowTypes.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/LoginFlowTypes.kt similarity index 95% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowTypes.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/LoginFlowTypes.kt index 4ff29d594a..922a4cca18 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowTypes.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/LoginFlowTypes.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.auth.data +package im.vector.matrix.android.api.auth.data object LoginFlowTypes { const val PASSWORD = "m.login.password" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/login/LoginWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/login/LoginWizard.kt index d7b2f5d960..9c296d5ddb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/login/LoginWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/login/LoginWizard.kt @@ -34,6 +34,12 @@ interface LoginWizard { deviceName: String, callback: MatrixCallback): Cancelable + /** + * Exchange a login token to an access token + */ + fun loginWithToken(loginToken: String, + callback: MatrixCallback): Cancelable + /** * Reset user password */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt index c3f4864232..ccc5908911 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt @@ -21,15 +21,12 @@ sealed class Stage(open val mandatory: Boolean) { // m.login.recaptcha data class ReCaptcha(override val mandatory: Boolean, val publicKey: String) : Stage(mandatory) - // m.login.oauth2 // m.login.email.identity data class Email(override val mandatory: Boolean) : Stage(mandatory) // m.login.msisdn data class Msisdn(override val mandatory: Boolean) : Stage(mandatory) - // m.login.token - // m.login.dummy, can be mandatory if there is no other stages. In this case the account cannot be created by just sending a username // and a password, the dummy stage has to be done data class Dummy(override val mandatory: Boolean) : Stage(mandatory) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt index 93761dfd26..bc6c17a130 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt @@ -73,15 +73,17 @@ interface RoomService { /** * Get a snapshot list of Breadcrumbs + * @param queryParams parameters to query the room summaries. It can be use to keep only joined rooms, for instance. * @return the immutable list of [RoomSummary] */ - fun getBreadcrumbs(): List + fun getBreadcrumbs(queryParams: RoomSummaryQueryParams): List /** * Get a live list of Breadcrumbs + * @param queryParams parameters to query the room summaries. It can be use to keep only joined rooms, for instance. * @return the [LiveData] of [RoomSummary] */ - fun getBreadcrumbsLive(): LiveData> + fun getBreadcrumbsLive(queryParams: RoomSummaryQueryParams): LiveData> /** * Inform the Matrix SDK that a room is displayed. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt index 2f03c99421..cbd6658c44 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt @@ -21,6 +21,7 @@ import im.vector.matrix.android.api.auth.data.Versions import im.vector.matrix.android.internal.auth.data.LoginFlowResponse import im.vector.matrix.android.internal.auth.data.PasswordLoginParams import im.vector.matrix.android.internal.auth.data.RiotConfig +import im.vector.matrix.android.internal.auth.data.TokenLoginParams import im.vector.matrix.android.internal.auth.login.ResetPasswordMailConfirmed import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationParams import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationResponse @@ -54,7 +55,7 @@ internal interface AuthAPI { fun versions(): Call /** - * Register to the homeserver + * Register to the homeserver, or get error 401 with a RegistrationFlowResponse object if registration is incomplete * Ref: https://matrix.org/docs/spec/client_server/latest#account-registration-and-management */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register") @@ -91,6 +92,11 @@ internal interface AuthAPI { @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login") fun login(@Body loginParams: PasswordLoginParams): Call + // Unfortunately we cannot use interface for @Body parameter, so I duplicate the method for the type TokenLoginParams + @Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000") + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login") + fun login(@Body loginParams: TokenLoginParams): Call + /** * Ask the homeserver to reset the password associated with the provided email. */ 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 b543fa7507..90d62e619c 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 @@ -236,7 +236,7 @@ internal class DefaultAuthenticationService @Inject constructor( val loginFlowResponse = executeRequest(null) { apiCall = authAPI.getLoginFlows() } - LoginFlowResult.Success(loginFlowResponse, versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl) + LoginFlowResult.Success(loginFlowResponse.flows.orEmpty().mapNotNull { it.type }, versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl) } else { // Not supported LoginFlowResult.OutdatedHomeserver diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowResponse.kt index 598506d0a7..ca57a3aa0c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowResponse.kt @@ -20,7 +20,19 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -data class LoginFlowResponse( +internal data class LoginFlowResponse( + /** + * The homeserver's supported login types + */ @Json(name = "flows") - val flows: List + val flows: List? +) + +@JsonClass(generateAdapter = true) +internal data class LoginFlow( + /** + * The login type. This is supplied as the type when logging in. + */ + @Json(name = "type") + val type: String? ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/PasswordLoginParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/PasswordLoginParams.kt index f467b4d3a0..26b1b65218 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/PasswordLoginParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/PasswordLoginParams.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.auth.data import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.auth.data.LoginFlowTypes /** * Ref: diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/TokenLoginParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/TokenLoginParams.kt new file mode 100644 index 0000000000..e95a998d38 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/TokenLoginParams.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.auth.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.auth.data.LoginFlowTypes + +@JsonClass(generateAdapter = true) +internal data class TokenLoginParams( + @Json(name = "type") override val type: String = LoginFlowTypes.TOKEN, + @Json(name = "token") val token: String +) : LoginParams 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 132073b340..2ce9372903 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 @@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.auth.PendingSessionStore import im.vector.matrix.android.internal.auth.SessionCreator import im.vector.matrix.android.internal.auth.data.PasswordLoginParams import im.vector.matrix.android.internal.auth.data.ThreePidMedium +import im.vector.matrix.android.internal.auth.data.TokenLoginParams import im.vector.matrix.android.internal.auth.db.PendingSessionData import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationParams import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationResponse @@ -65,6 +66,22 @@ internal class DefaultLoginWizard( } } + /** + * Ref: https://matrix.org/docs/spec/client_server/latest#handling-the-authentication-endpoint + */ + override fun loginWithToken(loginToken: String, callback: MatrixCallback): Cancelable { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { + val loginParams = TokenLoginParams( + token = loginToken + ) + val credentials = executeRequest(null) { + apiCall = authAPI.login(loginParams) + } + + sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig) + } + } + private suspend fun loginInternal(login: String, password: String, deviceName: String) = withContext(coroutineDispatchers.computation) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParams.kt index ad85579550..9e20be240a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParams.kt @@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.auth.registration import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import im.vector.matrix.android.internal.auth.data.LoginFlowTypes +import im.vector.matrix.android.api.auth.data.LoginFlowTypes /** * Open class, parent to all possible authentication parameters 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 5a39de72ca..750d806b6f 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 @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.auth.registration import dagger.Lazy import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.auth.data.LoginFlowTypes import im.vector.matrix.android.api.auth.registration.RegisterThreePid import im.vector.matrix.android.api.auth.registration.RegistrationResult import im.vector.matrix.android.api.auth.registration.RegistrationWizard @@ -28,7 +29,6 @@ import im.vector.matrix.android.api.util.NoOpCancellable import im.vector.matrix.android.internal.auth.AuthAPI import im.vector.matrix.android.internal.auth.PendingSessionStore import im.vector.matrix.android.internal.auth.SessionCreator -import im.vector.matrix.android.internal.auth.data.LoginFlowTypes 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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt index dd5a553193..5bb87f9557 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt @@ -18,12 +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.auth.data.LoginFlowTypes import im.vector.matrix.android.api.auth.registration.FlowResult import im.vector.matrix.android.api.auth.registration.Stage import im.vector.matrix.android.api.auth.registration.TermPolicies import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.internal.auth.data.InteractiveAuthenticationFlow -import im.vector.matrix.android.internal.auth.data.LoginFlowTypes @JsonClass(generateAdapter = true) data class RegistrationFlowResponse( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryBody.kt index da2dd781dd..758967d05e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryBody.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryBody.kt @@ -36,7 +36,7 @@ internal data class KeysQueryBody( * A map from user ID, to a list of device IDs, or to an empty list to indicate all devices for the corresponding user. */ @Json(name = "device_keys") - val deviceKeys: Map, + val deviceKeys: Map>, /** * If the client is fetching keys as a result of a device update received in a sync request, this should be the 'since' token diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UserPasswordAuth.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UserPasswordAuth.kt index 5e672d4f59..0e0fa9002d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UserPasswordAuth.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UserPasswordAuth.kt @@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.crypto.model.rest import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import im.vector.matrix.android.internal.auth.data.LoginFlowTypes +import im.vector.matrix.android.api.auth.data.LoginFlowTypes /** * This class provides the authentication data by using user and password diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt index 940fa9c7fb..ef9a9ead8e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt @@ -16,7 +16,7 @@ package im.vector.matrix.android.internal.crypto.tasks -import im.vector.matrix.android.internal.auth.data.LoginFlowTypes +import im.vector.matrix.android.api.auth.data.LoginFlowTypes import im.vector.matrix.android.internal.crypto.api.CryptoApi import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceParams import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DownloadKeysForUsersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DownloadKeysForUsersTask.kt index 94fe3c1e8d..5e4b2184b0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DownloadKeysForUsersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DownloadKeysForUsersTask.kt @@ -27,7 +27,7 @@ import javax.inject.Inject internal interface DownloadKeysForUsersTask : Task { data class Params( // the list of users to get keys for. - val userIds: List?, + val userIds: List, // the up-to token val token: String? ) @@ -39,7 +39,7 @@ internal class DefaultDownloadKeysForUsers @Inject constructor( ) : DownloadKeysForUsersTask { override suspend fun execute(params: DownloadKeysForUsersTask.Params): KeysQueryResponse { - val downloadQuery = params.userIds?.associateWith { emptyMap() }.orEmpty() + val downloadQuery = params.userIds.associateWith { emptyList() } val body = KeysQueryBody( deviceKeys = downloadQuery, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt index 84fc357160..8868ab4fe1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt @@ -111,24 +111,22 @@ internal class DefaultRoomService @Inject constructor( return query } - override fun getBreadcrumbs(): List { + override fun getBreadcrumbs(queryParams: RoomSummaryQueryParams): List { return monarchy.fetchAllMappedSync( - { breadcrumbsQuery(it) }, + { breadcrumbsQuery(it, queryParams) }, { roomSummaryMapper.map(it) } ) } - override fun getBreadcrumbsLive(): LiveData> { + override fun getBreadcrumbsLive(queryParams: RoomSummaryQueryParams): LiveData> { return monarchy.findAllMappedWithChanges( - { breadcrumbsQuery(it) }, + { breadcrumbsQuery(it, queryParams) }, { roomSummaryMapper.map(it) } ) } - private fun breadcrumbsQuery(realm: Realm): RealmQuery { - return RoomSummaryEntity.where(realm) - .isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) - .notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name) + private fun breadcrumbsQuery(realm: Realm, queryParams: RoomSummaryQueryParams): RealmQuery { + return roomSummariesQuery(realm, queryParams) .greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummary.NOT_IN_BREADCRUMBS) .sort(RoomSummaryEntityFields.BREADCRUMBS_INDEX) } diff --git a/vector/build.gradle b/vector/build.gradle index 4e0098321d..1eac3106ea 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -332,6 +332,9 @@ dependencies { implementation 'com.google.android:flexbox:1.1.1' implementation "androidx.autofill:autofill:$autofill_version" + // Custom Tab + implementation 'androidx.browser:browser:1.2.0' + // Passphrase strength helper implementation 'com.nulab-inc:zxcvbn:1.2.7' diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 6af791e402..833267483a 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -48,7 +48,19 @@ + android:launchMode="singleTask" + android:windowSoftInputMode="adjustResize"> + + + + + + + + + + + = Build.VERSION_CODES.O_MR1) { + setNavigationBarColor(ContextCompat.getColor(context, R.color.riotx_header_panel_background_light)) + } + } + .setNavigationBarColor(ContextCompat.getColor(context, R.color.riotx_background_light)) + .setColorScheme(CustomTabsIntent.COLOR_SCHEME_LIGHT) + // Note: setting close button icon does not work + .setCloseButtonIcon(BitmapFactory.decodeResource(context.resources, R.drawable.ic_back_24dp)) + .setStartAnimations(context, R.anim.enter_fade_in, R.anim.exit_fade_out) + .setExitAnimations(context, R.anim.enter_fade_in, R.anim.exit_fade_out) + .apply { session?.let { setSession(it) } } + .build() + .launchUrl(context, Uri.parse(url)) + } catch (activityNotFoundException: ActivityNotFoundException) { + context.toast(R.string.error_no_external_application_found) + } +} + /** * Open sound recorder external application */ diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt index b70902631a..3c22260f7f 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt @@ -16,6 +16,7 @@ package im.vector.riotx.features.crypto.recover +import im.vector.matrix.android.api.auth.data.LoginFlowTypes import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.api.failure.toRegistrationFlowResponse @@ -28,7 +29,6 @@ import im.vector.matrix.android.api.session.securestorage.EmptyKeySigner import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo import im.vector.matrix.android.api.session.securestorage.SsssKeySpec -import im.vector.matrix.android.internal.auth.data.LoginFlowTypes import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsViewModel.kt index f8a1e302af..2e847176ff 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsViewModel.kt @@ -21,7 +21,10 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.query.QueryStringValue import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.room.model.Membership +import im.vector.matrix.android.api.session.room.roomSummaryQueryParams import im.vector.matrix.rx.rx import im.vector.riotx.core.platform.EmptyAction import im.vector.riotx.core.platform.EmptyViewEvents @@ -58,7 +61,10 @@ class BreadcrumbsViewModel @AssistedInject constructor(@Assisted initialState: B private fun observeBreadcrumbs() { session.rx() - .liveBreadcrumbs() + .liveBreadcrumbs(roomSummaryQueryParams { + displayName = QueryStringValue.NoCondition + memberships = listOf(Membership.JOIN) + }) .observeOn(Schedulers.computation()) .execute { asyncBreadcrumbs -> copy(asyncBreadcrumbs = asyncBreadcrumbs) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index e4de61c20f..3a2665bc99 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -672,6 +672,8 @@ class RoomDetailFragment @Inject constructor( return } if (text.isNotBlank()) { + // We collapse ASAP, if not there will be a slight anoying delay + composerLayout.collapse(true) lockSendButton = true roomDetailViewModel.handle(RoomDetailAction.SendMessage(text, vectorPreferences.isMarkdownEnabled())) } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt index 3403760136..afd27f04a7 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt @@ -24,6 +24,7 @@ sealed class LoginAction : VectorViewModelAction { data class UpdateServerType(val serverType: ServerType) : LoginAction() data class UpdateHomeServer(val homeServerUrl: String) : LoginAction() data class UpdateSignMode(val signMode: SignMode) : LoginAction() + data class LoginWithToken(val loginToken: String) : LoginAction() data class WebLoginSuccess(val credentials: Credentials) : LoginAction() data class InitWith(val loginConfig: LoginConfig) : LoginAction() data class ResetPassword(val email: String, val newPassword: String) : LoginAction() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index a8c9775980..13bde075a6 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -33,6 +33,7 @@ import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.withState import im.vector.matrix.android.api.auth.registration.FlowResult import im.vector.matrix.android.api.auth.registration.Stage +import im.vector.matrix.android.api.extensions.tryThis import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.POP_BACK_STACK_EXCLUSIVE @@ -155,7 +156,11 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { is LoginViewEvents.OnSignModeSelected -> onSignModeSelected() is LoginViewEvents.OnLoginFlowRetrieved -> addFragmentToBackstack(R.id.loginFragmentContainer, - LoginSignUpSignInSelectionFragment::class.java, + if (loginViewEvents.isSso) { + LoginSignUpSignInSsoFragment::class.java + } else { + LoginSignUpSignInSelectionFragment::class.java + }, option = commonOption) is LoginViewEvents.OnWebLoginError -> onWebLoginError(loginViewEvents) is LoginViewEvents.OnForgetPasswordClicked -> @@ -239,16 +244,14 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { SignMode.SignIn -> { // It depends on the LoginMode when (state.loginMode) { - LoginMode.Unknown -> error("Developer error") + LoginMode.Unknown, + LoginMode.Sso -> error("Developer error") LoginMode.Password -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragment::class.java, tag = FRAGMENT_LOGIN_TAG, option = commonOption) - LoginMode.Sso -> addFragmentToBackstack(R.id.loginFragmentContainer, - LoginWebFragment::class.java, - option = commonOption) LoginMode.Unsupported -> onLoginModeNotSupported(state.loginModeSupportedTypes) - } + }.exhaustive } SignMode.SignInWithMatrixId -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragment::class.java, @@ -257,6 +260,17 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { }.exhaustive } + /** + * Handle the SSO redirection here + */ + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + + intent?.data + ?.let { tryThis { it.getQueryParameter("loginToken") } } + ?.let { loginViewModel.handle(LoginAction.LoginWithToken(it)) } + } + private fun onRegistrationStageNotSupported() { AlertDialog.Builder(this) .setTitle(R.string.app_name) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt index 0e234d3da8..e947a31c25 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt @@ -55,7 +55,7 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment } @OnClick(R.id.loginServerChoiceModularLearnMore) - fun learMore() { + fun learnMore() { openUrlInExternalBrowser(requireActivity(), MODULAR_LINK) } @@ -113,7 +113,7 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment if (state.loginMode != LoginMode.Unknown) { // LoginFlow for matrix.org has been retrieved - loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved)) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved(state.loginMode == LoginMode.Sso))) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt index 6f95847b20..6edd9d06ab 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt @@ -124,7 +124,7 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment() if (state.loginMode != LoginMode.Unknown) { // The home server url is valid - loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved)) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved(state.loginMode == LoginMode.Sso))) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt index f09053c883..427ad99b41 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt @@ -26,13 +26,11 @@ import javax.inject.Inject /** * In this screen, the user is asked to sign up or to sign in to the homeserver */ -class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFragment() { +open class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFragment() { override fun getLayoutResId() = R.layout.fragment_login_signup_signin_selection - private var isSsoSignIn: Boolean = false - - private fun setupUi(state: LoginViewState) { + protected fun setupUi(state: LoginViewState) { when (state.serverType) { ServerType.MatrixOrg -> { loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) @@ -54,25 +52,14 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr } } - private fun setupButtons(state: LoginViewState) { - isSsoSignIn = state.loginMode == LoginMode.Sso - - if (isSsoSignIn) { - loginSignupSigninSubmit.text = getString(R.string.login_signin_sso) - loginSignupSigninSignIn.isVisible = false - } else { - loginSignupSigninSubmit.text = getString(R.string.login_signup) - loginSignupSigninSignIn.isVisible = true - } + private fun setupButtons() { + loginSignupSigninSubmit.text = getString(R.string.login_signup) + loginSignupSigninSignIn.isVisible = true } @OnClick(R.id.loginSignupSigninSubmit) - fun signUp() { - if (isSsoSignIn) { - signIn() - } else { - loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp)) - } + open fun submit() { + loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp)) } @OnClick(R.id.loginSignupSigninSignIn) @@ -86,6 +73,6 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr override fun updateWithState(state: LoginViewState) { setupUi(state) - setupButtons(state) + setupButtons() } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSsoFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSsoFragment.kt new file mode 100644 index 0000000000..538e8be675 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSsoFragment.kt @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.login + +import android.content.ComponentName +import android.net.Uri +import androidx.browser.customtabs.CustomTabsClient +import androidx.browser.customtabs.CustomTabsServiceConnection +import androidx.browser.customtabs.CustomTabsSession +import androidx.core.view.isVisible +import im.vector.riotx.R +import im.vector.riotx.core.utils.openUrlInChromeCustomTab +import kotlinx.android.synthetic.main.fragment_login_signup_signin_selection.* +import javax.inject.Inject + +/** + * In this screen, the user is asked to sign up or to sign in using SSO + * This Fragment binds a CustomTabsServiceConnection if available, then prefetch the SSO url, as it will be likely to be opened. + */ +open class LoginSignUpSignInSsoFragment @Inject constructor() : LoginSignUpSignInSelectionFragment() { + + private var ssoUrl: String? = null + private var customTabsServiceConnection: CustomTabsServiceConnection? = null + private var customTabsClient: CustomTabsClient? = null + private var customTabsSession: CustomTabsSession? = null + + override fun onStart() { + super.onStart() + + val packageName = CustomTabsClient.getPackageName(requireContext(), null) + + // packageName can be null if there are 0 or several CustomTabs compatible browsers installed on the device + if (packageName != null) { + customTabsServiceConnection = object : CustomTabsServiceConnection() { + override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) { + customTabsClient = client + .also { it.warmup(0L) } + } + + override fun onServiceDisconnected(name: ComponentName?) { + } + } + .also { + CustomTabsClient.bindCustomTabsService( + requireContext(), + // Despite the API, packageName cannot be null + packageName, + it + ) + } + } + } + + private fun prefetchUrl(url: String) { + if (ssoUrl != null) return + + ssoUrl = url + if (customTabsSession == null) { + customTabsSession = customTabsClient?.newSession(null) + } + + customTabsSession?.mayLaunchUrl(Uri.parse(url), null, null) + } + + override fun onStop() { + super.onStop() + customTabsServiceConnection?.let { requireContext().unbindService(it) } + customTabsServiceConnection = null + } + + private fun setupButtons() { + loginSignupSigninSubmit.text = getString(R.string.login_signin_sso) + loginSignupSigninSignIn.isVisible = false + } + + override fun submit() { + ssoUrl?.let { openUrlInChromeCustomTab(requireContext(), customTabsSession, it) } + } + + override fun updateWithState(state: LoginViewState) { + setupUi(state) + setupButtons() + prefetchUrl(state.getSsoUrl()) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt index c7c2ee6273..9b69ba8a4f 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt @@ -34,7 +34,7 @@ sealed class LoginViewEvents : VectorViewEvents { object OpenServerSelection : LoginViewEvents() object OnServerSelectionDone : LoginViewEvents() - object OnLoginFlowRetrieved : LoginViewEvents() + data class OnLoginFlowRetrieved(val isSso: Boolean) : LoginViewEvents() object OnSignModeSelected : LoginViewEvents() object OnForgetPasswordClicked : LoginViewEvents() object OnResetPasswordSendThreePidDone : LoginViewEvents() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index 81dcfcea9f..f15cb801ed 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -32,6 +32,7 @@ import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.AuthenticationService 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.LoginFlowTypes import im.vector.matrix.android.api.auth.login.LoginWizard import im.vector.matrix.android.api.auth.registration.FlowResult import im.vector.matrix.android.api.auth.registration.RegistrationResult @@ -40,7 +41,6 @@ import im.vector.matrix.android.api.auth.registration.Stage import im.vector.matrix.android.api.auth.wellknown.WellknownResult import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable -import im.vector.matrix.android.internal.auth.data.LoginFlowTypes import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder @@ -110,6 +110,7 @@ class LoginViewModel @AssistedInject constructor( is LoginAction.InitWith -> handleInitWith(action) is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action) is LoginAction.LoginOrRegister -> handleLoginOrRegister(action) + is LoginAction.LoginWithToken -> handleLoginWithToken(action) is LoginAction.WebLoginSuccess -> handleWebLoginSuccess(action) is LoginAction.ResetPassword -> handleResetPassword(action) is LoginAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed() @@ -120,6 +121,41 @@ class LoginViewModel @AssistedInject constructor( }.exhaustive } + private fun handleLoginWithToken(action: LoginAction.LoginWithToken) { + val safeLoginWizard = loginWizard + + if (safeLoginWizard == null) { + setState { + copy( + asyncLoginAction = Fail(Throwable("Bad configuration")) + ) + } + } else { + setState { + copy( + asyncLoginAction = Loading() + ) + } + + currentTask = safeLoginWizard.loginWithToken( + action.loginToken, + object : MatrixCallback { + override fun onSuccess(data: Session) { + onSessionCreated(data) + } + + override fun onFailure(failure: Throwable) { + _viewEvents.post(LoginViewEvents.Failure(failure)) + setState { + copy( + asyncLoginAction = Fail(failure) + ) + } + } + }) + } + } + private fun handleSetupSsoForSessionRecovery(action: LoginAction.SetupSsoForSessionRecovery) { setState { copy( @@ -635,9 +671,9 @@ class LoginViewModel @AssistedInject constructor( is LoginFlowResult.Success -> { val loginMode = when { // SSO login is taken first - data.loginFlowResponse.flows.any { it.type == LoginFlowTypes.SSO } -> LoginMode.Sso - data.loginFlowResponse.flows.any { it.type == LoginFlowTypes.PASSWORD } -> LoginMode.Password - else -> LoginMode.Unsupported + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso + data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password + else -> LoginMode.Unsupported } if (loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) { @@ -648,7 +684,7 @@ class LoginViewModel @AssistedInject constructor( asyncHomeServerLoginFlowRequest = Uninitialized, homeServerUrl = data.homeServerUrl, loginMode = loginMode, - loginModeSupportedTypes = data.loginFlowResponse.flows.mapNotNull { it.type }.toList() + loginModeSupportedTypes = data.supportedLoginTypes.toList() ) } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt index 3f81fa8f4b..944d1f7d82 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt @@ -22,6 +22,9 @@ import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.PersistState import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.auth.SSO_REDIRECT_PATH +import im.vector.matrix.android.api.auth.SSO_REDIRECT_URL_PARAM +import im.vector.riotx.core.extensions.appendParamToUrl data class LoginViewState( val asyncLoginAction: Async = Uninitialized, @@ -64,4 +67,22 @@ data class LoginViewState( fun isUserLogged(): Boolean { return asyncLoginAction is Success } + + fun getSsoUrl(): String { + return buildString { + append(homeServerUrl?.trim { it == '/' }) + append(SSO_REDIRECT_PATH) + // Set a redirect url we will intercept later + appendParamToUrl(SSO_REDIRECT_URL_PARAM, RIOTX_REDIRECT_URL) + deviceId?.takeIf { it.isNotBlank() }?.let { + // But https://github.com/matrix-org/synapse/issues/5755 + appendParamToUrl("device_id", it) + } + } + } + + companion object { + // Note that the domain can be displayed to the user for confirmation that he trusts it. So use a human readable string + private const val RIOTX_REDIRECT_URL = "riotx://riotx" + } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt index cf3b39ebb0..62431647c0 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt @@ -33,8 +33,6 @@ import androidx.appcompat.app.AlertDialog import com.airbnb.mvrx.activityViewModel import im.vector.matrix.android.api.auth.LOGIN_FALLBACK_PATH import im.vector.matrix.android.api.auth.REGISTER_FALLBACK_PATH -import im.vector.matrix.android.api.auth.SSO_FALLBACK_PATH -import im.vector.matrix.android.api.auth.SSO_REDIRECT_URL_PARAM import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.riotx.R @@ -48,7 +46,7 @@ import java.net.URLDecoder import javax.inject.Inject /** - * This screen is displayed for SSO login and also when the application does not support login flow or registration flow + * This screen is displayed when the application does not support login flow or registration flow * of the homeserver, as a fallback to login or to create an account */ class LoginWebFragment @Inject constructor( @@ -128,17 +126,7 @@ class LoginWebFragment @Inject constructor( val url = buildString { append(state.homeServerUrl?.trim { it == '/' }) if (state.signMode == SignMode.SignIn) { - if (state.loginMode == LoginMode.Sso) { - append(SSO_FALLBACK_PATH) - // We do not want to deal with the result, so let the fallback login page to handle it for us - appendParamToUrl(SSO_REDIRECT_URL_PARAM, - buildString { - append(state.homeServerUrl?.trim { it == '/' }) - append(LOGIN_FALLBACK_PATH) - }) - } else { - append(LOGIN_FALLBACK_PATH) - } + append(LOGIN_FALLBACK_PATH) state.deviceId?.takeIf { it.isNotBlank() }?.let { // But https://github.com/matrix-org/synapse/issues/5755 appendParamToUrl("device_id", it) @@ -226,7 +214,9 @@ class LoginWebFragment @Inject constructor( * @return */ override fun shouldOverrideUrlLoading(view: WebView, url: String?): Boolean { - if (null != url && url.startsWith("js:")) { + if (url == null) return super.shouldOverrideUrlLoading(view, url as String?) + + if (url.startsWith("js:")) { var json = url.substring(3) var javascriptResponse: JavascriptResponse? = null diff --git a/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsViewModel.kt index 24fc1bfdf8..f21a9c69d4 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsViewModel.kt @@ -22,10 +22,10 @@ import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.auth.data.LoginFlowTypes import im.vector.matrix.android.api.failure.toRegistrationFlowResponse import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo -import im.vector.matrix.android.internal.auth.data.LoginFlowTypes import im.vector.matrix.android.internal.crypto.crosssigning.isVerified import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth import im.vector.matrix.rx.rx diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt index c3b645787f..b8aa490aa9 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt @@ -30,13 +30,13 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.NoOpMatrixCallback +import im.vector.matrix.android.api.auth.data.LoginFlowTypes import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod import im.vector.matrix.android.api.session.crypto.verification.VerificationService import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState -import im.vector.matrix.android.internal.auth.data.LoginFlowTypes import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo diff --git a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewModel.kt b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewModel.kt index e2fe17b461..b1d5cb85c1 100644 --- a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewModel.kt @@ -28,9 +28,9 @@ import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.AuthenticationService import im.vector.matrix.android.api.auth.data.LoginFlowResult +import im.vector.matrix.android.api.auth.data.LoginFlowTypes import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable -import im.vector.matrix.android.internal.auth.data.LoginFlowTypes import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.extensions.hasUnsavedKeys import im.vector.riotx.core.platform.VectorViewModel @@ -105,9 +105,9 @@ class SoftLogoutViewModel @AssistedInject constructor( is LoginFlowResult.Success -> { val loginMode = when { // SSO login is taken first - data.loginFlowResponse.flows.any { it.type == LoginFlowTypes.SSO } -> LoginMode.Sso - data.loginFlowResponse.flows.any { it.type == LoginFlowTypes.PASSWORD } -> LoginMode.Password - else -> LoginMode.Unsupported + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso + data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password + else -> LoginMode.Unsupported } if (loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) { diff --git a/vector/src/main/res/drawable/ic_back_24dp.xml b/vector/src/main/res/drawable/ic_back_24dp.xml new file mode 100644 index 0000000000..db9c94314d --- /dev/null +++ b/vector/src/main/res/drawable/ic_back_24dp.xml @@ -0,0 +1,23 @@ + + + + diff --git a/vector/src/main/res/values-v21/theme_light.xml b/vector/src/main/res/values-v21/theme_light.xml index d5a31ad36f..8f52615316 100644 --- a/vector/src/main/res/values-v21/theme_light.xml +++ b/vector/src/main/res/values-v21/theme_light.xml @@ -2,8 +2,10 @@ diff --git a/vector/src/main/res/values-v23/theme_status.xml b/vector/src/main/res/values-v23/theme_status.xml index 054e9fcbc1..b2e12ab55d 100644 --- a/vector/src/main/res/values-v23/theme_status.xml +++ b/vector/src/main/res/values-v23/theme_status.xml @@ -2,7 +2,8 @@ diff --git a/vector/src/main/res/values-v27/theme_status.xml b/vector/src/main/res/values-v27/theme_status.xml index 3c72ede37d..f29383f8a3 100644 --- a/vector/src/main/res/values-v27/theme_status.xml +++ b/vector/src/main/res/values-v27/theme_status.xml @@ -2,6 +2,7 @@