mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 10:25:35 +03:00
Merge branch 'develop' into feature/power_level
This commit is contained in:
commit
579d4f7a5b
47 changed files with 575 additions and 116 deletions
|
@ -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)
|
||||
===================================================
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
23
vector/src/main/res/drawable/ic_back_24dp.xml
Normal file
23
vector/src/main/res/drawable/ic_back_24dp.xml
Normal 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>
|
Loading…
Reference in a new issue