Merge branch 'develop' into feature/power_level

This commit is contained in:
ganfra 2020-06-10 17:21:04 +02:00
commit 579d4f7a5b
47 changed files with 575 additions and 116 deletions

View file

@ -10,6 +10,9 @@ Improvements 🙌:
- Hide "X made no changes" event by default in timeline (#1430)
- Hide left rooms in breadcrumbs (#766)
- Handle PowerLevel properly (#627)
- Correctly handle SSO login redirection
- SSO login is now performed in the default browser, or in Chrome Custom tab if available (#1400)
- Improve checking of homeserver version support (#1442)
Bugfix 🐛:
- Switch theme is not fully taken into account without restarting the app
@ -17,6 +20,8 @@ Bugfix 🐛:
- Reply composer overlay stays on screen too long after send (#1169)
- Fix navigation bar icon contrast on API in [21,27[ (#1342)
- Fix status bar icon contrast on API in [21,23[
- Wrong /query request (#1444)
- Make Credentials.homeServer optional because it is deprecated (#1443)
Translations 🗣:
-
@ -29,6 +34,7 @@ Build 🧱:
Other changes:
- Send plain text in the body of events containing formatted body, as per https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes
- Update link to Modular url from "https://modular.im/" to "https://modular.im/services/matrix-hosting-riot" and open it using ChromeCustomTab
Changes in RiotX 0.21.0 (2020-05-28)
===================================================

View file

@ -2,7 +2,7 @@
This document describes the flow of signin to a homeserver, and also the flow when user want to reset his password. Examples come from the `matrix.org` homeserver.
## Sign up flows
## Sign in flows
### Get the flow
@ -58,7 +58,7 @@ We get credential (200)
```json
{
"user_id": "@alice:matrix.org",
"access_token": "MDAxOGxvY2F0aW9uIG1hdHREDACTEDb2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gfnYrSypfdTtkNXIuNWx1KgowMDJmc2lnbmF0dXJlIOsh1XqeAkXexh4qcofl_aR4kHJoSOWYGOhE7-ubX-DZCg",
"access_token": "MDAxOGxvY2F0aW9uIG1hdHREDACTEDb2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lr",
"home_server": "matrix.org",
"device_id": "GTVREDALBF",
"well_known": {
@ -117,7 +117,7 @@ We get the credentials (200)
```json
{
"user_id": "@alice:matrix.org",
"access_token": "MDAxOGxvY2F0aW9uIG1hdHJpeC5vcmREDACTEDZXJfaWQgPSBAYmVub2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gNjtDY0MwRlNPSFFoOC5wOgowMDJmc2lnbmF0dXJlIGiTRm1mYLLxQywxOh3qzQVT8HoEorSokEP2u-bAwtnYCg",
"access_token": "MDAxOGxvY2F0aW9uIG1hdHJpeC5vcmREDACTEDZXJfaWQgPSBAYmVub2l0MDgxNjptYXRyaXgub3Jnfrfdegfszsefddvf",
"home_server": "matrix.org",
"device_id": "WBSREDASND",
"well_known": {
@ -145,12 +145,59 @@ Not supported yet in RiotX
"flows": [
{
"type": "m.login.sso"
},
{
"type": "m.login.token"
}
]
}
```
In this case, the user can click on "Sign in with SSO" and the web screen will be displayed on the page `https://homeserver.with.sso/_matrix/static/client/login/` and the credentials will be passed back to the native code through the JS bridge
In this case, the user can click on "Sign in with SSO" and the native web browser, or a ChromeCustomTab if the device supports it, will be launched on the page
> https://homeserver.with.sso/_matrix/client/r0/login/sso/redirect?redirectUrl=riotx%3A%2F%2Friotx
The parameter `redirectUrl` is set to `riotx://riotx`.
ChromeCustomTabs are an intermediate way to display a WebPage, between a WebView and using the external browser. More info can be found [here](https://developer.chrome.com/multidevice/android/customtabs)
The browser will then take care of the SSO login, which may include creating a third party account, entering an email, settings a display name, or any other possibilities.
During the process, user may be asked to validate an email by clicking on a link it contains. The link has to be opened in the browser which initiates the authentication. This is why we cannot use WebView anymore.
Once the process is finished, the web page will call the `redirectUrl` with an extra parameter `loginToken`
> riotx://riotx?loginToken=MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy
This navigation is intercepted by RiotX by the `LoginActivity`, which will then ask the homeserver to convert this `loginToken` to an access token
> curl -X POST --data $'{"type":"m.login.token","token":"MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy"}' 'https://homeserver.with.sso/_matrix/client/r0/login'
```json
{
"type": "m.login.token",
"token": "MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy"
}
```
We get the credentials (200)
```json
{
"user_id": "@alice:homeserver.with.sso",
"access_token": "MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVyIGtleQowMDEwY2lkIGdlbiA9IDEKMDAyY2NpZCB1c2",
"home_server": "homeserver.with.sso",
"device_id": "DETBTVAHCH",
"well_known": {
"m.homeserver": {
"base_url": "https:\/\/homeserver.with.sso\/"
},
"m.identity_server": {
"base_url": "https:\/\/vector.im"
}
}
}
```
## Reset password

View file

@ -32,6 +32,6 @@ const val REGISTER_FALLBACK_PATH = "/_matrix/static/client/register/"
* Path to use when the client want to connect using SSO
* Ref: https://matrix.org/docs/spec/client_server/latest#sso-client-login
*/
const val SSO_FALLBACK_PATH = "/_matrix/client/r0/login/sso/redirect"
const val SSO_REDIRECT_PATH = "/_matrix/client/r0/login/sso/redirect"
const val SSO_REDIRECT_URL_PARAM = "redirectUrl"

View file

@ -45,7 +45,7 @@ data class Credentials(
* @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,
@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.
*/

View file

@ -16,12 +16,10 @@
package im.vector.matrix.android.api.auth.data
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
// Either a LoginFlowResponse, or an error if the homeserver is outdated
// Either a list of supported login types, or an error if the homeserver is outdated
sealed class LoginFlowResult {
data class Success(
val loginFlowResponse: LoginFlowResponse,
val supportedLoginTypes: List<String>,
val isLoginAndRegistrationSupported: Boolean,
val homeServerUrl: String
) : LoginFlowResult()

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
package im.vector.matrix.android.internal.auth.data
package im.vector.matrix.android.api.auth.data
object LoginFlowTypes {
const val PASSWORD = "m.login.password"

View file

@ -34,6 +34,12 @@ interface LoginWizard {
deviceName: String,
callback: MatrixCallback<Session>): Cancelable
/**
* Exchange a login token to an access token
*/
fun loginWithToken(loginToken: String,
callback: MatrixCallback<Session>): Cancelable
/**
* Reset user password
*/

View file

@ -21,15 +21,12 @@ sealed class Stage(open val mandatory: Boolean) {
// m.login.recaptcha
data class ReCaptcha(override val mandatory: Boolean, val publicKey: String) : Stage(mandatory)
// m.login.oauth2
// m.login.email.identity
data class Email(override val mandatory: Boolean) : Stage(mandatory)
// m.login.msisdn
data class Msisdn(override val mandatory: Boolean) : Stage(mandatory)
// m.login.token
// m.login.dummy, can be mandatory if there is no other stages. In this case the account cannot be created by just sending a username
// and a password, the dummy stage has to be done
data class Dummy(override val mandatory: Boolean) : Stage(mandatory)

View file

@ -17,16 +17,17 @@
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.data.RiotConfig
import im.vector.matrix.android.internal.auth.data.TokenLoginParams
import im.vector.matrix.android.internal.auth.login.ResetPasswordMailConfirmed
import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationParams
import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationResponse
import im.vector.matrix.android.internal.auth.registration.RegistrationParams
import im.vector.matrix.android.internal.auth.registration.SuccessResult
import im.vector.matrix.android.internal.auth.registration.ValidationCodeBody
import im.vector.matrix.android.internal.auth.version.Versions
import im.vector.matrix.android.internal.network.NetworkConstants
import retrofit2.Call
import retrofit2.http.Body
@ -54,7 +55,7 @@ internal interface AuthAPI {
fun versions(): Call<Versions>
/**
* Register to the homeserver
* Register to the homeserver, or get error 401 with a RegistrationFlowResponse object if registration is incomplete
* Ref: https://matrix.org/docs/spec/client_server/latest#account-registration-and-management
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register")
@ -91,6 +92,11 @@ internal interface AuthAPI {
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login")
fun login(@Body loginParams: PasswordLoginParams): Call<Credentials>
// Unfortunately we cannot use interface for @Body parameter, so I duplicate the method for the type TokenLoginParams
@Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000")
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login")
fun login(@Body loginParams: TokenLoginParams): Call<Credentials>
/**
* Ask the homeserver to reset the password associated with the provided email.
*/

View file

@ -23,9 +23,6 @@ import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.auth.data.LoginFlowResult
import im.vector.matrix.android.api.auth.data.Versions
import im.vector.matrix.android.api.auth.data.isLoginAndRegistrationSupportedBySdk
import im.vector.matrix.android.api.auth.data.isSupportedBySdk
import im.vector.matrix.android.api.auth.login.LoginWizard
import im.vector.matrix.android.api.auth.registration.RegistrationWizard
import im.vector.matrix.android.api.auth.wellknown.WellknownResult
@ -40,6 +37,9 @@ import im.vector.matrix.android.internal.auth.db.PendingSessionData
import im.vector.matrix.android.internal.auth.login.DefaultLoginWizard
import im.vector.matrix.android.internal.auth.login.DirectLoginTask
import im.vector.matrix.android.internal.auth.registration.DefaultRegistrationWizard
import im.vector.matrix.android.internal.auth.version.Versions
import im.vector.matrix.android.internal.auth.version.isLoginAndRegistrationSupportedBySdk
import im.vector.matrix.android.internal.auth.version.isSupportedBySdk
import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.network.executeRequest
@ -236,7 +236,7 @@ internal class DefaultAuthenticationService @Inject constructor(
val loginFlowResponse = executeRequest<LoginFlowResponse>(null) {
apiCall = authAPI.getLoginFlows()
}
LoginFlowResult.Success(loginFlowResponse, versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl)
LoginFlowResult.Success(loginFlowResponse.flows.orEmpty().mapNotNull { it.type }, versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl)
} else {
// Not supported
LoginFlowResult.OutdatedHomeserver

View file

@ -20,7 +20,19 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class LoginFlowResponse(
internal data class LoginFlowResponse(
/**
* The homeserver's supported login types
*/
@Json(name = "flows")
val flows: List<InteractiveAuthenticationFlow>
val flows: List<LoginFlow>?
)
@JsonClass(generateAdapter = true)
internal data class LoginFlow(
/**
* The login type. This is supplied as the type when logging in.
*/
@Json(name = "type")
val type: String?
)

View file

@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.auth.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
/**
* Ref:

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.auth.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
@JsonClass(generateAdapter = true)
internal data class TokenLoginParams(
@Json(name = "type") override val type: String = LoginFlowTypes.TOKEN,
@Json(name = "token") val token: String
) : LoginParams

View file

@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.auth.PendingSessionStore
import im.vector.matrix.android.internal.auth.SessionCreator
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
import im.vector.matrix.android.internal.auth.data.ThreePidMedium
import im.vector.matrix.android.internal.auth.data.TokenLoginParams
import im.vector.matrix.android.internal.auth.db.PendingSessionData
import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationParams
import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationResponse
@ -65,6 +66,22 @@ internal class DefaultLoginWizard(
}
}
/**
* Ref: https://matrix.org/docs/spec/client_server/latest#handling-the-authentication-endpoint
*/
override fun loginWithToken(loginToken: String, callback: MatrixCallback<Session>): Cancelable {
return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) {
val loginParams = TokenLoginParams(
token = loginToken
)
val credentials = executeRequest<Credentials>(null) {
apiCall = authAPI.login(loginParams)
}
sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig)
}
}
private suspend fun loginInternal(login: String,
password: String,
deviceName: String) = withContext(coroutineDispatchers.computation) {

View file

@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.auth.registration
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
/**
* Open class, parent to all possible authentication parameters

View file

@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.auth.registration
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
import im.vector.matrix.android.api.auth.registration.RegisterThreePid
import im.vector.matrix.android.api.auth.registration.RegistrationResult
import im.vector.matrix.android.api.auth.registration.RegistrationWizard
@ -28,7 +29,6 @@ import im.vector.matrix.android.api.util.NoOpCancellable
import im.vector.matrix.android.internal.auth.AuthAPI
import im.vector.matrix.android.internal.auth.PendingSessionStore
import im.vector.matrix.android.internal.auth.SessionCreator
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.auth.db.PendingSessionData
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.task.launchToCallback

View file

@ -18,12 +18,12 @@ package im.vector.matrix.android.internal.auth.registration
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
import im.vector.matrix.android.api.auth.registration.FlowResult
import im.vector.matrix.android.api.auth.registration.Stage
import im.vector.matrix.android.api.auth.registration.TermPolicies
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.auth.data.InteractiveAuthenticationFlow
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
@JsonClass(generateAdapter = true)
data class RegistrationFlowResponse(

View file

@ -0,0 +1,60 @@
/*
* 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.version
/**
* Values will take the form "rX.Y.Z".
* Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-versions
*/
internal data class HomeServerVersion(
val major: Int,
val minor: Int,
val patch: Int
) : Comparable<HomeServerVersion> {
override fun compareTo(other: HomeServerVersion): Int {
return when {
major > other.major -> 1
major < other.major -> -1
minor > other.minor -> 1
minor < other.minor -> -1
patch > other.patch -> 1
patch < other.patch -> -1
else -> 0
}
}
companion object {
internal val pattern = Regex("""r(\d+)\.(\d+)\.(\d+)""")
internal fun parse(value: String): HomeServerVersion? {
val result = pattern.matchEntire(value) ?: return null
return HomeServerVersion(
major = result.groupValues[1].toInt(),
minor = result.groupValues[2].toInt(),
patch = result.groupValues[3].toInt()
)
}
val r0_0_0 = HomeServerVersion(major = 0, minor = 0, patch = 0)
val r0_1_0 = HomeServerVersion(major = 0, minor = 1, patch = 0)
val r0_2_0 = HomeServerVersion(major = 0, minor = 2, patch = 0)
val r0_3_0 = HomeServerVersion(major = 0, minor = 3, patch = 0)
val r0_4_0 = HomeServerVersion(major = 0, minor = 4, patch = 0)
val r0_5_0 = HomeServerVersion(major = 0, minor = 5, patch = 0)
val r0_6_0 = HomeServerVersion(major = 0, minor = 6, patch = 0)
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright 2018 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,7 +14,7 @@
* limitations under the License.
*/
package im.vector.matrix.android.api.auth.data
package im.vector.matrix.android.internal.auth.version
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@ -38,7 +38,7 @@ import com.squareup.moshi.JsonClass
* </pre>
*/
@JsonClass(generateAdapter = true)
data class Versions(
internal data class Versions(
@Json(name = "versions")
val supportedVersions: List<String>? = null,
@ -46,15 +46,6 @@ data class Versions(
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"
@ -64,14 +55,14 @@ 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 {
internal fun Versions.isSupportedBySdk(): Boolean {
return supportLazyLoadMembers()
}
/**
* Return true if the SDK supports this homeserver version for login and registration
*/
fun Versions.isLoginAndRegistrationSupportedBySdk(): Boolean {
internal fun Versions.isLoginAndRegistrationSupportedBySdk(): Boolean {
return !doesServerRequireIdentityServerParam()
&& doesServerAcceptIdentityAccessToken()
&& doesServerSeparatesAddAndBind()
@ -83,7 +74,7 @@ fun Versions.isLoginAndRegistrationSupportedBySdk(): Boolean {
* @return true if the server support the lazy loading of room members
*/
private fun Versions.supportLazyLoadMembers(): Boolean {
return supportedVersions?.contains(r0_5_0) == true
return getMaxVersion() >= HomeServerVersion.r0_5_0
|| unstableFeatures?.get(FEATURE_LAZY_LOAD_MEMBERS) == true
}
@ -92,7 +83,7 @@ private fun Versions.supportLazyLoadMembers(): Boolean {
* adding a 3pid or resetting password.
*/
private fun Versions.doesServerRequireIdentityServerParam(): Boolean {
if (supportedVersions?.contains(r0_6_0) == true) return false
if (getMaxVersion() >= HomeServerVersion.r0_6_0) return false
return unstableFeatures?.get(FEATURE_REQUIRE_IDENTITY_SERVER) ?: true
}
@ -101,11 +92,18 @@ private fun Versions.doesServerRequireIdentityServerParam(): Boolean {
* 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
return getMaxVersion() >= HomeServerVersion.r0_6_0
|| unstableFeatures?.get(FEATURE_ID_ACCESS_TOKEN) ?: false
}
private fun Versions.doesServerSeparatesAddAndBind(): Boolean {
return supportedVersions?.contains(r0_6_0) == true
return getMaxVersion() >= HomeServerVersion.r0_6_0
|| unstableFeatures?.get(FEATURE_SEPARATE_ADD_AND_BIND) ?: false
}
private fun Versions.getMaxVersion(): HomeServerVersion {
return supportedVersions
?.mapNotNull { HomeServerVersion.parse(it) }
?.max()
?: HomeServerVersion.r0_0_0
}

View file

@ -36,7 +36,7 @@ internal data class KeysQueryBody(
* A map from user ID, to a list of device IDs, or to an empty list to indicate all devices for the corresponding user.
*/
@Json(name = "device_keys")
val deviceKeys: Map<String, Any>,
val deviceKeys: Map<String, List<String>>,
/**
* If the client is fetching keys as a result of a device update received in a sync request, this should be the 'since' token

View file

@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.crypto.model.rest
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
/**
* This class provides the authentication data by using user and password

View file

@ -16,7 +16,7 @@
package im.vector.matrix.android.internal.crypto.tasks
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.crypto.api.CryptoApi
import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceParams
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth

View file

@ -27,7 +27,7 @@ import javax.inject.Inject
internal interface DownloadKeysForUsersTask : Task<DownloadKeysForUsersTask.Params, KeysQueryResponse> {
data class Params(
// the list of users to get keys for.
val userIds: List<String>?,
val userIds: List<String>,
// the up-to token
val token: String?
)
@ -39,7 +39,7 @@ internal class DefaultDownloadKeysForUsers @Inject constructor(
) : DownloadKeysForUsersTask {
override suspend fun execute(params: DownloadKeysForUsersTask.Params): KeysQueryResponse {
val downloadQuery = params.userIds?.associateWith { emptyMap<String, Any>() }.orEmpty()
val downloadQuery = params.userIds.associateWith { emptyList<String>() }
val body = KeysQueryBody(
deviceKeys = downloadQuery,

View file

@ -16,7 +16,7 @@
package im.vector.matrix.android.internal.session.homeserver
import im.vector.matrix.android.api.auth.data.Versions
import im.vector.matrix.android.internal.auth.version.Versions
import im.vector.matrix.android.internal.network.NetworkConstants
import retrofit2.Call
import retrofit2.http.GET

View file

@ -17,10 +17,10 @@
package im.vector.matrix.android.internal.session.homeserver
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.auth.data.Versions
import im.vector.matrix.android.api.auth.data.isLoginAndRegistrationSupportedBySdk
import im.vector.matrix.android.api.auth.wellknown.WellknownResult
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities
import im.vector.matrix.android.internal.auth.version.Versions
import im.vector.matrix.android.internal.auth.version.isLoginAndRegistrationSupportedBySdk
import im.vector.matrix.android.internal.database.model.HomeServerCapabilitiesEntity
import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.di.UserId

View file

@ -27,7 +27,7 @@ internal interface WidgetsAPI {
/**
* register to the server
*
* @param requestOpenIdTokenResponse the body content (Ref: https://github.com/matrix-org/matrix-doc/pull/1961)
* @param body the body content (Ref: https://github.com/matrix-org/matrix-doc/pull/1961)
*/
@POST("register")
fun register(@Body body: RequestOpenIdTokenResponse, @Query("v") version: String?): Call<RegisterWidgetResponse>

View file

@ -0,0 +1,57 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.auth.data
import im.vector.matrix.android.internal.auth.version.Versions
import im.vector.matrix.android.internal.auth.version.isSupportedBySdk
import org.amshove.kluent.shouldBe
import org.junit.Test
class VersionsKtTest {
@Test
fun isSupportedBySdkTooLow() {
Versions(supportedVersions = listOf("r0.4.0")).isSupportedBySdk() shouldBe false
Versions(supportedVersions = listOf("r0.4.1")).isSupportedBySdk() shouldBe false
}
@Test
fun isSupportedBySdkUnstable() {
Versions(supportedVersions = listOf("r0.4.0"), unstableFeatures = mapOf("m.lazy_load_members" to true)).isSupportedBySdk() shouldBe true
}
@Test
fun isSupportedBySdkOk() {
Versions(supportedVersions = listOf("r0.5.0")).isSupportedBySdk() shouldBe true
Versions(supportedVersions = listOf("r0.5.1")).isSupportedBySdk() shouldBe true
}
// Was not working
@Test
fun isSupportedBySdkLater() {
Versions(supportedVersions = listOf("r0.6.0")).isSupportedBySdk() shouldBe true
Versions(supportedVersions = listOf("r0.6.1")).isSupportedBySdk() shouldBe true
}
// Cover cases of issue #1442
@Test
fun isSupportedBySdk1442() {
Versions(supportedVersions = listOf("r0.5.0", "r0.6.0")).isSupportedBySdk() shouldBe true
Versions(supportedVersions = listOf("r0.5.0", "r0.6.1")).isSupportedBySdk() shouldBe true
Versions(supportedVersions = listOf("r0.6.0")).isSupportedBySdk() shouldBe true
}
}

View file

@ -332,6 +332,9 @@ dependencies {
implementation 'com.google.android:flexbox:1.1.1'
implementation "androidx.autofill:autofill:$autofill_version"
// Custom Tab
implementation 'androidx.browser:browser:1.2.0'
// Passphrase strength helper
implementation 'com.nulab-inc:zxcvbn:1.2.7'

View file

@ -48,7 +48,19 @@
<activity android:name=".features.home.HomeActivity" />
<activity
android:name=".features.login.LoginActivity"
android:windowSoftInputMode="adjustResize" />
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize">
<!-- Add intent filter to handle redirection URL after SSO login in external browser -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="riotx" />
<data android:host="riotx" />
</intent-filter>
</activity>
<activity android:name=".features.media.ImageMediaViewerActivity" />
<activity android:name=".features.media.BigImageViewerActivity" />
<activity

View file

@ -59,6 +59,7 @@ import im.vector.riotx.features.login.LoginResetPasswordSuccessFragment
import im.vector.riotx.features.login.LoginServerSelectionFragment
import im.vector.riotx.features.login.LoginServerUrlFormFragment
import im.vector.riotx.features.login.LoginSignUpSignInSelectionFragment
import im.vector.riotx.features.login.LoginSignUpSignInSsoFragment
import im.vector.riotx.features.login.LoginSplashFragment
import im.vector.riotx.features.login.LoginWaitForEmailFragment
import im.vector.riotx.features.login.LoginWebFragment
@ -217,6 +218,11 @@ interface FragmentModule {
@FragmentKey(LoginSignUpSignInSelectionFragment::class)
fun bindLoginSignUpSignInSelectionFragment(fragment: LoginSignUpSignInSelectionFragment): Fragment
@Binds
@IntoMap
@FragmentKey(LoginSignUpSignInSsoFragment::class)
fun bindLoginSignUpSignInSsoFragment(fragment: LoginSignUpSignInSsoFragment): Fragment
@Binds
@IntoMap
@FragmentKey(LoginSplashFragment::class)

View file

@ -21,10 +21,14 @@ import android.content.ActivityNotFoundException
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
import android.provider.Browser
import android.provider.MediaStore
import androidx.browser.customtabs.CustomTabsIntent
import androidx.browser.customtabs.CustomTabsSession
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.fragment.app.Fragment
import im.vector.riotx.BuildConfig
@ -64,6 +68,34 @@ fun openUrlInExternalBrowser(context: Context, uri: Uri?) {
}
}
/**
* Open url in custom tab or, if not available, in the default browser
* If several compatible browsers are installed, the user will be proposed to choose one.
* Ref: https://developer.chrome.com/multidevice/android/customtabs
*/
fun openUrlInChromeCustomTab(context: Context, session: CustomTabsSession?, url: String) {
try {
CustomTabsIntent.Builder()
.setToolbarColor(ContextCompat.getColor(context, R.color.riotx_background_light))
.apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setNavigationBarColor(ContextCompat.getColor(context, R.color.riotx_header_panel_background_light))
}
}
.setNavigationBarColor(ContextCompat.getColor(context, R.color.riotx_background_light))
.setColorScheme(CustomTabsIntent.COLOR_SCHEME_LIGHT)
// Note: setting close button icon does not work
.setCloseButtonIcon(BitmapFactory.decodeResource(context.resources, R.drawable.ic_back_24dp))
.setStartAnimations(context, R.anim.enter_fade_in, R.anim.exit_fade_out)
.setExitAnimations(context, R.anim.enter_fade_in, R.anim.exit_fade_out)
.apply { session?.let { setSession(it) } }
.build()
.launchUrl(context, Uri.parse(url))
} catch (activityNotFoundException: ActivityNotFoundException) {
context.toast(R.string.error_no_external_application_found)
}
}
/**
* Open sound recorder external application
*/

View file

@ -16,6 +16,7 @@
package im.vector.riotx.features.crypto.recover
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.api.failure.toRegistrationFlowResponse
@ -28,7 +29,6 @@ import im.vector.matrix.android.api.session.securestorage.EmptyKeySigner
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService
import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo
import im.vector.matrix.android.api.session.securestorage.SsssKeySpec
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion

View file

@ -16,4 +16,7 @@
package im.vector.riotx.features.login
const val MODULAR_LINK = "https://modular.im/?utm_source=riot-x-android&utm_medium=native&utm_campaign=riot-x-android-authentication"
const val MODULAR_LINK = "https://modular.im/services/matrix-hosting-riot" +
"?utm_source=riot-x-android" +
"&utm_medium=native" +
"&utm_campaign=riot-x-android-authentication"

View file

@ -24,6 +24,7 @@ sealed class LoginAction : VectorViewModelAction {
data class UpdateServerType(val serverType: ServerType) : LoginAction()
data class UpdateHomeServer(val homeServerUrl: String) : LoginAction()
data class UpdateSignMode(val signMode: SignMode) : LoginAction()
data class LoginWithToken(val loginToken: String) : LoginAction()
data class WebLoginSuccess(val credentials: Credentials) : LoginAction()
data class InitWith(val loginConfig: LoginConfig) : LoginAction()
data class ResetPassword(val email: String, val newPassword: String) : LoginAction()

View file

@ -33,6 +33,7 @@ import com.airbnb.mvrx.viewModel
import com.airbnb.mvrx.withState
import im.vector.matrix.android.api.auth.registration.FlowResult
import im.vector.matrix.android.api.auth.registration.Stage
import im.vector.matrix.android.api.extensions.tryThis
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.POP_BACK_STACK_EXCLUSIVE
@ -155,7 +156,11 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
is LoginViewEvents.OnSignModeSelected -> onSignModeSelected()
is LoginViewEvents.OnLoginFlowRetrieved ->
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginSignUpSignInSelectionFragment::class.java,
if (loginViewEvents.isSso) {
LoginSignUpSignInSsoFragment::class.java
} else {
LoginSignUpSignInSelectionFragment::class.java
},
option = commonOption)
is LoginViewEvents.OnWebLoginError -> onWebLoginError(loginViewEvents)
is LoginViewEvents.OnForgetPasswordClicked ->
@ -239,16 +244,14 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
SignMode.SignIn -> {
// It depends on the LoginMode
when (state.loginMode) {
LoginMode.Unknown -> error("Developer error")
LoginMode.Unknown,
LoginMode.Sso -> error("Developer error")
LoginMode.Password -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginFragment::class.java,
tag = FRAGMENT_LOGIN_TAG,
option = commonOption)
LoginMode.Sso -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginWebFragment::class.java,
option = commonOption)
LoginMode.Unsupported -> onLoginModeNotSupported(state.loginModeSupportedTypes)
}
}.exhaustive
}
SignMode.SignInWithMatrixId -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginFragment::class.java,
@ -257,6 +260,17 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
}.exhaustive
}
/**
* Handle the SSO redirection here
*/
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
intent?.data
?.let { tryThis { it.getQueryParameter("loginToken") } }
?.let { loginViewModel.handle(LoginAction.LoginWithToken(it)) }
}
private fun onRegistrationStageNotSupported() {
AlertDialog.Builder(this)
.setTitle(R.string.app_name)

View file

@ -21,7 +21,7 @@ import android.view.View
import butterknife.OnClick
import com.airbnb.mvrx.withState
import im.vector.riotx.R
import im.vector.riotx.core.utils.openUrlInExternalBrowser
import im.vector.riotx.core.utils.openUrlInChromeCustomTab
import kotlinx.android.synthetic.main.fragment_login_server_selection.*
import me.gujun.android.span.span
import javax.inject.Inject
@ -55,8 +55,8 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment
}
@OnClick(R.id.loginServerChoiceModularLearnMore)
fun learMore() {
openUrlInExternalBrowser(requireActivity(), MODULAR_LINK)
fun learnMore() {
openUrlInChromeCustomTab(requireActivity(), null, MODULAR_LINK)
}
@OnClick(R.id.loginServerChoiceMatrixOrg)
@ -113,7 +113,7 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment
if (state.loginMode != LoginMode.Unknown) {
// LoginFlow for matrix.org has been retrieved
loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved))
loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved(state.loginMode == LoginMode.Sso)))
}
}
}

View file

@ -26,7 +26,7 @@ import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.riotx.R
import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.core.utils.ensureProtocol
import im.vector.riotx.core.utils.openUrlInExternalBrowser
import im.vector.riotx.core.utils.openUrlInChromeCustomTab
import kotlinx.android.synthetic.main.fragment_login_server_url_form.*
import javax.inject.Inject
@ -84,7 +84,7 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment()
@OnClick(R.id.loginServerUrlFormLearnMore)
fun learnMore() {
openUrlInExternalBrowser(requireActivity(), MODULAR_LINK)
openUrlInChromeCustomTab(requireActivity(), null, MODULAR_LINK)
}
override fun resetViewModel() {
@ -124,7 +124,7 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment()
if (state.loginMode != LoginMode.Unknown) {
// The home server url is valid
loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved))
loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved(state.loginMode == LoginMode.Sso)))
}
}
}

View file

@ -26,13 +26,11 @@ import javax.inject.Inject
/**
* In this screen, the user is asked to sign up or to sign in to the homeserver
*/
class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFragment() {
open class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFragment() {
override fun getLayoutResId() = R.layout.fragment_login_signup_signin_selection
private var isSsoSignIn: Boolean = false
private fun setupUi(state: LoginViewState) {
protected fun setupUi(state: LoginViewState) {
when (state.serverType) {
ServerType.MatrixOrg -> {
loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_matrix_org)
@ -54,25 +52,14 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr
}
}
private fun setupButtons(state: LoginViewState) {
isSsoSignIn = state.loginMode == LoginMode.Sso
if (isSsoSignIn) {
loginSignupSigninSubmit.text = getString(R.string.login_signin_sso)
loginSignupSigninSignIn.isVisible = false
} else {
loginSignupSigninSubmit.text = getString(R.string.login_signup)
loginSignupSigninSignIn.isVisible = true
}
private fun setupButtons() {
loginSignupSigninSubmit.text = getString(R.string.login_signup)
loginSignupSigninSignIn.isVisible = true
}
@OnClick(R.id.loginSignupSigninSubmit)
fun signUp() {
if (isSsoSignIn) {
signIn()
} else {
loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp))
}
open fun submit() {
loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp))
}
@OnClick(R.id.loginSignupSigninSignIn)
@ -86,6 +73,6 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr
override fun updateWithState(state: LoginViewState) {
setupUi(state)
setupButtons(state)
setupButtons()
}
}

View file

@ -0,0 +1,99 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.login
import android.content.ComponentName
import android.net.Uri
import androidx.browser.customtabs.CustomTabsClient
import androidx.browser.customtabs.CustomTabsServiceConnection
import androidx.browser.customtabs.CustomTabsSession
import androidx.core.view.isVisible
import im.vector.riotx.R
import im.vector.riotx.core.utils.openUrlInChromeCustomTab
import kotlinx.android.synthetic.main.fragment_login_signup_signin_selection.*
import javax.inject.Inject
/**
* In this screen, the user is asked to sign up or to sign in using SSO
* This Fragment binds a CustomTabsServiceConnection if available, then prefetch the SSO url, as it will be likely to be opened.
*/
open class LoginSignUpSignInSsoFragment @Inject constructor() : LoginSignUpSignInSelectionFragment() {
private var ssoUrl: String? = null
private var customTabsServiceConnection: CustomTabsServiceConnection? = null
private var customTabsClient: CustomTabsClient? = null
private var customTabsSession: CustomTabsSession? = null
override fun onStart() {
super.onStart()
val packageName = CustomTabsClient.getPackageName(requireContext(), null)
// packageName can be null if there are 0 or several CustomTabs compatible browsers installed on the device
if (packageName != null) {
customTabsServiceConnection = object : CustomTabsServiceConnection() {
override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) {
customTabsClient = client
.also { it.warmup(0L) }
}
override fun onServiceDisconnected(name: ComponentName?) {
}
}
.also {
CustomTabsClient.bindCustomTabsService(
requireContext(),
// Despite the API, packageName cannot be null
packageName,
it
)
}
}
}
private fun prefetchUrl(url: String) {
if (ssoUrl != null) return
ssoUrl = url
if (customTabsSession == null) {
customTabsSession = customTabsClient?.newSession(null)
}
customTabsSession?.mayLaunchUrl(Uri.parse(url), null, null)
}
override fun onStop() {
super.onStop()
customTabsServiceConnection?.let { requireContext().unbindService(it) }
customTabsServiceConnection = null
}
private fun setupButtons() {
loginSignupSigninSubmit.text = getString(R.string.login_signin_sso)
loginSignupSigninSignIn.isVisible = false
}
override fun submit() {
ssoUrl?.let { openUrlInChromeCustomTab(requireContext(), customTabsSession, it) }
}
override fun updateWithState(state: LoginViewState) {
setupUi(state)
setupButtons()
prefetchUrl(state.getSsoUrl())
}
}

View file

@ -34,7 +34,7 @@ sealed class LoginViewEvents : VectorViewEvents {
object OpenServerSelection : LoginViewEvents()
object OnServerSelectionDone : LoginViewEvents()
object OnLoginFlowRetrieved : LoginViewEvents()
data class OnLoginFlowRetrieved(val isSso: Boolean) : LoginViewEvents()
object OnSignModeSelected : LoginViewEvents()
object OnForgetPasswordClicked : LoginViewEvents()
object OnResetPasswordSendThreePidDone : LoginViewEvents()

View file

@ -32,6 +32,7 @@ import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.auth.data.LoginFlowResult
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
import im.vector.matrix.android.api.auth.login.LoginWizard
import im.vector.matrix.android.api.auth.registration.FlowResult
import im.vector.matrix.android.api.auth.registration.RegistrationResult
@ -40,7 +41,6 @@ import im.vector.matrix.android.api.auth.registration.Stage
import im.vector.matrix.android.api.auth.wellknown.WellknownResult
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
@ -110,6 +110,7 @@ class LoginViewModel @AssistedInject constructor(
is LoginAction.InitWith -> handleInitWith(action)
is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action)
is LoginAction.LoginOrRegister -> handleLoginOrRegister(action)
is LoginAction.LoginWithToken -> handleLoginWithToken(action)
is LoginAction.WebLoginSuccess -> handleWebLoginSuccess(action)
is LoginAction.ResetPassword -> handleResetPassword(action)
is LoginAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed()
@ -120,6 +121,41 @@ class LoginViewModel @AssistedInject constructor(
}.exhaustive
}
private fun handleLoginWithToken(action: LoginAction.LoginWithToken) {
val safeLoginWizard = loginWizard
if (safeLoginWizard == null) {
setState {
copy(
asyncLoginAction = Fail(Throwable("Bad configuration"))
)
}
} else {
setState {
copy(
asyncLoginAction = Loading()
)
}
currentTask = safeLoginWizard.loginWithToken(
action.loginToken,
object : MatrixCallback<Session> {
override fun onSuccess(data: Session) {
onSessionCreated(data)
}
override fun onFailure(failure: Throwable) {
_viewEvents.post(LoginViewEvents.Failure(failure))
setState {
copy(
asyncLoginAction = Fail(failure)
)
}
}
})
}
}
private fun handleSetupSsoForSessionRecovery(action: LoginAction.SetupSsoForSessionRecovery) {
setState {
copy(
@ -635,9 +671,9 @@ class LoginViewModel @AssistedInject constructor(
is LoginFlowResult.Success -> {
val loginMode = when {
// SSO login is taken first
data.loginFlowResponse.flows.any { it.type == LoginFlowTypes.SSO } -> LoginMode.Sso
data.loginFlowResponse.flows.any { it.type == LoginFlowTypes.PASSWORD } -> LoginMode.Password
else -> LoginMode.Unsupported
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password
else -> LoginMode.Unsupported
}
if (loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) {
@ -648,7 +684,7 @@ class LoginViewModel @AssistedInject constructor(
asyncHomeServerLoginFlowRequest = Uninitialized,
homeServerUrl = data.homeServerUrl,
loginMode = loginMode,
loginModeSupportedTypes = data.loginFlowResponse.flows.mapNotNull { it.type }.toList()
loginModeSupportedTypes = data.supportedLoginTypes.toList()
)
}
}

View file

@ -22,6 +22,9 @@ import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.PersistState
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.auth.SSO_REDIRECT_PATH
import im.vector.matrix.android.api.auth.SSO_REDIRECT_URL_PARAM
import im.vector.riotx.core.extensions.appendParamToUrl
data class LoginViewState(
val asyncLoginAction: Async<Unit> = Uninitialized,
@ -64,4 +67,22 @@ data class LoginViewState(
fun isUserLogged(): Boolean {
return asyncLoginAction is Success
}
fun getSsoUrl(): String {
return buildString {
append(homeServerUrl?.trim { it == '/' })
append(SSO_REDIRECT_PATH)
// Set a redirect url we will intercept later
appendParamToUrl(SSO_REDIRECT_URL_PARAM, RIOTX_REDIRECT_URL)
deviceId?.takeIf { it.isNotBlank() }?.let {
// But https://github.com/matrix-org/synapse/issues/5755
appendParamToUrl("device_id", it)
}
}
}
companion object {
// Note that the domain can be displayed to the user for confirmation that he trusts it. So use a human readable string
private const val RIOTX_REDIRECT_URL = "riotx://riotx"
}
}

View file

@ -33,8 +33,6 @@ import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.activityViewModel
import im.vector.matrix.android.api.auth.LOGIN_FALLBACK_PATH
import im.vector.matrix.android.api.auth.REGISTER_FALLBACK_PATH
import im.vector.matrix.android.api.auth.SSO_FALLBACK_PATH
import im.vector.matrix.android.api.auth.SSO_REDIRECT_URL_PARAM
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.riotx.R
@ -48,7 +46,7 @@ import java.net.URLDecoder
import javax.inject.Inject
/**
* This screen is displayed for SSO login and also when the application does not support login flow or registration flow
* This screen is displayed when the application does not support login flow or registration flow
* of the homeserver, as a fallback to login or to create an account
*/
class LoginWebFragment @Inject constructor(
@ -128,17 +126,7 @@ class LoginWebFragment @Inject constructor(
val url = buildString {
append(state.homeServerUrl?.trim { it == '/' })
if (state.signMode == SignMode.SignIn) {
if (state.loginMode == LoginMode.Sso) {
append(SSO_FALLBACK_PATH)
// We do not want to deal with the result, so let the fallback login page to handle it for us
appendParamToUrl(SSO_REDIRECT_URL_PARAM,
buildString {
append(state.homeServerUrl?.trim { it == '/' })
append(LOGIN_FALLBACK_PATH)
})
} else {
append(LOGIN_FALLBACK_PATH)
}
append(LOGIN_FALLBACK_PATH)
state.deviceId?.takeIf { it.isNotBlank() }?.let {
// But https://github.com/matrix-org/synapse/issues/5755
appendParamToUrl("device_id", it)
@ -226,7 +214,9 @@ class LoginWebFragment @Inject constructor(
* @return
*/
override fun shouldOverrideUrlLoading(view: WebView, url: String?): Boolean {
if (null != url && url.startsWith("js:")) {
if (url == null) return super.shouldOverrideUrlLoading(view, url as String?)
if (url.startsWith("js:")) {
var json = url.substring(3)
var javascriptResponse: JavascriptResponse? = null

View file

@ -22,10 +22,10 @@ import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
import im.vector.matrix.android.api.failure.toRegistrationFlowResponse
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.crypto.crosssigning.isVerified
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import im.vector.matrix.rx.rx

View file

@ -30,13 +30,13 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.NoOpMatrixCallback
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod
import im.vector.matrix.android.api.session.crypto.verification.VerificationService
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo

View file

@ -28,9 +28,9 @@ import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.api.auth.data.LoginFlowResult
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.extensions.hasUnsavedKeys
import im.vector.riotx.core.platform.VectorViewModel
@ -105,9 +105,9 @@ class SoftLogoutViewModel @AssistedInject constructor(
is LoginFlowResult.Success -> {
val loginMode = when {
// SSO login is taken first
data.loginFlowResponse.flows.any { it.type == LoginFlowTypes.SSO } -> LoginMode.Sso
data.loginFlowResponse.flows.any { it.type == LoginFlowTypes.PASSWORD } -> LoginMode.Password
else -> LoginMode.Unsupported
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password
else -> LoginMode.Unsupported
}
if (loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) {

View file

@ -0,0 +1,23 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#00000000"
android:pathData="M20,12H4"
android:strokeWidth="2"
android:strokeColor="#2E2F32"
android:strokeLineCap="round"
android:strokeLineJoin="round"
tools:strokeColor="#00F000" />
<path
android:fillColor="#00000000"
android:pathData="M10,18L4,12L10,6"
android:strokeWidth="2"
android:strokeColor="#2E2F32"
android:strokeLineCap="round"
android:strokeLineJoin="round"
tools:strokeColor="#00F000" />
</vector>