Login screens: ensure homeserver version is supported

This commit is contained in:
Benoit Marty 2019-11-26 11:37:51 +01:00
parent 3f1540b54e
commit 9b207dd5dc
24 changed files with 228 additions and 58 deletions

View file

@ -19,12 +19,12 @@ package im.vector.matrix.android.api.auth
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.auth.data.LoginFlowResult
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.auth.login.LoginWizard
import im.vector.matrix.android.api.auth.registration.RegistrationWizard
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
/**
* This interface defines methods to authenticate or to create an account to a matrix server.
@ -35,7 +35,7 @@ interface AuthenticationService {
* Request the supported login flows for this homeserver.
* This is the first method to call to be able to get a wizard to login or the create an account
*/
fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResponse>): Cancelable
fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResult>): Cancelable
/**
* Return a LoginWizard, to login to the homeserver

View file

@ -0,0 +1,25 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.auth.data
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
// Either a LoginFlowResponse, or an error if the homeserver is outdated
sealed class LoginFlowResult {
data class Success(val loginFlowResponse: LoginFlowResponse) : LoginFlowResult()
object OutdatedHomeserver : LoginFlowResult()
}

View file

@ -0,0 +1,106 @@
/*
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.auth.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Model for https://matrix.org/docs/spec/client_server/latest#get-matrix-client-versions
*
* Ex:
* <pre>
* {
* "unstable_features": {
* "m.lazy_load_members": true
* },
* "versions": [
* "r0.0.1",
* "r0.1.0",
* "r0.2.0",
* "r0.3.0"
* ]
* }
* </pre>
*/
@JsonClass(generateAdapter = true)
data class Versions(
@Json(name = "versions")
val supportedVersions: List<String>? = null,
@Json(name = "unstable_features")
val unstableFeatures: Map<String, Boolean>? = null
)
// MatrixClientServerAPIVersion
private const val r0_0_1 = "r0_0_1"
private const val r0_1_0 = "r0_1_0"
private const val r0_2_0 = "r0_2_0"
private const val r0_3_0 = "r0_3_0"
private const val r0_4_0 = "r0_4_0"
private const val r0_5_0 = "r0_5_0"
private const val r0_6_0 = "r0_6_0"
// MatrixVersionsFeature
private const val FEATURE_LAZY_LOAD_MEMBERS = "m.lazy_load_members"
private const val FEATURE_REQUIRE_IDENTITY_SERVER = "m.require_identity_server"
private const val FEATURE_ID_ACCESS_TOKEN = "m.id_access_token"
private const val FEATURE_SEPARATE_ADD_AND_BIND = "m.separate_add_and_bind"
/**
* Return true if the SDK supports this homeserver version
*/
fun Versions.isSupportedBySdk(): Boolean {
return supportLazyLoadMembers()
&& !doesServerRequireIdentityServerParam()
&& doesServerAcceptIdentityAccessToken()
&& doesServerSeparatesAddAndBind()
}
/**
* Return true if the server support the lazy loading of room members
*
* @return true if the server support the lazy loading of room members
*/
private fun Versions.supportLazyLoadMembers(): Boolean {
return supportedVersions?.contains(r0_5_0) == true
|| unstableFeatures?.get(FEATURE_LAZY_LOAD_MEMBERS) == true
}
/**
* Indicate if the `id_server` parameter is required when registering with an 3pid,
* adding a 3pid or resetting password.
*/
private fun Versions.doesServerRequireIdentityServerParam(): Boolean {
if (supportedVersions?.contains(r0_6_0) == true) return false
return unstableFeatures?.get(FEATURE_REQUIRE_IDENTITY_SERVER) ?: true
}
/**
* Indicate if the `id_access_token` parameter can be safely passed to the homeserver.
* Some homeservers may trigger errors if they are not prepared for the new parameter.
*/
private fun Versions.doesServerAcceptIdentityAccessToken(): Boolean {
return supportedVersions?.contains(r0_6_0) == true
|| unstableFeatures?.get(FEATURE_ID_ACCESS_TOKEN) ?: false
}
private fun Versions.doesServerSeparatesAddAndBind(): Boolean {
return supportedVersions?.contains(r0_6_0) == true
|| unstableFeatures?.get(FEATURE_SEPARATE_ADD_AND_BIND) ?: false
}

