Merge branch 'develop' into feature/fix_1444

This commit is contained in:
Benoit Marty 2020-06-08 18:07:15 +02:00 committed by GitHub
commit 050407f7c7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 455 additions and 99 deletions

View file

@ -8,11 +8,16 @@ Features ✨:
Improvements 🙌: Improvements 🙌:
- New wording for notice when current user is the sender - New wording for notice when current user is the sender
- Hide "X made no changes" event by default in timeline (#1430) - Hide "X made no changes" event by default in timeline (#1430)
- Hide left rooms in breadcrumbs (#766)
- Correctly handle SSO login redirection
- SSO login is now performed in the default browser, or in Chrome Custom tab if available (#1400)
Bugfix 🐛: Bugfix 🐛:
- Switch theme is not fully taken into account without restarting the app - Switch theme is not fully taken into account without restarting the app
- Temporary fix to show error when user is creating an account on matrix.org with userId containing only digits (#1410) - Temporary fix to show error when user is creating an account on matrix.org with userId containing only digits (#1410)
- Reply composer overlay stays on screen too long after send (#1169) - 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) - Wrong /query request (#1444)
Translations 🗣: Translations 🗣:

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. 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 ### Get the flow
@ -58,7 +58,7 @@ We get credential (200)
```json ```json
{ {
"user_id": "@alice:matrix.org", "user_id": "@alice:matrix.org",
"access_token": "MDAxOGxvY2F0aW9uIG1hdHREDACTEDb2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gfnYrSypfdTtkNXIuNWx1KgowMDJmc2lnbmF0dXJlIOsh1XqeAkXexh4qcofl_aR4kHJoSOWYGOhE7-ubX-DZCg", "access_token": "MDAxOGxvY2F0aW9uIG1hdHREDACTEDb2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lr",
"home_server": "matrix.org", "home_server": "matrix.org",
"device_id": "GTVREDALBF", "device_id": "GTVREDALBF",
"well_known": { "well_known": {
@ -117,7 +117,7 @@ We get the credentials (200)
```json ```json
{ {
"user_id": "@alice:matrix.org", "user_id": "@alice:matrix.org",
"access_token": "MDAxOGxvY2F0aW9uIG1hdHJpeC5vcmREDACTEDZXJfaWQgPSBAYmVub2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gNjtDY0MwRlNPSFFoOC5wOgowMDJmc2lnbmF0dXJlIGiTRm1mYLLxQywxOh3qzQVT8HoEorSokEP2u-bAwtnYCg", "access_token": "MDAxOGxvY2F0aW9uIG1hdHJpeC5vcmREDACTEDZXJfaWQgPSBAYmVub2l0MDgxNjptYXRyaXgub3Jnfrfdegfszsefddvf",
"home_server": "matrix.org", "home_server": "matrix.org",
"device_id": "WBSREDASND", "device_id": "WBSREDASND",
"well_known": { "well_known": {
@ -145,12 +145,59 @@ Not supported yet in RiotX
"flows": [ "flows": [
{ {
"type": "m.login.sso" "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 ## Reset password

View file

@ -29,6 +29,7 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.api.session.widgets.model.Widget
import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional import im.vector.matrix.android.api.util.toOptional
@ -36,7 +37,6 @@ import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
import im.vector.matrix.android.api.session.widgets.model.Widget
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Single import io.reactivex.Single
@ -56,10 +56,10 @@ class RxSession(private val session: Session) {
} }
} }
fun liveBreadcrumbs(): Observable<List<RoomSummary>> { fun liveBreadcrumbs(queryParams: RoomSummaryQueryParams): Observable<List<RoomSummary>> {
return session.getBreadcrumbsLive().asObservable() return session.getBreadcrumbsLive(queryParams).asObservable()
.startWithCallable { .startWithCallable {
session.getBreadcrumbs() session.getBreadcrumbs(queryParams)
} }
} }

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 * Path to use when the client want to connect using SSO
* Ref: https://matrix.org/docs/spec/client_server/latest#sso-client-login * 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" const val SSO_REDIRECT_URL_PARAM = "redirectUrl"

View file

@ -18,10 +18,10 @@ package im.vector.matrix.android.api.auth.data
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse 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 { sealed class LoginFlowResult {
data class Success( data class Success(
val loginFlowResponse: LoginFlowResponse, val supportedLoginTypes: List<String>,
val isLoginAndRegistrationSupported: Boolean, val isLoginAndRegistrationSupported: Boolean,
val homeServerUrl: String val homeServerUrl: String
) : LoginFlowResult() ) : LoginFlowResult()

View file

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

View file

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

View file

@ -21,15 +21,12 @@ sealed class Stage(open val mandatory: Boolean) {
// m.login.recaptcha // m.login.recaptcha
data class ReCaptcha(override val mandatory: Boolean, val publicKey: String) : Stage(mandatory) data class ReCaptcha(override val mandatory: Boolean, val publicKey: String) : Stage(mandatory)
// m.login.oauth2
// m.login.email.identity // m.login.email.identity
data class Email(override val mandatory: Boolean) : Stage(mandatory) data class Email(override val mandatory: Boolean) : Stage(mandatory)
// m.login.msisdn // m.login.msisdn
data class Msisdn(override val mandatory: Boolean) : Stage(mandatory) 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 // 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 // and a password, the dummy stage has to be done
data class Dummy(override val mandatory: Boolean) : Stage(mandatory) data class Dummy(override val mandatory: Boolean) : Stage(mandatory)

View file

@ -73,15 +73,17 @@ interface RoomService {
/** /**
* Get a snapshot list of Breadcrumbs * Get a snapshot list of Breadcrumbs
* @param queryParams parameters to query the room summaries. It can be use to keep only joined rooms, for instance.
* @return the immutable list of [RoomSummary] * @return the immutable list of [RoomSummary]
*/ */
fun getBreadcrumbs(): List<RoomSummary> fun getBreadcrumbs(queryParams: RoomSummaryQueryParams): List<RoomSummary>
/** /**
* Get a live list of Breadcrumbs * Get a live list of Breadcrumbs
* @param queryParams parameters to query the room summaries. It can be use to keep only joined rooms, for instance.
* @return the [LiveData] of [RoomSummary] * @return the [LiveData] of [RoomSummary]
*/ */
fun getBreadcrumbsLive(): LiveData<List<RoomSummary>> fun getBreadcrumbsLive(queryParams: RoomSummaryQueryParams): LiveData<List<RoomSummary>>
/** /**
* Inform the Matrix SDK that a room is displayed. * Inform the Matrix SDK that a room is displayed.

View file

@ -21,6 +21,7 @@ import im.vector.matrix.android.api.auth.data.Versions
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams 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.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.login.ResetPasswordMailConfirmed
import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationParams 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.AddThreePidRegistrationResponse
@ -54,7 +55,7 @@ internal interface AuthAPI {
fun versions(): Call<Versions> 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 * Ref: https://matrix.org/docs/spec/client_server/latest#account-registration-and-management
*/ */
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register") @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register")
@ -91,6 +92,11 @@ internal interface AuthAPI {
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login") @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login")
fun login(@Body loginParams: PasswordLoginParams): Call<Credentials> 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. * Ask the homeserver to reset the password associated with the provided email.
*/ */

View file

@ -236,7 +236,7 @@ internal class DefaultAuthenticationService @Inject constructor(
val loginFlowResponse = executeRequest<LoginFlowResponse>(null) { val loginFlowResponse = executeRequest<LoginFlowResponse>(null) {
apiCall = authAPI.getLoginFlows() apiCall = authAPI.getLoginFlows()
} }
LoginFlowResult.Success(loginFlowResponse, versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl) LoginFlowResult.Success(loginFlowResponse.flows.orEmpty().mapNotNull { it.type }, versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl)
} else { } else {
// Not supported // Not supported
LoginFlowResult.OutdatedHomeserver LoginFlowResult.OutdatedHomeserver

View file

@ -20,7 +20,19 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class LoginFlowResponse( internal data class LoginFlowResponse(
/**
* The homeserver's supported login types
*/
@Json(name = "flows") @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.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
/** /**
* Ref: * 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.SessionCreator
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams 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.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.db.PendingSessionData
import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationParams 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.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, private suspend fun loginInternal(login: String,
password: String, password: String,
deviceName: String) = withContext(coroutineDispatchers.computation) { 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.Json
import com.squareup.moshi.JsonClass 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 * 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 dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback 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.RegisterThreePid
import im.vector.matrix.android.api.auth.registration.RegistrationResult import im.vector.matrix.android.api.auth.registration.RegistrationResult
import im.vector.matrix.android.api.auth.registration.RegistrationWizard import im.vector.matrix.android.api.auth.registration.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.AuthAPI
import im.vector.matrix.android.internal.auth.PendingSessionStore import im.vector.matrix.android.internal.auth.PendingSessionStore
import im.vector.matrix.android.internal.auth.SessionCreator 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.auth.db.PendingSessionData
import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.task.launchToCallback 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.Json
import com.squareup.moshi.JsonClass 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.FlowResult
import im.vector.matrix.android.api.auth.registration.Stage import im.vector.matrix.android.api.auth.registration.Stage
import im.vector.matrix.android.api.auth.registration.TermPolicies import im.vector.matrix.android.api.auth.registration.TermPolicies
import im.vector.matrix.android.api.util.JsonDict 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.InteractiveAuthenticationFlow
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class RegistrationFlowResponse( data class RegistrationFlowResponse(

View file

@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.crypto.model.rest
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass 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 * 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 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.api.CryptoApi
import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceParams import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceParams
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth

View file

@ -111,24 +111,22 @@ internal class DefaultRoomService @Inject constructor(
return query return query
} }
override fun getBreadcrumbs(): List<RoomSummary> { override fun getBreadcrumbs(queryParams: RoomSummaryQueryParams): List<RoomSummary> {
return monarchy.fetchAllMappedSync( return monarchy.fetchAllMappedSync(
{ breadcrumbsQuery(it) }, { breadcrumbsQuery(it, queryParams) },
{ roomSummaryMapper.map(it) } { roomSummaryMapper.map(it) }
) )
} }
override fun getBreadcrumbsLive(): LiveData<List<RoomSummary>> { override fun getBreadcrumbsLive(queryParams: RoomSummaryQueryParams): LiveData<List<RoomSummary>> {
return monarchy.findAllMappedWithChanges( return monarchy.findAllMappedWithChanges(
{ breadcrumbsQuery(it) }, { breadcrumbsQuery(it, queryParams) },
{ roomSummaryMapper.map(it) } { roomSummaryMapper.map(it) }
) )
} }
private fun breadcrumbsQuery(realm: Realm): RealmQuery<RoomSummaryEntity> { private fun breadcrumbsQuery(realm: Realm, queryParams: RoomSummaryQueryParams): RealmQuery<RoomSummaryEntity> {
return RoomSummaryEntity.where(realm) return roomSummariesQuery(realm, queryParams)
.isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME)
.notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name)
.greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummary.NOT_IN_BREADCRUMBS) .greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummary.NOT_IN_BREADCRUMBS)
.sort(RoomSummaryEntityFields.BREADCRUMBS_INDEX) .sort(RoomSummaryEntityFields.BREADCRUMBS_INDEX)
} }

View file

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

View file

@ -48,7 +48,19 @@
<activity android:name=".features.home.HomeActivity" /> <activity android:name=".features.home.HomeActivity" />
<activity <activity
android:name=".features.login.LoginActivity" 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.ImageMediaViewerActivity" />
<activity android:name=".features.media.BigImageViewerActivity" /> <activity android:name=".features.media.BigImageViewerActivity" />
<activity <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.LoginServerSelectionFragment
import im.vector.riotx.features.login.LoginServerUrlFormFragment import im.vector.riotx.features.login.LoginServerUrlFormFragment
import im.vector.riotx.features.login.LoginSignUpSignInSelectionFragment 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.LoginSplashFragment
import im.vector.riotx.features.login.LoginWaitForEmailFragment import im.vector.riotx.features.login.LoginWaitForEmailFragment
import im.vector.riotx.features.login.LoginWebFragment import im.vector.riotx.features.login.LoginWebFragment
@ -217,6 +218,11 @@ interface FragmentModule {
@FragmentKey(LoginSignUpSignInSelectionFragment::class) @FragmentKey(LoginSignUpSignInSelectionFragment::class)
fun bindLoginSignUpSignInSelectionFragment(fragment: LoginSignUpSignInSelectionFragment): Fragment fun bindLoginSignUpSignInSelectionFragment(fragment: LoginSignUpSignInSelectionFragment): Fragment
@Binds
@IntoMap
@FragmentKey(LoginSignUpSignInSsoFragment::class)
fun bindLoginSignUpSignInSsoFragment(fragment: LoginSignUpSignInSsoFragment): Fragment
@Binds @Binds
@IntoMap @IntoMap
@FragmentKey(LoginSplashFragment::class) @FragmentKey(LoginSplashFragment::class)

View file

@ -21,10 +21,14 @@ import android.content.ActivityNotFoundException
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.provider.Browser import android.provider.Browser
import android.provider.MediaStore 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.core.content.FileProvider
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import im.vector.riotx.BuildConfig 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 * Open sound recorder external application
*/ */

View file

@ -16,6 +16,7 @@
package im.vector.riotx.features.crypto.recover 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.Failure
import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.api.failure.toRegistrationFlowResponse 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.SharedSecretStorageService
import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo
import im.vector.matrix.android.api.session.securestorage.SsssKeySpec 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.crosssigning.toBase64NoPadding
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion

View file

@ -21,7 +21,10 @@ import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotx.core.platform.EmptyAction import im.vector.riotx.core.platform.EmptyAction
import im.vector.riotx.core.platform.EmptyViewEvents import im.vector.riotx.core.platform.EmptyViewEvents
@ -58,7 +61,10 @@ class BreadcrumbsViewModel @AssistedInject constructor(@Assisted initialState: B
private fun observeBreadcrumbs() { private fun observeBreadcrumbs() {
session.rx() session.rx()
.liveBreadcrumbs() .liveBreadcrumbs(roomSummaryQueryParams {
displayName = QueryStringValue.NoCondition
memberships = listOf(Membership.JOIN)
})
.observeOn(Schedulers.computation()) .observeOn(Schedulers.computation())
.execute { asyncBreadcrumbs -> .execute { asyncBreadcrumbs ->
copy(asyncBreadcrumbs = asyncBreadcrumbs) copy(asyncBreadcrumbs = asyncBreadcrumbs)

View file

@ -24,6 +24,7 @@ sealed class LoginAction : VectorViewModelAction {
data class UpdateServerType(val serverType: ServerType) : LoginAction() data class UpdateServerType(val serverType: ServerType) : LoginAction()
data class UpdateHomeServer(val homeServerUrl: String) : LoginAction() data class UpdateHomeServer(val homeServerUrl: String) : LoginAction()
data class UpdateSignMode(val signMode: SignMode) : LoginAction() data class UpdateSignMode(val signMode: SignMode) : LoginAction()
data class LoginWithToken(val loginToken: String) : LoginAction()
data class WebLoginSuccess(val credentials: Credentials) : LoginAction() data class WebLoginSuccess(val credentials: Credentials) : LoginAction()
data class InitWith(val loginConfig: LoginConfig) : LoginAction() data class InitWith(val loginConfig: LoginConfig) : LoginAction()
data class ResetPassword(val email: String, val newPassword: String) : 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 com.airbnb.mvrx.withState
import im.vector.matrix.android.api.auth.registration.FlowResult 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.Stage
import im.vector.matrix.android.api.extensions.tryThis
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.POP_BACK_STACK_EXCLUSIVE 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.OnSignModeSelected -> onSignModeSelected()
is LoginViewEvents.OnLoginFlowRetrieved -> is LoginViewEvents.OnLoginFlowRetrieved ->
addFragmentToBackstack(R.id.loginFragmentContainer, addFragmentToBackstack(R.id.loginFragmentContainer,
LoginSignUpSignInSelectionFragment::class.java, if (loginViewEvents.isSso) {
LoginSignUpSignInSsoFragment::class.java
} else {
LoginSignUpSignInSelectionFragment::class.java
},
option = commonOption) option = commonOption)
is LoginViewEvents.OnWebLoginError -> onWebLoginError(loginViewEvents) is LoginViewEvents.OnWebLoginError -> onWebLoginError(loginViewEvents)
is LoginViewEvents.OnForgetPasswordClicked -> is LoginViewEvents.OnForgetPasswordClicked ->
@ -239,16 +244,14 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
SignMode.SignIn -> { SignMode.SignIn -> {
// It depends on the LoginMode // It depends on the LoginMode
when (state.loginMode) { when (state.loginMode) {
LoginMode.Unknown -> error("Developer error") LoginMode.Unknown,
LoginMode.Sso -> error("Developer error")
LoginMode.Password -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginMode.Password -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginFragment::class.java, LoginFragment::class.java,
tag = FRAGMENT_LOGIN_TAG, tag = FRAGMENT_LOGIN_TAG,
option = commonOption) option = commonOption)
LoginMode.Sso -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginWebFragment::class.java,
option = commonOption)
LoginMode.Unsupported -> onLoginModeNotSupported(state.loginModeSupportedTypes) LoginMode.Unsupported -> onLoginModeNotSupported(state.loginModeSupportedTypes)
} }.exhaustive
} }
SignMode.SignInWithMatrixId -> addFragmentToBackstack(R.id.loginFragmentContainer, SignMode.SignInWithMatrixId -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginFragment::class.java, LoginFragment::class.java,
@ -257,6 +260,17 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
}.exhaustive }.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() { private fun onRegistrationStageNotSupported() {
AlertDialog.Builder(this) AlertDialog.Builder(this)
.setTitle(R.string.app_name) .setTitle(R.string.app_name)

View file

@ -55,7 +55,7 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment
} }
@OnClick(R.id.loginServerChoiceModularLearnMore) @OnClick(R.id.loginServerChoiceModularLearnMore)
fun learMore() { fun learnMore() {
openUrlInExternalBrowser(requireActivity(), MODULAR_LINK) openUrlInExternalBrowser(requireActivity(), MODULAR_LINK)
} }
@ -113,7 +113,7 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment
if (state.loginMode != LoginMode.Unknown) { if (state.loginMode != LoginMode.Unknown) {
// LoginFlow for matrix.org has been retrieved // 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

@ -124,7 +124,7 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment()
if (state.loginMode != LoginMode.Unknown) { if (state.loginMode != LoginMode.Unknown) {
// The home server url is valid // 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 * 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 override fun getLayoutResId() = R.layout.fragment_login_signup_signin_selection
private var isSsoSignIn: Boolean = false protected fun setupUi(state: LoginViewState) {
private fun setupUi(state: LoginViewState) {
when (state.serverType) { when (state.serverType) {
ServerType.MatrixOrg -> { ServerType.MatrixOrg -> {
loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_matrix_org)
@ -54,25 +52,14 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr
} }
} }
private fun setupButtons(state: LoginViewState) { private fun setupButtons() {
isSsoSignIn = state.loginMode == LoginMode.Sso loginSignupSigninSubmit.text = getString(R.string.login_signup)
loginSignupSigninSignIn.isVisible = true
if (isSsoSignIn) {
loginSignupSigninSubmit.text = getString(R.string.login_signin_sso)
loginSignupSigninSignIn.isVisible = false
} else {
loginSignupSigninSubmit.text = getString(R.string.login_signup)
loginSignupSigninSignIn.isVisible = true
}
} }
@OnClick(R.id.loginSignupSigninSubmit) @OnClick(R.id.loginSignupSigninSubmit)
fun signUp() { open fun submit() {
if (isSsoSignIn) { loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp))
signIn()
} else {
loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp))
}
} }
@OnClick(R.id.loginSignupSigninSignIn) @OnClick(R.id.loginSignupSigninSignIn)
@ -86,6 +73,6 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr
override fun updateWithState(state: LoginViewState) { override fun updateWithState(state: LoginViewState) {
setupUi(state) 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 OpenServerSelection : LoginViewEvents()
object OnServerSelectionDone : LoginViewEvents() object OnServerSelectionDone : LoginViewEvents()
object OnLoginFlowRetrieved : LoginViewEvents() data class OnLoginFlowRetrieved(val isSso: Boolean) : LoginViewEvents()
object OnSignModeSelected : LoginViewEvents() object OnSignModeSelected : LoginViewEvents()
object OnForgetPasswordClicked : LoginViewEvents() object OnForgetPasswordClicked : LoginViewEvents()
object OnResetPasswordSendThreePidDone : 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.AuthenticationService
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig 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.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.login.LoginWizard
import im.vector.matrix.android.api.auth.registration.FlowResult import im.vector.matrix.android.api.auth.registration.FlowResult
import im.vector.matrix.android.api.auth.registration.RegistrationResult import im.vector.matrix.android.api.auth.registration.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.auth.wellknown.WellknownResult
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.Cancelable 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.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.ActiveSessionHolder
@ -110,6 +110,7 @@ class LoginViewModel @AssistedInject constructor(
is LoginAction.InitWith -> handleInitWith(action) is LoginAction.InitWith -> handleInitWith(action)
is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action) is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action)
is LoginAction.LoginOrRegister -> handleLoginOrRegister(action) is LoginAction.LoginOrRegister -> handleLoginOrRegister(action)
is LoginAction.LoginWithToken -> handleLoginWithToken(action)
is LoginAction.WebLoginSuccess -> handleWebLoginSuccess(action) is LoginAction.WebLoginSuccess -> handleWebLoginSuccess(action)
is LoginAction.ResetPassword -> handleResetPassword(action) is LoginAction.ResetPassword -> handleResetPassword(action)
is LoginAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed() is LoginAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed()
@ -120,6 +121,41 @@ class LoginViewModel @AssistedInject constructor(
}.exhaustive }.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) { private fun handleSetupSsoForSessionRecovery(action: LoginAction.SetupSsoForSessionRecovery) {
setState { setState {
copy( copy(
@ -635,9 +671,9 @@ class LoginViewModel @AssistedInject constructor(
is LoginFlowResult.Success -> { is LoginFlowResult.Success -> {
val loginMode = when { val loginMode = when {
// SSO login is taken first // SSO login is taken first
data.loginFlowResponse.flows.any { it.type == LoginFlowTypes.SSO } -> LoginMode.Sso data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso
data.loginFlowResponse.flows.any { it.type == LoginFlowTypes.PASSWORD } -> LoginMode.Password data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password
else -> LoginMode.Unsupported else -> LoginMode.Unsupported
} }
if (loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) { if (loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) {
@ -648,7 +684,7 @@ class LoginViewModel @AssistedInject constructor(
asyncHomeServerLoginFlowRequest = Uninitialized, asyncHomeServerLoginFlowRequest = Uninitialized,
homeServerUrl = data.homeServerUrl, homeServerUrl = data.homeServerUrl,
loginMode = loginMode, 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.PersistState
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized 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( data class LoginViewState(
val asyncLoginAction: Async<Unit> = Uninitialized, val asyncLoginAction: Async<Unit> = Uninitialized,
@ -64,4 +67,22 @@ data class LoginViewState(
fun isUserLogged(): Boolean { fun isUserLogged(): Boolean {
return asyncLoginAction is Success 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 com.airbnb.mvrx.activityViewModel
import im.vector.matrix.android.api.auth.LOGIN_FALLBACK_PATH 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.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.api.auth.data.Credentials
import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.riotx.R import im.vector.riotx.R
@ -48,7 +46,7 @@ import java.net.URLDecoder
import javax.inject.Inject 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 * of the homeserver, as a fallback to login or to create an account
*/ */
class LoginWebFragment @Inject constructor( class LoginWebFragment @Inject constructor(
@ -128,17 +126,7 @@ class LoginWebFragment @Inject constructor(
val url = buildString { val url = buildString {
append(state.homeServerUrl?.trim { it == '/' }) append(state.homeServerUrl?.trim { it == '/' })
if (state.signMode == SignMode.SignIn) { if (state.signMode == SignMode.SignIn) {
if (state.loginMode == LoginMode.Sso) { append(LOGIN_FALLBACK_PATH)
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)
}
state.deviceId?.takeIf { it.isNotBlank() }?.let { state.deviceId?.takeIf { it.isNotBlank() }?.let {
// But https://github.com/matrix-org/synapse/issues/5755 // But https://github.com/matrix-org/synapse/issues/5755
appendParamToUrl("device_id", it) appendParamToUrl("device_id", it)
@ -226,7 +214,9 @@ class LoginWebFragment @Inject constructor(
* @return * @return
*/ */
override fun shouldOverrideUrlLoading(view: WebView, url: String?): Boolean { 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 json = url.substring(3)
var javascriptResponse: JavascriptResponse? = null 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.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback 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.failure.toRegistrationFlowResponse
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo 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.crosssigning.isVerified
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import im.vector.matrix.rx.rx 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 com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.NoOpMatrixCallback 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.failure.Failure
import im.vector.matrix.android.api.session.Session 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.VerificationMethod
import im.vector.matrix.android.api.session.crypto.verification.VerificationService 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.VerificationTransaction
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState 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.crosssigning.DeviceTrustLevel
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.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.MatrixCallback
import im.vector.matrix.android.api.auth.AuthenticationService 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.LoginFlowResult
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.Cancelable 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.di.ActiveSessionHolder
import im.vector.riotx.core.extensions.hasUnsavedKeys import im.vector.riotx.core.extensions.hasUnsavedKeys
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
@ -105,9 +105,9 @@ class SoftLogoutViewModel @AssistedInject constructor(
is LoginFlowResult.Success -> { is LoginFlowResult.Success -> {
val loginMode = when { val loginMode = when {
// SSO login is taken first // SSO login is taken first
data.loginFlowResponse.flows.any { it.type == LoginFlowTypes.SSO } -> LoginMode.Sso data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso
data.loginFlowResponse.flows.any { it.type == LoginFlowTypes.PASSWORD } -> LoginMode.Password data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password
else -> LoginMode.Unsupported else -> LoginMode.Unsupported
} }
if (loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) { 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>

View file

@ -2,8 +2,10 @@
<resources> <resources>
<style name="AppTheme.Light.v21" parent="AppTheme.Base.Light"> <style name="AppTheme.Light.v21" parent="AppTheme.Base.Light">
<item name="android:statusBarColor">@color/riotx_header_panel_background_light</item> <!-- Use dark color, to have enough contrast with icons color. windowLightStatusBar is only available in API 23+ -->
<item name="android:navigationBarColor">@color/riotx_header_panel_background_light</item> <item name="android:statusBarColor">@color/riotx_header_panel_background_dark</item>
<!-- Use dark color, to have enough contrast with icons color. windowLightNavigationBar is only available in API 27+ -->
<item name="android:navigationBarColor">@color/riotx_header_panel_background_dark</item>
<!-- enable window content transitions --> <!-- enable window content transitions -->
<item name="android:windowContentTransitions">true</item> <item name="android:windowContentTransitions">true</item>

View file

@ -2,8 +2,10 @@
<resources> <resources>
<style name="AppTheme.Status.v21" parent="AppTheme.Base.Status"> <style name="AppTheme.Status.v21" parent="AppTheme.Base.Status">
<item name="android:statusBarColor">@color/riotx_header_panel_background_light</item> <!-- Use dark color, to have enough contrast with icons color. windowLightStatusBar is only available in API 23+ -->
<item name="android:navigationBarColor">@color/riotx_header_panel_background_light</item> <item name="android:statusBarColor">@color/riotx_header_panel_background_dark</item>
<!-- Use dark color, to have enough contrast with icons color. windowLightNavigationBar is only available in API 27+ -->
<item name="android:navigationBarColor">@color/riotx_header_panel_background_dark</item>
<!-- enable window content transitions --> <!-- enable window content transitions -->
<item name="android:windowContentTransitions">true</item> <item name="android:windowContentTransitions">true</item>

View file

@ -2,6 +2,7 @@
<resources> <resources>
<style name="AppTheme.Light.v23" parent="AppTheme.Light.v21"> <style name="AppTheme.Light.v23" parent="AppTheme.Light.v21">
<item name="android:statusBarColor">@color/riotx_header_panel_background_light</item>
<item name="android:windowLightStatusBar">true</item> <item name="android:windowLightStatusBar">true</item>
</style> </style>

View file

@ -2,7 +2,8 @@
<resources> <resources>
<style name="AppTheme.Status.v23" parent="AppTheme.Status.v21"> <style name="AppTheme.Status.v23" parent="AppTheme.Status.v21">
<item name="android:windowLightStatusBar">false</item> <item name="android:statusBarColor">@color/riotx_header_panel_background_light</item>
<item name="android:windowLightStatusBar">true</item>
</style> </style>
<style name="AppTheme.Status" parent="AppTheme.Status.v23"/> <style name="AppTheme.Status" parent="AppTheme.Status.v23"/>

View file

@ -2,6 +2,7 @@
<resources> <resources>
<style name="AppTheme.Light.v27" parent="AppTheme.Light.v23"> <style name="AppTheme.Light.v27" parent="AppTheme.Light.v23">
<item name="android:navigationBarColor">@color/riotx_header_panel_background_light</item>
<item name="android:windowLightNavigationBar">true</item> <item name="android:windowLightNavigationBar">true</item>
</style> </style>

View file

@ -2,6 +2,7 @@
<resources> <resources>
<style name="AppTheme.Status.v27" parent="AppTheme.Status.v23"> <style name="AppTheme.Status.v27" parent="AppTheme.Status.v23">
<item name="android:navigationBarColor">@color/riotx_header_panel_background_light</item>
<item name="android:windowLightNavigationBar">true</item> <item name="android:windowLightNavigationBar">true</item>
</style> </style>