diff --git a/CHANGES.md b/CHANGES.md index 52896df63d..03817341bc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ Features ✨: Improvements 🙌: - Invite member(s) to an existing room (#1276) - Improve notification accessibility with ticker text (#1226) + - Support homeserver discovery from MXID (DISABLED: waiting for design) (#476) Bugfix 🐛: - Sometimes the same device appears twice in the list of devices of a user (#1329) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/AuthenticationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/AuthenticationService.kt index 140d1c259f..5150420de2 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 @@ -23,6 +23,7 @@ import im.vector.matrix.android.api.auth.data.LoginFlowResult import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.login.LoginWizard import im.vector.matrix.android.api.auth.registration.RegistrationWizard +import im.vector.matrix.android.api.auth.wellknown.WellknownResult import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable @@ -30,7 +31,6 @@ import im.vector.matrix.android.api.util.Cancelable * This interface defines methods to authenticate or to create an account to a matrix server. */ interface AuthenticationService { - /** * Request the supported login flows for this homeserver. * This is the first method to call to be able to get a wizard to login or the create an account @@ -89,4 +89,20 @@ interface AuthenticationService { fun createSessionFromSso(homeServerConnectionConfig: HomeServerConnectionConfig, credentials: Credentials, callback: MatrixCallback): Cancelable + + /** + * Perform a wellknown request, using the domain from the matrixId + */ + fun getWellKnownData(matrixId: String, + callback: MatrixCallback): Cancelable + + /** + * Authenticate with a matrixId and a password + * Usually call this after a successful call to getWellKnownData() + */ + fun directAuthentication(homeServerConnectionConfig: HomeServerConnectionConfig, + matrixId: String, + password: String, + initialDeviceName: String, + callback: MatrixCallback): Cancelable } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt index 72affe24bb..d88cd5e74d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt @@ -24,16 +24,38 @@ import im.vector.matrix.android.internal.util.md5 * This data class hold credentials user data. * You shouldn't have to instantiate it. * The access token should be use to authenticate user in all server requests. + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-login */ @JsonClass(generateAdapter = true) data class Credentials( + /** + * The fully-qualified Matrix ID that has been registered. + */ @Json(name = "user_id") val userId: String, - @Json(name = "home_server") val homeServer: String, + /** + * An access token for the account. This access token can then be used to authorize other requests. + */ @Json(name = "access_token") val accessToken: String, + /** + * Not documented + */ @Json(name = "refresh_token") val refreshToken: String?, + /** + * The server_name of the homeserver on which the account has been registered. + * @Deprecated. Clients should extract the server_name from user_id (by splitting at the first colon) + * if they require it. Note also that homeserver is not spelt this way. + */ + @Json(name = "home_server") val homeServer: String, + /** + * ID of the logged-in device. Will be the same as the corresponding parameter in the request, if one was specified. + */ @Json(name = "device_id") val deviceId: String?, - // Optional data that may contain info to override home server and/or identity server - @Json(name = "well_known") val wellKnown: WellKnown? = null + /** + * Optional client configuration provided by the server. If present, clients SHOULD use the provided object to + * reconfigure themselves, optionally validating the URLs within. + * This object takes the same form as the one returned from .well-known autodiscovery. + */ + @Json(name = "well_known") val discoveryInformation: DiscoveryInformation? = null ) internal fun Credentials.sessionId(): String { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/DiscoveryInformation.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/DiscoveryInformation.kt new file mode 100644 index 0000000000..2aa741bad3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/DiscoveryInformation.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.auth.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This is a light version of Wellknown model, used for login response + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-login + */ +@JsonClass(generateAdapter = true) +data class DiscoveryInformation( + /** + * Required. Used by clients to discover homeserver information. + */ + @Json(name = "m.homeserver") + val homeServer: WellKnownBaseConfig? = null, + + /** + * Used by clients to discover identity server information. + * Note: matrix.org does not send this field + */ + @Json(name = "m.identity_server") + val identityServer: WellKnownBaseConfig? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnown.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnown.kt index bdad4702b7..9dd1fa2012 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnown.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnown.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.api.auth.data import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.util.JsonDict /** * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery @@ -52,7 +53,7 @@ data class WellKnown( val identityServer: WellKnownBaseConfig? = null, @Json(name = "m.integrations") - val integrations: Map? = null + val integrations: JsonDict? = null ) { /** * Returns the list of integration managers proposed diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnownManagerConfig.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnownManagerConfig.kt index 33ed412a2a..ffdea37afe 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnownManagerConfig.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnownManagerConfig.kt @@ -16,6 +16,6 @@ package im.vector.matrix.android.api.auth.data data class WellKnownManagerConfig( - val apiUrl : String, + val apiUrl: String, val uiUrl: String ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/wellknown/WellknownResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/wellknown/WellknownResult.kt new file mode 100644 index 0000000000..58c7cf730e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/wellknown/WellknownResult.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.auth.wellknown + +import im.vector.matrix.android.api.auth.data.WellKnown + +/** + * Ref: https://matrix.org/docs/spec/client_server/latest#well-known-uri + */ +sealed class WellknownResult { + /** + * The provided matrixId is no valid. Unable to extract a domain name. + */ + object InvalidMatrixId : WellknownResult() + + /** + * Retrieve the specific piece of information from the user in a way which fits within the existing client user experience, + * if the client is inclined to do so. Failure can take place instead if no good user experience for this is possible at this point. + */ + data class Prompt(val homeServerUrl: String, + val identityServerUrl: String?, + val wellKnown: WellKnown) : WellknownResult() + + /** + * Stop the current auto-discovery mechanism. If no more auto-discovery mechanisms are available, + * then the client may use other methods of determining the required parameters, such as prompting the user, or using default values. + */ + object Ignore : WellknownResult() + + /** + * Inform the user that auto-discovery failed due to invalid/empty data and PROMPT for the parameter. + */ + object FailPrompt : WellknownResult() + + /** + * Inform the user that auto-discovery did not return any usable URLs. Do not continue further with the current login process. + * At this point, valid data was obtained, but no homeserver is available to serve the client. + * No further guess should be attempted and the user should make a conscientious decision what to do next. + */ + object FailError : WellknownResult() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt index 6b6321de36..232cb3f541 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,6 +25,10 @@ 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.database.RealmKeysUtils import im.vector.matrix.android.internal.di.AuthDatabase import io.realm.RealmConfiguration @@ -59,14 +63,20 @@ internal abstract class AuthModule { } @Binds - abstract fun bindSessionParamsStore(sessionParamsStore: RealmSessionParamsStore): SessionParamsStore + abstract fun bindSessionParamsStore(store: RealmSessionParamsStore): SessionParamsStore @Binds - abstract fun bindPendingSessionStore(pendingSessionStore: RealmPendingSessionStore): PendingSessionStore + abstract fun bindPendingSessionStore(store: RealmPendingSessionStore): PendingSessionStore @Binds - abstract fun bindAuthenticationService(authenticationService: DefaultAuthenticationService): AuthenticationService + abstract fun bindAuthenticationService(service: DefaultAuthenticationService): AuthenticationService @Binds - abstract fun bindSessionCreator(sessionCreator: DefaultSessionCreator): SessionCreator + abstract fun bindSessionCreator(creator: DefaultSessionCreator): SessionCreator + + @Binds + abstract fun 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 85c2cdbf3d..997cf70e5a 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 @@ -29,6 +29,7 @@ import im.vector.matrix.android.api.auth.data.isLoginAndRegistrationSupportedByS import im.vector.matrix.android.api.auth.data.isSupportedBySdk import im.vector.matrix.android.api.auth.login.LoginWizard import im.vector.matrix.android.api.auth.registration.RegistrationWizard +import im.vector.matrix.android.api.auth.wellknown.WellknownResult import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable @@ -38,11 +39,16 @@ 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.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 +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.launchToCallback import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers +import im.vector.matrix.android.internal.util.exhaustive import im.vector.matrix.android.internal.util.toCancelable import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -59,7 +65,10 @@ internal class DefaultAuthenticationService @Inject constructor( private val sessionParamsStore: SessionParamsStore, private val sessionManager: SessionManager, private val sessionCreator: SessionCreator, - private val pendingSessionStore: PendingSessionStore + private val pendingSessionStore: PendingSessionStore, + private val getWellknownTask: GetWellknownTask, + private val directLoginTask: DirectLoginTask, + private val taskExecutor: TaskExecutor ) : AuthenticationService { private var pendingSessionData: PendingSessionData? = pendingSessionStore.getPendingSessionData() @@ -148,27 +157,71 @@ internal class DefaultAuthenticationService @Inject constructor( val authAPI = buildAuthAPI(homeServerConnectionConfig) // Ok, try to get the config.json file of a RiotWeb client - val riotConfig = executeRequest(null) { - apiCall = authAPI.getRiotConfig() - } - - if (riotConfig.defaultHomeServerUrl?.isNotBlank() == true) { - // Ok, good sign, we got a default hs url - val newHomeServerConnectionConfig = homeServerConnectionConfig.copy( - homeServerUri = Uri.parse(riotConfig.defaultHomeServerUrl) - ) - - val newAuthAPI = buildAuthAPI(newHomeServerConnectionConfig) - - val versions = executeRequest(null) { - apiCall = newAuthAPI.versions() + return runCatching { + executeRequest(null) { + apiCall = authAPI.getRiotConfig() } - - return getLoginFlowResult(newAuthAPI, versions, riotConfig.defaultHomeServerUrl) - } else { - // Config exists, but there is no default homeserver url (ex: https://riot.im/app) - throw Failure.OtherServerError("", HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) } + .map { riotConfig -> + if (riotConfig.defaultHomeServerUrl?.isNotBlank() == true) { + // Ok, good sign, we got a default hs url + val newHomeServerConnectionConfig = homeServerConnectionConfig.copy( + homeServerUri = Uri.parse(riotConfig.defaultHomeServerUrl) + ) + + val newAuthAPI = buildAuthAPI(newHomeServerConnectionConfig) + + val versions = executeRequest(null) { + apiCall = newAuthAPI.versions() + } + + getLoginFlowResult(newAuthAPI, versions, riotConfig.defaultHomeServerUrl) + } else { + // Config exists, but there is no default homeserver url (ex: https://riot.im/app) + throw Failure.OtherServerError("", HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) + } + } + .fold( + { + it + }, + { + if (it is Failure.OtherServerError + && it.httpCode == HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) { + // Try with wellknown + getWellknownLoginFlowInternal(homeServerConnectionConfig) + } else { + throw it + } + } + ) + } + + private suspend fun getWellknownLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig): LoginFlowResult { + val domain = homeServerConnectionConfig.homeServerUri.host + ?: throw Failure.OtherServerError("", HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) + + // Create a fake userId, for the getWellknown task + val fakeUserId = "@alice:$domain" + val wellknownResult = getWellknownTask.execute(GetWellknownTask.Params(fakeUserId)) + + return when (wellknownResult) { + is WellknownResult.Prompt -> { + val newHomeServerConnectionConfig = homeServerConnectionConfig.copy( + homeServerUri = Uri.parse(wellknownResult.homeServerUrl), + identityServerUri = wellknownResult.identityServerUrl?.let { Uri.parse(it) } + ) + + val newAuthAPI = buildAuthAPI(newHomeServerConnectionConfig) + + val versions = executeRequest(null) { + apiCall = newAuthAPI.versions() + } + + getLoginFlowResult(newAuthAPI, versions, wellknownResult.homeServerUrl) + } + else -> throw Failure.OtherServerError("", HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) + }.exhaustive } private suspend fun getLoginFlowResult(authAPI: AuthAPI, versions: Versions, homeServerUrl: String): LoginFlowResult { @@ -260,6 +313,26 @@ internal class DefaultAuthenticationService @Inject constructor( } } + override fun getWellKnownData(matrixId: String, callback: MatrixCallback): Cancelable { + return getWellknownTask + .configureWith(GetWellknownTask.Params(matrixId)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun directAuthentication(homeServerConnectionConfig: HomeServerConnectionConfig, + matrixId: String, + password: String, + initialDeviceName: String, + callback: MatrixCallback): Cancelable { + return directLoginTask + .configureWith(DirectLoginTask.Params(homeServerConnectionConfig, matrixId, password, initialDeviceName)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + private suspend fun createSessionFromSso(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session = withContext(coroutineDispatchers.computation) { sessionCreator.createSession(credentials, homeServerConnectionConfig) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionCreator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionCreator.kt index 95a9fbb506..74f7cad67d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionCreator.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionCreator.kt @@ -46,14 +46,14 @@ internal class DefaultSessionCreator @Inject constructor( val sessionParams = SessionParams( credentials = credentials, homeServerConnectionConfig = homeServerConnectionConfig.copy( - homeServerUri = credentials.wellKnown?.homeServer?.baseURL + homeServerUri = credentials.discoveryInformation?.homeServer?.baseURL // remove trailing "/" ?.trim { it == '/' } ?.takeIf { it.isNotBlank() } ?.also { Timber.d("Overriding homeserver url to $it") } ?.let { Uri.parse(it) } ?: homeServerConnectionConfig.homeServerUri, - identityServerUri = credentials.wellKnown?.identityServer?.baseURL + identityServerUri = credentials.discoveryInformation?.identityServer?.baseURL // remove trailing "/" ?.trim { it == '/' } ?.takeIf { it.isNotBlank() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/DirectLoginTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/DirectLoginTask.kt new file mode 100644 index 0000000000..01a3ab192d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/DirectLoginTask.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.auth.wellknown + +import dagger.Lazy +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.internal.auth.AuthAPI +import im.vector.matrix.android.internal.auth.SessionCreator +import im.vector.matrix.android.internal.auth.data.PasswordLoginParams +import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.network.RetrofitFactory +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task +import okhttp3.OkHttpClient +import javax.inject.Inject + +internal interface DirectLoginTask : Task { + data class Params( + val homeServerConnectionConfig: HomeServerConnectionConfig, + val userId: String, + val password: String, + val deviceName: String + ) +} + +internal class DefaultDirectLoginTask @Inject constructor( + @Unauthenticated + private val okHttpClient: Lazy, + private val retrofitFactory: RetrofitFactory, + private val sessionCreator: SessionCreator +) : DirectLoginTask { + + override suspend fun execute(params: DirectLoginTask.Params): Session { + val authAPI = retrofitFactory.create(okHttpClient, params.homeServerConnectionConfig.homeServerUri.toString()) + .create(AuthAPI::class.java) + + val loginParams = PasswordLoginParams.userIdentifier(params.userId, params.password, params.deviceName) + + val credentials = executeRequest(null) { + apiCall = authAPI.login(loginParams) + } + + return sessionCreator.createSession(credentials, params.homeServerConnectionConfig) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/GetWellknownTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/GetWellknownTask.kt new file mode 100644 index 0000000000..8ed2cb3b0f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/GetWellknownTask.kt @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.auth.wellknown + +import android.util.MalformedJsonException +import dagger.Lazy +import im.vector.matrix.android.api.MatrixPatterns +import im.vector.matrix.android.api.auth.data.WellKnown +import im.vector.matrix.android.api.auth.wellknown.WellknownResult +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.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.task.Task +import im.vector.matrix.android.internal.util.isValidUrl +import okhttp3.OkHttpClient +import java.io.EOFException +import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection + +internal interface GetWellknownTask : Task { + data class Params( + val matrixId: String + ) +} + +/** + * Inspired from AutoDiscovery class from legacy Matrix Android SDK + */ +internal class DefaultGetWellknownTask @Inject constructor( + @Unauthenticated + private val okHttpClient: Lazy, + private val retrofitFactory: RetrofitFactory +) : GetWellknownTask { + + override suspend fun execute(params: GetWellknownTask.Params): WellknownResult { + if (!MatrixPatterns.isUserId(params.matrixId)) { + return WellknownResult.InvalidMatrixId + } + + val homeServerDomain = params.matrixId.substringAfter(":") + + return findClientConfig(homeServerDomain) + } + + /** + * Find client config + * + * - Do the .well-known request + * - validate homeserver url and identity server url if provide in .well-known result + * - return action and .well-known data + * + * @param domain: homeserver domain, deduced from mx userId (ex: "matrix.org" from userId "@user:matrix.org") + */ + private suspend fun findClientConfig(domain: String): WellknownResult { + val wellKnownAPI = retrofitFactory.create(okHttpClient, "https://dummy.org") + .create(WellKnownAPI::class.java) + + return try { + val wellKnown = executeRequest(null) { + apiCall = wellKnownAPI.getWellKnown(domain) + } + + // Success + val homeServerBaseUrl = wellKnown.homeServer?.baseURL + if (homeServerBaseUrl.isNullOrBlank()) { + WellknownResult.FailPrompt + } else { + if (homeServerBaseUrl.isValidUrl()) { + // Check that HS is a real one + validateHomeServer(homeServerBaseUrl, wellKnown) + } else { + WellknownResult.FailError + } + } + } catch (throwable: Throwable) { + when (throwable) { + is Failure.NetworkConnection -> { + WellknownResult.Ignore + } + is Failure.OtherServerError -> { + when (throwable.httpCode) { + HttpsURLConnection.HTTP_NOT_FOUND -> WellknownResult.Ignore + else -> WellknownResult.FailPrompt + } + } + is MalformedJsonException, is EOFException -> { + WellknownResult.FailPrompt + } + else -> { + throw throwable + } + } + } + } + + /** + * Return true if home server is valid, and (if applicable) if identity server is pingable + */ + private suspend fun validateHomeServer(homeServerBaseUrl: String, wellKnown: WellKnown): WellknownResult { + val capabilitiesAPI = retrofitFactory.create(okHttpClient, homeServerBaseUrl) + .create(CapabilitiesAPI::class.java) + + try { + executeRequest(null) { + apiCall = capabilitiesAPI.getVersions() + } + } catch (throwable: Throwable) { + return WellknownResult.FailError + } + + return if (wellKnown.identityServer == null) { + // No identity server + WellknownResult.Prompt(homeServerBaseUrl, null, wellKnown) + } else { + // if m.identity_server is present it must be valid + val identityServerBaseUrl = wellKnown.identityServer.baseURL + if (identityServerBaseUrl.isNullOrBlank()) { + WellknownResult.FailError + } else { + if (identityServerBaseUrl.isValidUrl()) { + if (validateIdentityServer(identityServerBaseUrl)) { + // All is ok + WellknownResult.Prompt(homeServerBaseUrl, identityServerBaseUrl, wellKnown) + } else { + WellknownResult.FailError + } + } else { + WellknownResult.FailError + } + } + } + } + + /** + * Return true if identity server is pingable + */ + private suspend fun validateIdentityServer(identityServerBaseUrl: String): Boolean { + val identityPingApi = retrofitFactory.create(okHttpClient, identityServerBaseUrl) + .create(IdentityPingApi::class.java) + + return try { + executeRequest(null) { + apiCall = identityPingApi.ping() + } + + true + } catch (throwable: Throwable) { + false + } + } + + /** + * Try to get an identity server URL from a home server URL, using a .wellknown request + */ + /* + fun getIdentityServer(homeServerUrl: String, callback: ApiCallback) { + if (homeServerUrl.startsWith("https://")) { + wellKnownRestClient.getWellKnown(homeServerUrl.substring("https://".length), + object : SimpleApiCallback(callback) { + override fun onSuccess(info: WellKnown) { + callback.onSuccess(info.identityServer?.baseURL) + } + }) + } else { + callback.onUnexpectedError(InvalidParameterException("malformed url")) + } + } + + fun getServerPreferredIntegrationManagers(homeServerUrl: String, callback: ApiCallback>) { + if (homeServerUrl.startsWith("https://")) { + wellKnownRestClient.getWellKnown(homeServerUrl.substring("https://".length), + object : SimpleApiCallback(callback) { + override fun onSuccess(info: WellKnown) { + callback.onSuccess(info.getIntegrationManagers()) + } + }) + } else { + callback.onUnexpectedError(InvalidParameterException("malformed url")) + } + } + */ +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/WellKnownAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/WellKnownAPI.kt new file mode 100644 index 0000000000..71928123bf --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/WellKnownAPI.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.auth.wellknown + +import im.vector.matrix.android.api.auth.data.WellKnown +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Path + +internal interface WellKnownAPI { + @GET("https://{domain}/.well-known/matrix/client") + fun getWellKnown(@Path("domain") domain: String): Call +} diff --git a/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/identity/IdentityPingApi.kt new file mode 100644 index 0000000000..2a0e00704c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/identity/IdentityPingApi.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.identity + +import im.vector.matrix.android.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.GET + +internal interface IdentityPingApi { + + /** + * 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 + */ + @GET(NetworkConstants.URI_API_PREFIX_IDENTITY) + fun ping(): Call +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt index c6c10d9a8f..ab6745148f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt @@ -26,4 +26,10 @@ internal object NetworkConstants { // Media private const val URI_API_MEDIA_PREFIX_PATH = "_matrix/media" const val URI_API_MEDIA_PREFIX_PATH_R0 = "$URI_API_MEDIA_PREFIX_PATH/r0/" + + // Identity server + const val URI_IDENTITY_PATH = "_matrix/identity/api/v1/" + const val URI_IDENTITY_PATH_V2 = "_matrix/identity/v2/" + + const val URI_API_PREFIX_IDENTITY = "_matrix/identity/api/v1" } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/SearchUserAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/SearchUserAPI.kt index e57daed617..4adcee88aa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/SearchUserAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/SearchUserAPI.kt @@ -16,7 +16,7 @@ package im.vector.matrix.android.internal.session.user -import im.vector.matrix.android.internal.network.NetworkConstants.URI_API_PREFIX_PATH_R0 +import im.vector.matrix.android.internal.network.NetworkConstants import im.vector.matrix.android.internal.session.user.model.SearchUsersParams import im.vector.matrix.android.internal.session.user.model.SearchUsersResponse import retrofit2.Call @@ -30,6 +30,6 @@ internal interface SearchUserAPI { * * @param searchUsersParams the search params. */ - @POST(URI_API_PREFIX_PATH_R0 + "user_directory/search") + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user_directory/search") fun searchUsers(@Body searchUsersParams: SearchUsersParams): Call } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSharedActionViewModel.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/UrlUtils.kt similarity index 67% rename from vector/src/main/java/im/vector/riotx/features/login/LoginSharedActionViewModel.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/UrlUtils.kt index 625208b682..8ad5e89605 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSharedActionViewModel.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/UrlUtils.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright (c) 2020 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,9 +14,15 @@ * limitations under the License. */ -package im.vector.riotx.features.login +package im.vector.matrix.android.internal.util -import im.vector.riotx.core.platform.VectorSharedActionViewModel -import javax.inject.Inject +import java.net.URL -class LoginSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel() +internal fun String.isValidUrl(): Boolean { + return try { + URL(this) + true + } catch (t: Throwable) { + false + } +} diff --git a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt index e480cf22ca..8046f67668 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt @@ -30,11 +30,10 @@ import im.vector.riotx.features.home.HomeSharedActionViewModel import im.vector.riotx.features.home.room.detail.RoomDetailSharedActionViewModel import im.vector.riotx.features.home.room.detail.timeline.action.MessageSharedActionViewModel import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel -import im.vector.riotx.features.login.LoginSharedActionViewModel -import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel import im.vector.riotx.features.reactions.EmojiChooserViewModel import im.vector.riotx.features.roomdirectory.RoomDirectorySharedActionViewModel import im.vector.riotx.features.roomprofile.RoomProfileSharedActionViewModel +import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel import im.vector.riotx.features.workers.signout.SignOutViewModel @Module @@ -110,11 +109,6 @@ interface ViewModelModule { @ViewModelKey(RoomDirectorySharedActionViewModel::class) fun bindRoomDirectorySharedActionViewModel(viewModel: RoomDirectorySharedActionViewModel): ViewModel - @Binds - @IntoMap - @ViewModelKey(LoginSharedActionViewModel::class) - fun bindLoginSharedActionViewModel(viewModel: LoginSharedActionViewModel): ViewModel - @Binds @IntoMap @ViewModelKey(RoomDetailSharedActionViewModel::class) diff --git a/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt index 83263d05a2..8fceaad07f 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt @@ -38,7 +38,6 @@ import javax.net.ssl.HttpsURLConnection abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed { protected val loginViewModel: LoginViewModel by activityViewModel() - protected lateinit var loginSharedActionViewModel: LoginSharedActionViewModel private var isResetPasswordStarted = false @@ -57,8 +56,6 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - loginSharedActionViewModel = activityViewModelProvider.get(LoginSharedActionViewModel::class.java) - loginViewModel.observeViewEvents { handleLoginViewEvents(it) } 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 90d6754448..3403760136 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 @@ -58,4 +58,6 @@ sealed class LoginAction : VectorViewModelAction { // For the soft logout case data class SetupSsoForSessionRecovery(val homeServerUrl: String, val deviceId: String) : LoginAction() + + data class PostViewEvent(val viewEvent: LoginViewEvents) : 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 c67e45d9e7..99d8da490d 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 @@ -38,6 +38,7 @@ import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.POP_BACK_STACK_EXCLUSIVE import im.vector.riotx.core.extensions.addFragment import im.vector.riotx.core.extensions.addFragmentToBackstack +import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.platform.ToolbarConfigurable import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.features.home.HomeActivity @@ -54,7 +55,6 @@ import javax.inject.Inject open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { private val loginViewModel: LoginViewModel by viewModel() - private lateinit var loginSharedActionViewModel: LoginSharedActionViewModel @Inject lateinit var loginViewModelFactory: LoginViewModel.Factory @@ -98,14 +98,6 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { loginViewModel.handle(LoginAction.InitWith(loginConfig)) } - loginSharedActionViewModel = viewModelProvider.get(LoginSharedActionViewModel::class.java) - loginSharedActionViewModel - .observe() - .subscribe { - handleLoginNavigation(it) - } - .disposeOnDestroy() - loginViewModel .subscribe(this) { updateWithState(it) @@ -124,65 +116,9 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { addFragment(R.id.loginFragmentContainer, LoginSplashFragment::class.java) } - private fun handleLoginNavigation(loginNavigation: LoginNavigation) { - // Assigning to dummy make sure we do not forget a case - @Suppress("UNUSED_VARIABLE") - val dummy = when (loginNavigation) { - is LoginNavigation.OpenServerSelection -> - addFragmentToBackstack(R.id.loginFragmentContainer, - LoginServerSelectionFragment::class.java, - option = { ft -> - findViewById(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } - findViewById(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } - findViewById(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } - // TODO Disabled because it provokes a flickering - // ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) - }) - is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone() - is LoginNavigation.OnSignModeSelected -> onSignModeSelected() - is LoginNavigation.OnLoginFlowRetrieved -> - addFragmentToBackstack(R.id.loginFragmentContainer, - LoginSignUpSignInSelectionFragment::class.java, - option = commonOption) - is LoginNavigation.OnWebLoginError -> onWebLoginError(loginNavigation) - is LoginNavigation.OnForgetPasswordClicked -> - addFragmentToBackstack(R.id.loginFragmentContainer, - LoginResetPasswordFragment::class.java, - option = commonOption) - is LoginNavigation.OnResetPasswordSendThreePidDone -> { - supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) - addFragmentToBackstack(R.id.loginFragmentContainer, - LoginResetPasswordMailConfirmationFragment::class.java, - option = commonOption) - } - is LoginNavigation.OnResetPasswordMailConfirmationSuccess -> { - supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) - addFragmentToBackstack(R.id.loginFragmentContainer, - LoginResetPasswordSuccessFragment::class.java, - option = commonOption) - } - is LoginNavigation.OnResetPasswordMailConfirmationSuccessDone -> { - // Go back to the login fragment - supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) - } - is LoginNavigation.OnSendEmailSuccess -> - addFragmentToBackstack(R.id.loginFragmentContainer, - LoginWaitForEmailFragment::class.java, - LoginWaitForEmailFragmentArgument(loginNavigation.email), - tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption) - is LoginNavigation.OnSendMsisdnSuccess -> - addFragmentToBackstack(R.id.loginFragmentContainer, - LoginGenericTextInputFormFragment::class.java, - LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, loginNavigation.msisdn), - tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption) - } - } - private fun handleLoginViewEvents(loginViewEvents: LoginViewEvents) { when (loginViewEvents) { - is LoginViewEvents.RegistrationFlowResult -> { + is LoginViewEvents.RegistrationFlowResult -> { // Check that all flows are supported by the application if (loginViewEvents.flowResult.missingStages.any { !it.isSupported() }) { // Display a popup to propose use web fallback @@ -203,15 +139,64 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { } } } - is LoginViewEvents.OutdatedHomeserver -> + is LoginViewEvents.OutdatedHomeserver -> AlertDialog.Builder(this) .setTitle(R.string.login_error_outdated_homeserver_title) .setMessage(R.string.login_error_outdated_homeserver_content) .setPositiveButton(R.string.ok, null) .show() - is LoginViewEvents.Failure -> + is LoginViewEvents.Failure -> // This is handled by the Fragments Unit + is LoginViewEvents.OpenServerSelection -> + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginServerSelectionFragment::class.java, + option = { ft -> + findViewById(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + findViewById(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + findViewById(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + // TODO Disabled because it provokes a flickering + // ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) + }) + is LoginViewEvents.OnServerSelectionDone -> onServerSelectionDone() + is LoginViewEvents.OnSignModeSelected -> onSignModeSelected() + is LoginViewEvents.OnLoginFlowRetrieved -> + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginSignUpSignInSelectionFragment::class.java, + option = commonOption) + is LoginViewEvents.OnWebLoginError -> onWebLoginError(loginViewEvents) + is LoginViewEvents.OnForgetPasswordClicked -> + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginResetPasswordFragment::class.java, + option = commonOption) + is LoginViewEvents.OnResetPasswordSendThreePidDone -> { + supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginResetPasswordMailConfirmationFragment::class.java, + option = commonOption) + } + is LoginViewEvents.OnResetPasswordMailConfirmationSuccess -> { + supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginResetPasswordSuccessFragment::class.java, + option = commonOption) + } + is LoginViewEvents.OnResetPasswordMailConfirmationSuccessDone -> { + // Go back to the login fragment + supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) + } + is LoginViewEvents.OnSendEmailSuccess -> + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginWaitForEmailFragment::class.java, + LoginWaitForEmailFragmentArgument(loginViewEvents.email), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption) + is LoginViewEvents.OnSendMsisdnSuccess -> + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginGenericTextInputFormFragment::class.java, + LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, loginViewEvents.msisdn), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption) } } @@ -230,7 +215,7 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { loginLoading.isVisible = loginViewState.isLoading() } - private fun onWebLoginError(onWebLoginError: LoginNavigation.OnWebLoginError) { + private fun onWebLoginError(onWebLoginError: LoginViewEvents.OnWebLoginError) { // Pop the backstack supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) @@ -254,11 +239,11 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { private fun onSignModeSelected() = withState(loginViewModel) { state -> when (state.signMode) { - SignMode.Unknown -> error("Sign mode has to be set before calling this method") - SignMode.SignUp -> { + SignMode.Unknown -> error("Sign mode has to be set before calling this method") + SignMode.SignUp -> { // This is managed by the LoginViewEvents } - SignMode.SignIn -> { + SignMode.SignIn -> { // It depends on the LoginMode when (state.loginMode) { LoginMode.Unknown -> error("Developer error") @@ -272,7 +257,11 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { LoginMode.Unsupported -> onLoginModeNotSupported(state.loginModeSupportedTypes) } } - } + SignMode.SignInWithMatrixId -> addFragmentToBackstack(R.id.loginFragmentContainer, + LoginFragment::class.java, + tag = FRAGMENT_LOGIN_TAG, + option = commonOption) + }.exhaustive } private fun onRegistrationStageNotSupported() { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 8b89aeda2a..c2bd02b817 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -31,6 +31,7 @@ import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.api.failure.isInvalidPassword import im.vector.riotx.R +import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.showPassword import im.vector.riotx.core.extensions.toReducedUrl @@ -73,16 +74,17 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { private fun setupAutoFill(state: LoginViewState) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { when (state.signMode) { - SignMode.Unknown -> error("developer error") - SignMode.SignUp -> { + SignMode.Unknown -> error("developer error") + SignMode.SignUp -> { loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_USERNAME) passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD) } - SignMode.SignIn -> { + SignMode.SignIn, + SignMode.SignInWithMatrixId -> { loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_USERNAME) passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_PASSWORD) } - } + }.exhaustive } } @@ -116,35 +118,44 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { } private fun setupUi(state: LoginViewState) { - val resId = when (state.signMode) { - SignMode.Unknown -> error("developer error") - SignMode.SignUp -> R.string.login_signup_to - SignMode.SignIn -> R.string.login_connect_to - } - loginFieldTil.hint = getString(when (state.signMode) { - SignMode.Unknown -> error("developer error") - SignMode.SignUp -> R.string.login_signup_username_hint - SignMode.SignIn -> R.string.login_signin_username_hint + SignMode.Unknown -> error("developer error") + SignMode.SignUp -> R.string.login_signup_username_hint + SignMode.SignIn -> R.string.login_signin_username_hint + SignMode.SignInWithMatrixId -> R.string.login_signin_matrix_id_hint }) - when (state.serverType) { - ServerType.MatrixOrg -> { - loginServerIcon.isVisible = true - loginServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) - loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl()) - loginNotice.text = getString(R.string.login_server_matrix_org_text) + // Handle direct signin first + if (state.signMode == SignMode.SignInWithMatrixId) { + loginServerIcon.isVisible = false + loginTitle.text = getString(R.string.login_signin_matrix_id_title) + loginNotice.text = getString(R.string.login_signin_matrix_id_notice) + } else { + val resId = when (state.signMode) { + SignMode.Unknown -> error("developer error") + SignMode.SignUp -> R.string.login_signup_to + SignMode.SignIn -> R.string.login_connect_to + SignMode.SignInWithMatrixId -> R.string.login_connect_to } - ServerType.Modular -> { - loginServerIcon.isVisible = true - loginServerIcon.setImageResource(R.drawable.ic_logo_modular) - loginTitle.text = getString(resId, "Modular") - loginNotice.text = getString(R.string.login_server_modular_text) - } - ServerType.Other -> { - loginServerIcon.isVisible = false - loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl()) - loginNotice.text = getString(R.string.login_server_other_text) + + when (state.serverType) { + ServerType.MatrixOrg -> { + loginServerIcon.isVisible = true + loginServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) + loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl()) + loginNotice.text = getString(R.string.login_server_matrix_org_text) + } + ServerType.Modular -> { + loginServerIcon.isVisible = true + loginServerIcon.setImageResource(R.drawable.ic_logo_modular) + loginTitle.text = getString(resId, "Modular") + loginNotice.text = getString(R.string.login_server_modular_text) + } + ServerType.Other -> { + loginServerIcon.isVisible = false + loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl()) + loginNotice.text = getString(R.string.login_server_other_text) + } } } } @@ -153,9 +164,10 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { forgetPasswordButton.isVisible = state.signMode == SignMode.SignIn loginSubmit.text = getString(when (state.signMode) { - SignMode.Unknown -> error("developer error") - SignMode.SignUp -> R.string.login_signup_submit - SignMode.SignIn -> R.string.login_signin + SignMode.Unknown -> error("developer error") + SignMode.SignUp -> R.string.login_signup_submit + SignMode.SignIn, + SignMode.SignInWithMatrixId -> R.string.login_signin }) } @@ -178,7 +190,7 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { @OnClick(R.id.forgetPasswordButton) fun forgetPasswordClicked() { - loginSharedActionViewModel.post(LoginNavigation.OnForgetPasswordClicked) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnForgetPasswordClicked)) } private fun setupPasswordReveal() { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt index 3ee1cd6d64..5203e60b26 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt @@ -217,7 +217,7 @@ class LoginGenericTextInputFormFragment @Inject constructor() : AbstractLoginFra TextInputFormFragmentMode.SetEmail -> { if (throwable.is401()) { // This is normal use case, we go to the mail waiting screen - loginSharedActionViewModel.post(LoginNavigation.OnSendEmailSuccess(loginViewModel.currentThreePid ?: "")) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnSendEmailSuccess(loginViewModel.currentThreePid ?: ""))) } else { loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable) } @@ -225,7 +225,7 @@ class LoginGenericTextInputFormFragment @Inject constructor() : AbstractLoginFra TextInputFormFragmentMode.SetMsisdn -> { if (throwable.is401()) { // This is normal use case, we go to the enter code screen - loginSharedActionViewModel.post(LoginNavigation.OnSendMsisdnSuccess(loginViewModel.currentThreePid ?: "")) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnSendMsisdnSuccess(loginViewModel.currentThreePid ?: ""))) } else { loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable) } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt deleted file mode 100644 index 79c6409a3f..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.riotx.features.login - -import im.vector.riotx.core.platform.VectorSharedAction - -// Supported navigation actions for LoginActivity -sealed class LoginNavigation : VectorSharedAction { - object OpenServerSelection : LoginNavigation() - object OnServerSelectionDone : LoginNavigation() - object OnLoginFlowRetrieved : LoginNavigation() - object OnSignModeSelected : LoginNavigation() - object OnForgetPasswordClicked : LoginNavigation() - object OnResetPasswordSendThreePidDone : LoginNavigation() - object OnResetPasswordMailConfirmationSuccess : LoginNavigation() - object OnResetPasswordMailConfirmationSuccessDone : LoginNavigation() - - data class OnSendEmailSuccess(val email: String) : LoginNavigation() - data class OnSendMsisdnSuccess(val msisdn: String) : LoginNavigation() - - data class OnWebLoginError(val errorCode: Int, val description: String, val failingUrl: String) : LoginNavigation() -} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt index d3a86ef769..d90cfc77e5 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt @@ -149,7 +149,7 @@ class LoginResetPasswordFragment @Inject constructor() : AbstractLoginFragment() resetPasswordEmailTil.error = errorFormatter.toHumanReadable(state.asyncResetPassword.error) } is Success -> { - loginSharedActionViewModel.post(LoginNavigation.OnResetPasswordSendThreePidDone) + Unit } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordMailConfirmationFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordMailConfirmationFragment.kt index cace48b7f2..f340fb8f9f 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordMailConfirmationFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordMailConfirmationFragment.kt @@ -64,7 +64,7 @@ class LoginResetPasswordMailConfirmationFragment @Inject constructor() : Abstrac .show() } is Success -> { - loginSharedActionViewModel.post(LoginNavigation.OnResetPasswordMailConfirmationSuccess) + Unit } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordSuccessFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordSuccessFragment.kt index 4faeef1269..fd3c5e6377 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordSuccessFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordSuccessFragment.kt @@ -29,7 +29,7 @@ class LoginResetPasswordSuccessFragment @Inject constructor() : AbstractLoginFra @OnClick(R.id.resetPasswordSuccessSubmit) fun submit() { - loginSharedActionViewModel.post(LoginNavigation.OnResetPasswordMailConfirmationSuccessDone) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnResetPasswordMailConfirmationSuccessDone)) } override fun resetViewModel() { 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 9050ea2688..0e234d3da8 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 @@ -95,10 +95,15 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment // Request login flow here loginViewModel.handle(LoginAction.UpdateHomeServer(getString(R.string.matrix_org_server_url))) } else { - loginSharedActionViewModel.post(LoginNavigation.OnServerSelectionDone) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnServerSelectionDone)) } } + @OnClick(R.id.loginServerIKnowMyIdSubmit) + fun loginWithMatrixId() { + loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignInWithMatrixId)) + } + override fun resetViewModel() { loginViewModel.handle(LoginAction.ResetHomeServerType) } @@ -108,7 +113,7 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment if (state.loginMode != LoginMode.Unknown) { // LoginFlow for matrix.org has been retrieved - loginSharedActionViewModel.post(LoginNavigation.OnLoginFlowRetrieved) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved)) } } } 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 898ee97656..92dcfcc8aa 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 @@ -126,7 +126,7 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment() if (state.loginMode != LoginMode.Unknown) { // The home server url is valid - loginSharedActionViewModel.post(LoginNavigation.OnLoginFlowRetrieved) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved)) } } } 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 9f084299b7..f09053c883 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 @@ -78,7 +78,6 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr @OnClick(R.id.loginSignupSigninSignIn) fun signIn() { loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignIn)) - loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) } override fun resetViewModel() { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt index 53de8c2c43..c860d02fec 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt @@ -29,7 +29,7 @@ class LoginSplashFragment @Inject constructor() : AbstractLoginFragment() { @OnClick(R.id.loginSplashSubmit) fun getStarted() { - loginSharedActionViewModel.post(LoginNavigation.OpenServerSelection) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OpenServerSelection)) } override fun resetViewModel() { 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 25747df3d4..c7c2ee6273 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 @@ -23,10 +23,26 @@ import im.vector.riotx.core.platform.VectorViewEvents /** * Transient events for Login */ -sealed class LoginViewEvents: VectorViewEvents { +sealed class LoginViewEvents : VectorViewEvents { data class Loading(val message: CharSequence? = null) : LoginViewEvents() data class Failure(val throwable: Throwable) : LoginViewEvents() data class RegistrationFlowResult(val flowResult: FlowResult, val isRegistrationStarted: Boolean) : LoginViewEvents() object OutdatedHomeserver : LoginViewEvents() + + // Navigation event + + object OpenServerSelection : LoginViewEvents() + object OnServerSelectionDone : LoginViewEvents() + object OnLoginFlowRetrieved : LoginViewEvents() + object OnSignModeSelected : LoginViewEvents() + object OnForgetPasswordClicked : LoginViewEvents() + object OnResetPasswordSendThreePidDone : LoginViewEvents() + object OnResetPasswordMailConfirmationSuccess : LoginViewEvents() + object OnResetPasswordMailConfirmationSuccessDone : LoginViewEvents() + + data class OnSendEmailSuccess(val email: String) : LoginViewEvents() + data class OnSendMsisdnSuccess(val msisdn: String) : LoginViewEvents() + + data class OnWebLoginError(val errorCode: Int, val description: String, val failingUrl: String) : 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 80b04fe062..81dcfcea9f 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 @@ -17,6 +17,7 @@ package im.vector.riotx.features.login import android.content.Context +import android.net.Uri import androidx.fragment.app.FragmentActivity import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.Fail @@ -29,19 +30,24 @@ 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.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.login.LoginWizard import im.vector.matrix.android.api.auth.registration.FlowResult import im.vector.matrix.android.api.auth.registration.RegistrationResult import im.vector.matrix.android.api.auth.registration.RegistrationWizard 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 import im.vector.riotx.core.extensions.configureAndStart +import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.notifications.PushRuleTriggerListener import im.vector.riotx.features.session.SessionListener import im.vector.riotx.features.signout.soft.SoftLogoutActivity @@ -51,14 +57,16 @@ import java.util.concurrent.CancellationException /** * */ -class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginViewState, - private val applicationContext: Context, - private val authenticationService: AuthenticationService, - private val activeSessionHolder: ActiveSessionHolder, - private val pushRuleTriggerListener: PushRuleTriggerListener, - private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory, - private val sessionListener: SessionListener, - private val reAuthHelper: ReAuthHelper) +class LoginViewModel @AssistedInject constructor( + @Assisted initialState: LoginViewState, + private val applicationContext: Context, + private val authenticationService: AuthenticationService, + private val activeSessionHolder: ActiveSessionHolder, + private val pushRuleTriggerListener: PushRuleTriggerListener, + private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory, + private val sessionListener: SessionListener, + private val reAuthHelper: ReAuthHelper, + private val stringProvider: StringProvider) : VectorViewModel(initialState) { @AssistedInject.Factory @@ -108,7 +116,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi is LoginAction.RegisterAction -> handleRegisterAction(action) is LoginAction.ResetAction -> handleResetAction(action) is LoginAction.SetupSsoForSessionRecovery -> handleSetupSsoForSessionRecovery(action) - } + is LoginAction.PostViewEvent -> _viewEvents.post(action.viewEvent) + }.exhaustive } private fun handleSetupSsoForSessionRecovery(action: LoginAction.SetupSsoForSessionRecovery) { @@ -320,11 +329,12 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi ) } - if (action.signMode == SignMode.SignUp) { - startRegistrationFlow() - } else if (action.signMode == SignMode.SignIn) { - startAuthenticationFlow() - } + when (action.signMode) { + SignMode.SignUp -> startRegistrationFlow() + SignMode.SignIn -> startAuthenticationFlow() + SignMode.SignInWithMatrixId -> _viewEvents.post(LoginViewEvents.OnSignModeSelected) + SignMode.Unknown -> Unit + }.exhaustive } private fun handleUpdateServerType(action: LoginAction.UpdateServerType) { @@ -365,6 +375,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi resetPasswordEmail = action.email ) } + + _viewEvents.post(LoginViewEvents.OnResetPasswordSendThreePidDone) } override fun onFailure(failure: Throwable) { @@ -405,6 +417,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi resetPasswordEmail = null ) } + + _viewEvents.post(LoginViewEvents.OnResetPasswordMailConfirmationSuccess) } override fun onFailure(failure: Throwable) { @@ -421,10 +435,78 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi private fun handleLoginOrRegister(action: LoginAction.LoginOrRegister) = withState { state -> when (state.signMode) { - SignMode.SignIn -> handleLogin(action) - SignMode.SignUp -> handleRegisterWith(action) - else -> error("Developer error, invalid sign mode") + SignMode.Unknown -> error("Developer error, invalid sign mode") + SignMode.SignIn -> handleLogin(action) + SignMode.SignUp -> handleRegisterWith(action) + SignMode.SignInWithMatrixId -> handleDirectLogin(action) + }.exhaustive + } + + private fun handleDirectLogin(action: LoginAction.LoginOrRegister) { + setState { + copy( + asyncLoginAction = Loading() + ) } + + authenticationService.getWellKnownData(action.username, object : MatrixCallback { + override fun onSuccess(data: WellknownResult) { + when (data) { + is WellknownResult.Prompt -> + onWellknownSuccess(action, data) + is WellknownResult.InvalidMatrixId -> { + setState { + copy( + asyncLoginAction = Uninitialized + ) + } + _viewEvents.post(LoginViewEvents.Failure(Exception(stringProvider.getString(R.string.login_signin_matrix_id_error_invalid_matrix_id)))) + } + else -> { + setState { + copy( + asyncLoginAction = Uninitialized + ) + } + _viewEvents.post(LoginViewEvents.Failure(Exception(stringProvider.getString(R.string.autodiscover_well_known_error)))) + } + }.exhaustive + } + + override fun onFailure(failure: Throwable) { + setState { + copy( + asyncLoginAction = Fail(failure) + ) + } + } + }) + } + + private fun onWellknownSuccess(action: LoginAction.LoginOrRegister, wellKnownPrompt: WellknownResult.Prompt) { + val homeServerConnectionConfig = HomeServerConnectionConfig( + homeServerUri = Uri.parse(wellKnownPrompt.homeServerUrl), + identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) } + ) + + authenticationService.directAuthentication( + homeServerConnectionConfig, + action.username, + action.password, + action.initialDeviceName, + object : MatrixCallback { + override fun onSuccess(data: Session) { + onSessionCreated(data) + } + + override fun onFailure(failure: Throwable) { + setState { + copy( + asyncLoginAction = Fail(failure) + ) + } + } + }) } private fun handleLogin(action: LoginAction.LoginOrRegister) { @@ -477,6 +559,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi private fun startAuthenticationFlow() { // Ensure Wizard is ready loginWizard + + _viewEvents.post(LoginViewEvents.OnSignModeSelected) } private fun onFlowResponse(flowResult: FlowResult) { 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 f9b0b98f29..cf3b39ebb0 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 @@ -173,7 +173,7 @@ class LoginWebFragment @Inject constructor( override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) { super.onReceivedError(view, errorCode, description, failingUrl) - loginSharedActionViewModel.post(LoginNavigation.OnWebLoginError(errorCode, description, failingUrl)) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnWebLoginError(errorCode, description, failingUrl))) } override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { diff --git a/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt b/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt index b793a0fe1d..ad06f1f4a9 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt @@ -21,5 +21,7 @@ enum class SignMode { // Account creation SignUp, // Login - SignIn + SignIn, + // Login directly with matrix Id + SignInWithMatrixId } diff --git a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutFragment.kt b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutFragment.kt index d3288c5b2e..13b90f26e8 100644 --- a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutFragment.kt @@ -30,7 +30,7 @@ import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.features.login.AbstractLoginFragment import im.vector.riotx.features.login.LoginAction import im.vector.riotx.features.login.LoginMode -import im.vector.riotx.features.login.LoginNavigation +import im.vector.riotx.features.login.LoginViewEvents import kotlinx.android.synthetic.main.fragment_generic_recycler.* import javax.inject.Inject @@ -94,7 +94,7 @@ class SoftLogoutFragment @Inject constructor( } override fun signinFallbackSubmit() { - loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnSignModeSelected)) } override fun clearData() { @@ -124,7 +124,7 @@ class SoftLogoutFragment @Inject constructor( } override fun forgetPasswordClicked() { - loginSharedActionViewModel.post(LoginNavigation.OnForgetPasswordClicked) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnForgetPasswordClicked)) } override fun revealPasswordClicked() { diff --git a/vector/src/main/res/layout/fragment_login_server_selection.xml b/vector/src/main/res/layout/fragment_login_server_selection.xml index c97b32bd21..ba74ce26f8 100644 --- a/vector/src/main/res/layout/fragment_login_server_selection.xml +++ b/vector/src/main/res/layout/fragment_login_server_selection.xml @@ -16,7 +16,9 @@ style="@style/LoginFormScrollView" tools:ignore="MissingConstraints"> - + + + + + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 585102d46a..dd1043819d 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -57,4 +57,12 @@ + Alternatively, if you already have an account and you know your Matrix identifier and your password, you can use this method: + Sign in with my Matrix identifier + Sign in + Enter your identifier and your password + User identifier + This is not a valid user identifier. Expected format: \'@user:homeserver.org\' + Unable to find a valid homeserver. Please check your identifier +