View file

@ -17,10 +17,11 @@
package im.vector.matrix.android.internal.auth
import im.vector.matrix.android.api.auth.data.Credentials
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.registration.*
import im.vector.matrix.android.internal.auth.login.ResetPasswordMailConfirmed
import im.vector.matrix.android.internal.auth.registration.*
import im.vector.matrix.android.internal.network.NetworkConstants
import retrofit2.Call
import retrofit2.http.*
@ -30,6 +31,12 @@ import retrofit2.http.*
*/
internal interface AuthAPI {
/**
* Get the version information of the homeserver
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_ + "versions")
fun versions(): Call<Versions>
/**
* Register to the homeserver
* Ref: https://matrix.org/docs/spec/client_server/latest#account-registration-and-management

View file

@ -19,9 +19,7 @@ package im.vector.matrix.android.internal.auth
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.auth.data.*
import im.vector.matrix.android.api.auth.login.LoginWizard
import im.vector.matrix.android.api.auth.registration.RegistrationWizard
import im.vector.matrix.android.api.session.Session
@ -68,7 +66,7 @@ internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated
return sessionManager.getOrCreateSession(sessionParams)
}
override fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResponse>): Cancelable {
override fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResult>): Cancelable {
currentHomeServerConnectionConfig = null
return GlobalScope.launch(coroutineDispatchers.main) {
@ -77,8 +75,10 @@ internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated
}
result.fold(
{
// The homeserver exists, keep the config
currentHomeServerConnectionConfig = homeServerConnectionConfig
if (it is LoginFlowResult.Success) {
// The homeserver exists and up to date, keep the config
currentHomeServerConnectionConfig = homeServerConnectionConfig
}
callback.onSuccess(it)
},
{
@ -92,8 +92,20 @@ internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated
private suspend fun getLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig) = withContext(coroutineDispatchers.io) {
val authAPI = buildAuthAPI(homeServerConnectionConfig)
executeRequest<LoginFlowResponse> {
apiCall = authAPI.getLoginFlows()
// First check the homeserver version
val versions = executeRequest<Versions> {
apiCall = authAPI.versions()
}
if (versions.isSupportedBySdk()) {
// Get the login flow
val loginFlowResponse = executeRequest<LoginFlowResponse> {
apiCall = authAPI.getLoginFlows()
}
LoginFlowResult.Success(loginFlowResponse)
} else {
// Not supported
LoginFlowResult.OutdatedHomeserver
}
}

View file

@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.network
internal object NetworkConstants {
private const val URI_API_PREFIX_PATH = "_matrix/client"
const val URI_API_PREFIX_PATH_ = "$URI_API_PREFIX_PATH/"
const val URI_API_PREFIX_PATH_R0 = "$URI_API_PREFIX_PATH/r0/"
const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/"

View file

@ -52,14 +52,14 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed {
private fun handleLoginViewEvents(loginViewEvents: LoginViewEvents) {
when (loginViewEvents) {
is LoginViewEvents.RegistrationFlowResult ->
is LoginViewEvents.Error -> showError(loginViewEvents.throwable)
else ->
// This is handled by the Activity
Unit
is LoginViewEvents.RegistrationError -> displayRegistrationError(loginViewEvents.throwable)
}
}
private fun displayRegistrationError(throwable: Throwable) {
private fun showError(throwable: Throwable) {
when (throwable) {
is Failure.ServerError -> {
if (throwable.error.code == MatrixError.FORBIDDEN
@ -70,14 +70,14 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed {
.setPositiveButton(R.string.ok, null)
.show()
} else {
onRegistrationError(throwable)
onError(throwable)
}
}
else -> onRegistrationError(throwable)
else -> onError(throwable)
}
}
abstract fun onRegistrationError(throwable: Throwable)
abstract fun onError(throwable: Throwable)
override fun onBackPressed(toolbarButton: Boolean): Boolean {
return when {

View file

@ -141,7 +141,7 @@ class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
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
@ -160,7 +160,13 @@ class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
}
}
}
is LoginViewEvents.RegistrationError ->
is LoginViewEvents.OutdatedHomeserver ->
AlertDialog.Builder(this)
.setTitle(R.string.dialog_title_error)
.setMessage(R.string.login_error_outdated_homeserver)
.setPositiveButton(R.string.ok, null)
.show()
is LoginViewEvents.Error ->
// This is handled by the Fragments
Unit
}

View file

@ -178,8 +178,7 @@ class LoginCaptchaFragment @Inject constructor(
}
}
override fun onRegistrationError(throwable: Throwable) {
// Cannot happen here, but just in case
override fun onError(throwable: Throwable) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))

View file

@ -189,7 +189,7 @@ class LoginFragment @Inject constructor(
loginViewModel.handle(LoginAction.ResetLogin)
}
override fun onRegistrationError(throwable: Throwable) {
override fun onError(throwable: Throwable) {
loginFieldTil.error = errorFormatter.toHumanReadable(throwable)
}

View file

@ -213,7 +213,7 @@ class LoginGenericTextInputFormFragment @Inject constructor(private val errorFor
}
}
override fun onRegistrationError(throwable: Throwable) {
override fun onError(throwable: Throwable) {
when (params.mode) {
TextInputFormFragmentMode.SetEmail -> {
if (throwable.is401()) {

View file

@ -140,8 +140,7 @@ class LoginResetPasswordFragment @Inject constructor(
loginViewModel.handle(LoginAction.ResetResetPassword)
}
override fun onRegistrationError(throwable: Throwable) {
// TODO
override fun onError(throwable: Throwable) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))

View file

@ -53,8 +53,12 @@ class LoginResetPasswordMailConfirmationFragment @Inject constructor(
loginViewModel.handle(LoginAction.ResetPasswordMailConfirmed)
}
override fun onRegistrationError(throwable: Throwable) {
// No op
override fun onError(throwable: Throwable) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))
.setPositiveButton(R.string.ok, null)
.show()
}
override fun resetViewModel() {

View file

@ -36,8 +36,7 @@ class LoginResetPasswordSuccessFragment @Inject constructor(
loginSharedActionViewModel.post(LoginNavigation.OnResetPasswordMailConfirmationSuccessDone)
}
override fun onRegistrationError(throwable: Throwable) {
// Cannot happen here, but just in case
override fun onError(throwable: Throwable) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))

View file

@ -123,8 +123,7 @@ class LoginServerSelectionFragment @Inject constructor(
loginViewModel.handle(LoginAction.ResetHomeServerType)
}
override fun onRegistrationError(throwable: Throwable) {
// Cannot happen here, but just in case
override fun onError(throwable: Throwable) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))

View file

@ -125,8 +125,7 @@ class LoginServerUrlFormFragment @Inject constructor(
loginServerUrlFormHomeServerUrlTil.error = null
}
override fun onRegistrationError(throwable: Throwable) {
// Cannot happen here, but just in case
override fun onError(throwable: Throwable) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))

View file

@ -95,8 +95,7 @@ class LoginSignUpSignInSelectionFragment @Inject constructor(
loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected)
}
override fun onRegistrationError(throwable: Throwable) {
// Cannot happen here, but just in case
override fun onError(throwable: Throwable) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))

View file

@ -36,8 +36,7 @@ class LoginSplashFragment @Inject constructor(
loginSharedActionViewModel.post(LoginNavigation.OpenServerSelection)
}
override fun onRegistrationError(throwable: Throwable) {
// Cannot happen here, but just in case
override fun onError(throwable: Throwable) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))

View file

@ -24,5 +24,6 @@ import im.vector.matrix.android.api.auth.registration.FlowResult
*/
sealed class LoginViewEvents {
data class RegistrationFlowResult(val flowResult: FlowResult) : LoginViewEvents()
data class RegistrationError(val throwable: Throwable) : LoginViewEvents()
data class Error(val throwable: Throwable) : LoginViewEvents()
object OutdatedHomeserver : LoginViewEvents()
}

View file

@ -23,6 +23,7 @@ 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
@ -31,7 +32,6 @@ import im.vector.matrix.android.api.auth.registration.Stage
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.MatrixCallbackDelegate
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.extensions.configureAndStart
@ -166,7 +166,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
override fun onFailure(failure: Throwable) {
if (failure !is CancellationException) {
_viewEvents.post(LoginViewEvents.RegistrationError(failure))
_viewEvents.post(LoginViewEvents.Error(failure))
}
setState {
copy(
@ -188,7 +188,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
}
override fun onFailure(failure: Throwable) {
_viewEvents.post(LoginViewEvents.RegistrationError(failure))
_viewEvents.post(LoginViewEvents.Error(failure))
setState {
copy(
asyncRegistration = Uninitialized
@ -210,7 +210,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
}
override fun onFailure(failure: Throwable) {
_viewEvents.post(LoginViewEvents.RegistrationError(failure))
_viewEvents.post(LoginViewEvents.Error(failure))
setState {
copy(
asyncRegistration = Uninitialized
@ -525,8 +525,9 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
)
}
currentTask = authenticationService.getLoginFlow(homeServerConnectionConfigFinal, object : MatrixCallback<LoginFlowResponse> {
currentTask = authenticationService.getLoginFlow(homeServerConnectionConfigFinal, object : MatrixCallback<LoginFlowResult> {
override fun onFailure(failure: Throwable) {
_viewEvents.post(LoginViewEvents.Error(failure))
setState {
copy(
asyncHomeServerLoginFlowRequest = Fail(failure)
@ -534,18 +535,32 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
}
}
override fun onSuccess(data: LoginFlowResponse) {
val loginMode = when {
// SSO login is taken first
data.flows.any { it.type == LoginFlowTypes.SSO } -> LoginMode.Sso
data.flows.any { it.type == LoginFlowTypes.PASSWORD } -> LoginMode.Password
else -> LoginMode.Unsupported(data.flows.mapNotNull { it.type }.toList())
}
override fun onSuccess(data: LoginFlowResult) {
when (data) {
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.loginFlowResponse.flows.mapNotNull { it.type }.toList())
}
setState {
copy(
asyncHomeServerLoginFlowRequest = Success(loginMode)
)
setState {
copy(
asyncHomeServerLoginFlowRequest = Success(loginMode)
)
}
}
is LoginFlowResult.OutdatedHomeserver -> {
// Notify the UI
_viewEvents.post(LoginViewEvents.OutdatedHomeserver)
setState {
copy(
asyncHomeServerLoginFlowRequest = Uninitialized
)
}
}
}
}
})

View file

@ -64,7 +64,7 @@ class LoginWaitForEmailFragment @Inject constructor(private val errorFormatter:
loginWaitForEmailNotice.text = getString(R.string.login_wait_for_email_notice, params.email)
}
override fun onRegistrationError(throwable: Throwable) {
override fun onError(throwable: Throwable) {
if (throwable.is401()) {
// Try again, with a delay
loginViewModel.handle(LoginAction.CheckIfEmailHasBeenValidated(10_000))

View file

@ -245,8 +245,7 @@ class LoginWebFragment @Inject constructor(
loginViewModel.handle(LoginAction.ResetLogin)
}
override fun onRegistrationError(throwable: Throwable) {
// Cannot happen here, but just in case
override fun onError(throwable: Throwable) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))

View file

@ -101,7 +101,7 @@ class LoginTermsFragment @Inject constructor(
loginViewModel.handle(LoginAction.AcceptTerms)
}
override fun onRegistrationError(throwable: Throwable) {
override fun onError(throwable: Throwable) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))

View file

@ -129,6 +129,7 @@
<string name="login_wait_for_email_title">Please check your email</string>
<string name="login_wait_for_email_notice">We just sent an email to %1$s.\nPlease click on the link it contains to continue the account creation.</string>
<string name="login_validation_code_is_not_correct">The entered code is not correct. Please check.</string>
<string name="login_error_outdated_homeserver">The homeserver version is outdated, RiotX supports only recent versions of homeserver.\n\nYou may contact the administrator of the homeserver to request for an upgrade of the homeserver.</string>
<plurals name="login_error_limit_exceeded_retry_after">
<item quantity="one">Too many requests have been sent. You can retry in %1$d second…</item>