SSO UIA
This commit is contained in:
parent
98054815a4
commit
1244d00b31
@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.api.auth
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse
|
||||||
|
import org.matrix.android.sdk.internal.crypto.model.rest.UIABaseAuth
|
||||||
|
import kotlin.coroutines.Continuation
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some API endpoints require authentication that interacts with the user.
|
||||||
|
* The homeserver may provide many different ways of authenticating, such as user/password auth, login via a social network (OAuth2),
|
||||||
|
* login by confirming a token sent to their email address, etc.
|
||||||
|
*
|
||||||
|
* The process takes the form of one or more 'stages'.
|
||||||
|
* At each stage the client submits a set of data for a given authentication type and awaits a response from the server,
|
||||||
|
* which will either be a final success or a request to perform an additional stage.
|
||||||
|
* This exchange continues until the final success.
|
||||||
|
*
|
||||||
|
* For each endpoint, a server offers one or more 'flows' that the client can use to authenticate itself.
|
||||||
|
* Each flow comprises a series of stages, as described above.
|
||||||
|
* The client is free to choose which flow it follows, however the flow's stages must be completed in order.
|
||||||
|
* Failing to follow the flows in order must result in an HTTP 401 response.
|
||||||
|
* When all stages in a flow are complete, authentication is complete and the API call succeeds.
|
||||||
|
*/
|
||||||
|
interface UserInteractiveAuthInterceptor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the API needs additional auth, this will be called.
|
||||||
|
* Implementation should check the flows from flow response and act accordingly.
|
||||||
|
* Updated auth should be provider using promise.resume, this allow implementation to perform
|
||||||
|
* an async operation (prompt for user password, open sso fallback) and then resume initial API call when done.
|
||||||
|
*/
|
||||||
|
fun performStage(flowResponse: RegistrationFlowResponse, promise : Continuation<UIABaseAuth>)
|
||||||
|
}
|
@ -245,6 +245,8 @@ interface Session :
|
|||||||
|
|
||||||
val sharedSecretStorageService: SharedSecretStorageService
|
val sharedSecretStorageService: SharedSecretStorageService
|
||||||
|
|
||||||
|
fun getUIASsoFallbackUrl(authenticationSessionId: String): String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maintenance API, allows to print outs info on DB size to logcat
|
* Maintenance API, allows to print outs info on DB size to logcat
|
||||||
*/
|
*/
|
||||||
|
@ -20,6 +20,7 @@ import android.content.Context
|
|||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.paging.PagedList
|
import androidx.paging.PagedList
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.listeners.ProgressListener
|
import org.matrix.android.sdk.api.listeners.ProgressListener
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService
|
||||||
@ -53,7 +54,7 @@ interface CryptoService {
|
|||||||
|
|
||||||
fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>)
|
fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>)
|
||||||
|
|
||||||
fun deleteDevice(deviceId: String, callback: MatrixCallback<Unit>)
|
fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback<Unit>)
|
||||||
|
|
||||||
fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback<Unit>)
|
fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback<Unit>)
|
||||||
|
|
||||||
|
@ -18,10 +18,10 @@ package org.matrix.android.sdk.api.session.crypto.crosssigning
|
|||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.util.Optional
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustResult
|
import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustResult
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.UserTrustResult
|
import org.matrix.android.sdk.internal.crypto.crosssigning.UserTrustResult
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo
|
import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo
|
||||||
|
|
||||||
interface CrossSigningService {
|
interface CrossSigningService {
|
||||||
@ -40,7 +40,7 @@ interface CrossSigningService {
|
|||||||
* Initialize cross signing for this user.
|
* Initialize cross signing for this user.
|
||||||
* Users needs to enter credentials
|
* Users needs to enter credentials
|
||||||
*/
|
*/
|
||||||
fun initializeCrossSigning(authParams: UserPasswordAuth?,
|
fun initializeCrossSigning(uiaInterceptor: UserInteractiveAuthInterceptor?,
|
||||||
callback: MatrixCallback<Unit>)
|
callback: MatrixCallback<Unit>)
|
||||||
|
|
||||||
fun isCrossSigningInitialized(): Boolean = getMyCrossSigningKeys() != null
|
fun isCrossSigningInitialized(): Boolean = getMyCrossSigningKeys() != null
|
||||||
|
@ -30,6 +30,7 @@ import kotlinx.coroutines.runBlocking
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
import org.matrix.android.sdk.api.NoOpMatrixCallback
|
import org.matrix.android.sdk.api.NoOpMatrixCallback
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
|
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.failure.Failure
|
import org.matrix.android.sdk.api.failure.Failure
|
||||||
@ -207,9 +208,9 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
.executeBy(taskExecutor)
|
.executeBy(taskExecutor)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteDevice(deviceId: String, callback: MatrixCallback<Unit>) {
|
override fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback<Unit>) {
|
||||||
deleteDeviceTask
|
deleteDeviceTask
|
||||||
.configureWith(DeleteDeviceTask.Params(deviceId)) {
|
.configureWith(DeleteDeviceTask.Params(deviceId, userInteractiveAuthInterceptor, null)) {
|
||||||
this.executionThread = TaskThread.CRYPTO
|
this.executionThread = TaskThread.CRYPTO
|
||||||
this.callback = callback
|
this.callback = callback
|
||||||
}
|
}
|
||||||
|
@ -19,30 +19,30 @@ package org.matrix.android.sdk.internal.crypto.crosssigning
|
|||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.work.BackoffPolicy
|
import androidx.work.BackoffPolicy
|
||||||
import androidx.work.ExistingWorkPolicy
|
import androidx.work.ExistingWorkPolicy
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||||
import org.matrix.android.sdk.api.util.Optional
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
||||||
|
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder
|
import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||||
import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo
|
import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask
|
import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.UploadSignaturesTask
|
import org.matrix.android.sdk.internal.crypto.tasks.UploadSignaturesTask
|
||||||
|
import org.matrix.android.sdk.internal.di.SessionId
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
import org.matrix.android.sdk.internal.di.UserId
|
||||||
|
import org.matrix.android.sdk.internal.di.WorkManagerProvider
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
import org.matrix.android.sdk.internal.session.SessionScope
|
||||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||||
import org.matrix.android.sdk.internal.task.TaskThread
|
import org.matrix.android.sdk.internal.task.TaskThread
|
||||||
import org.matrix.android.sdk.internal.task.configureWith
|
import org.matrix.android.sdk.internal.task.configureWith
|
||||||
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
||||||
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
|
||||||
import org.matrix.android.sdk.internal.di.SessionId
|
|
||||||
import org.matrix.android.sdk.internal.di.WorkManagerProvider
|
|
||||||
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
||||||
import org.matrix.olm.OlmPkSigning
|
import org.matrix.olm.OlmPkSigning
|
||||||
import org.matrix.olm.OlmUtility
|
import org.matrix.olm.OlmUtility
|
||||||
@ -147,11 +147,11 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||||||
* - Sign the keys and upload them
|
* - Sign the keys and upload them
|
||||||
* - Sign the current device with SSK and sign MSK with device key (migration) and upload signatures
|
* - Sign the current device with SSK and sign MSK with device key (migration) and upload signatures
|
||||||
*/
|
*/
|
||||||
override fun initializeCrossSigning(authParams: UserPasswordAuth?, callback: MatrixCallback<Unit>) {
|
override fun initializeCrossSigning(uiaInterceptor: UserInteractiveAuthInterceptor?, callback: MatrixCallback<Unit>) {
|
||||||
Timber.d("## CrossSigning initializeCrossSigning")
|
Timber.d("## CrossSigning initializeCrossSigning")
|
||||||
|
|
||||||
val params = InitializeCrossSigningTask.Params(
|
val params = InitializeCrossSigningTask.Params(
|
||||||
authParams = authParams
|
interactiveAuthInterceptor = uiaInterceptor
|
||||||
)
|
)
|
||||||
initializeCrossSigningTask.configureWith(params) {
|
initializeCrossSigningTask.configureWith(params) {
|
||||||
this.callbackThread = TaskThread.CRYPTO
|
this.callbackThread = TaskThread.CRYPTO
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.internal.crypto.model.rest
|
||||||
|
|
||||||
|
data class DefaultBaseAuth(
|
||||||
|
/**
|
||||||
|
* This is a session identifier that the client must pass back to the homeserver,
|
||||||
|
* if one is provided, in subsequent attempts to authenticate in the same API call.
|
||||||
|
*/
|
||||||
|
override val session: String? = null
|
||||||
|
|
||||||
|
) : UIABaseAuth {
|
||||||
|
override fun hasAuthInfo() = true
|
||||||
|
|
||||||
|
override fun copyWithSession(session: String) = this.copy(session = session)
|
||||||
|
|
||||||
|
override fun asMap(): Map<String, *> = mapOf("session" to session)
|
||||||
|
}
|
@ -24,5 +24,5 @@ import com.squareup.moshi.JsonClass
|
|||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
internal data class DeleteDeviceParams(
|
internal data class DeleteDeviceParams(
|
||||||
@Json(name = "auth")
|
@Json(name = "auth")
|
||||||
val userPasswordAuth: UserPasswordAuth? = null
|
val auth: Map<String, *>? = null
|
||||||
)
|
)
|
||||||
|
@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.internal.crypto.model.rest
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class provides the authentication data by using user and password
|
||||||
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class TokenBasedAuth(
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a session identifier that the client must pass back to the homeserver,
|
||||||
|
* if one is provided, in subsequent attempts to authenticate in the same API call.
|
||||||
|
*/
|
||||||
|
@Json(name = "session")
|
||||||
|
override val session: String? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A client may receive a login token via some external service, such as email or SMS.
|
||||||
|
* Note that a login token is separate from an access token, the latter providing general authentication to various API endpoints.
|
||||||
|
*/
|
||||||
|
@Json(name = "token")
|
||||||
|
val token: String? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The txn_id should be a random string generated by the client for the request.
|
||||||
|
* The same txn_id should be used if retrying the request.
|
||||||
|
* The txn_id may be used by the server to disallow other devices from using the token,
|
||||||
|
* thus providing "single use" tokens while still allowing the device to retry the request.
|
||||||
|
* This would be done by tying the token to the txn_id server side, as well as potentially invalidating
|
||||||
|
* the token completely once the device has successfully logged in
|
||||||
|
* (e.g. when we receive a request from the newly provisioned access_token).
|
||||||
|
*/
|
||||||
|
@Json(name = "txn_id")
|
||||||
|
val transactionId: String? = null,
|
||||||
|
|
||||||
|
// registration information
|
||||||
|
@Json(name = "type")
|
||||||
|
val type: String? = LoginFlowTypes.TOKEN
|
||||||
|
|
||||||
|
) : UIABaseAuth {
|
||||||
|
override fun hasAuthInfo() = token != null
|
||||||
|
|
||||||
|
override fun copyWithSession(session: String) = this.copy(session = session)
|
||||||
|
|
||||||
|
override fun asMap(): Map<String, *> = mapOf(
|
||||||
|
"session" to session,
|
||||||
|
"token" to token,
|
||||||
|
"transactionId" to transactionId,
|
||||||
|
"type" to type
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.internal.crypto.model.rest
|
||||||
|
|
||||||
|
interface UIABaseAuth {
|
||||||
|
/**
|
||||||
|
* This is a session identifier that the client must pass back to the homeserver,
|
||||||
|
* if one is provided, in subsequent attempts to authenticate in the same API call.
|
||||||
|
*/
|
||||||
|
val session: String?
|
||||||
|
|
||||||
|
fun hasAuthInfo(): Boolean
|
||||||
|
|
||||||
|
fun copyWithSession(session: String): UIABaseAuth
|
||||||
|
|
||||||
|
fun asMap() : Map<String, *>
|
||||||
|
}
|
@ -30,5 +30,5 @@ internal data class UploadSigningKeysBody(
|
|||||||
val userSigningKey: RestKeyInfo? = null,
|
val userSigningKey: RestKeyInfo? = null,
|
||||||
|
|
||||||
@Json(name = "auth")
|
@Json(name = "auth")
|
||||||
val auth: UserPasswordAuth? = null
|
val auth: Map<String, *>? = null
|
||||||
)
|
)
|
||||||
|
@ -27,7 +27,7 @@ data class UserPasswordAuth(
|
|||||||
|
|
||||||
// device device session id
|
// device device session id
|
||||||
@Json(name = "session")
|
@Json(name = "session")
|
||||||
val session: String? = null,
|
override val session: String? = null,
|
||||||
|
|
||||||
// registration information
|
// registration information
|
||||||
@Json(name = "type")
|
@Json(name = "type")
|
||||||
@ -38,4 +38,16 @@ data class UserPasswordAuth(
|
|||||||
|
|
||||||
@Json(name = "password")
|
@Json(name = "password")
|
||||||
val password: String? = null
|
val password: String? = null
|
||||||
)
|
) : UIABaseAuth {
|
||||||
|
|
||||||
|
override fun hasAuthInfo() = password != null
|
||||||
|
|
||||||
|
override fun copyWithSession(session: String) = this.copy(session = session)
|
||||||
|
|
||||||
|
override fun asMap(): Map<String, *> = mapOf(
|
||||||
|
"session" to session,
|
||||||
|
"user" to user,
|
||||||
|
"password" to password,
|
||||||
|
"type" to type
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -16,18 +16,24 @@
|
|||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.tasks
|
package org.matrix.android.sdk.internal.crypto.tasks
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.failure.Failure
|
import org.matrix.android.sdk.api.failure.Failure
|
||||||
import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse
|
import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse
|
||||||
import org.matrix.android.sdk.internal.crypto.api.CryptoApi
|
import org.matrix.android.sdk.internal.crypto.api.CryptoApi
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams
|
import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams
|
||||||
|
import org.matrix.android.sdk.internal.crypto.model.rest.UIABaseAuth
|
||||||
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
||||||
import org.matrix.android.sdk.internal.network.executeRequest
|
import org.matrix.android.sdk.internal.network.executeRequest
|
||||||
import org.matrix.android.sdk.internal.task.Task
|
import org.matrix.android.sdk.internal.task.Task
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
internal interface DeleteDeviceTask : Task<DeleteDeviceTask.Params, Unit> {
|
internal interface DeleteDeviceTask : Task<DeleteDeviceTask.Params, Unit> {
|
||||||
data class Params(
|
data class Params(
|
||||||
val deviceId: String
|
val deviceId: String,
|
||||||
|
val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor?,
|
||||||
|
val userAuthParam: UIABaseAuth?
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,12 +45,49 @@ internal class DefaultDeleteDeviceTask @Inject constructor(
|
|||||||
override suspend fun execute(params: DeleteDeviceTask.Params) {
|
override suspend fun execute(params: DeleteDeviceTask.Params) {
|
||||||
try {
|
try {
|
||||||
executeRequest<Unit>(globalErrorReceiver) {
|
executeRequest<Unit>(globalErrorReceiver) {
|
||||||
apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams())
|
apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams(params.userAuthParam?.asMap()))
|
||||||
}
|
}
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
throw throwable.toRegistrationFlowResponse()
|
if (params.userInteractiveAuthInterceptor == null || !handleUIA(throwable, params)) {
|
||||||
?.let { Failure.RegistrationFlowError(it) }
|
Timber.d("## UIA: propagate failure")
|
||||||
?: throwable
|
throw throwable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun handleUIA(failure: Throwable, params: DeleteDeviceTask.Params): Boolean {
|
||||||
|
Timber.d("## UIA: check error delete device ${failure.message}")
|
||||||
|
if (failure is Failure.OtherServerError && failure.httpCode == 401) {
|
||||||
|
Timber.d("## UIA: error can be passed to interceptor")
|
||||||
|
// give a chance to the reauth helper?
|
||||||
|
val flowResponse = failure.toRegistrationFlowResponse()
|
||||||
|
?: return false.also {
|
||||||
|
Timber.d("## UIA: failed to parse flow response")
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.d("## UIA: type = ${flowResponse.flows}")
|
||||||
|
Timber.d("## UIA: has interceptor = ${params.userInteractiveAuthInterceptor != null}")
|
||||||
|
|
||||||
|
Timber.d("## UIA: delegate to interceptor...")
|
||||||
|
val authUpdate = try {
|
||||||
|
suspendCoroutine<UIABaseAuth> { continuation ->
|
||||||
|
params.userInteractiveAuthInterceptor!!.performStage(flowResponse, continuation)
|
||||||
|
}
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
Timber.w(failure, "## UIA: failed to participate")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.d("## UIA: delete device updated auth $authUpdate")
|
||||||
|
return try {
|
||||||
|
execute(params.copy(userAuthParam = authUpdate))
|
||||||
|
true
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
handleUIA(failure, params)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.d("## UIA: not a UIA error")
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,12 +44,12 @@ internal class DefaultDeleteDeviceWithUserPasswordTask @Inject constructor(
|
|||||||
return executeRequest(globalErrorReceiver) {
|
return executeRequest(globalErrorReceiver) {
|
||||||
apiCall = cryptoApi.deleteDevice(params.deviceId,
|
apiCall = cryptoApi.deleteDevice(params.deviceId,
|
||||||
DeleteDeviceParams(
|
DeleteDeviceParams(
|
||||||
userPasswordAuth = UserPasswordAuth(
|
auth = UserPasswordAuth(
|
||||||
type = LoginFlowTypes.PASSWORD,
|
type = LoginFlowTypes.PASSWORD,
|
||||||
session = params.authSession,
|
session = params.authSession,
|
||||||
user = userId,
|
user = userId,
|
||||||
password = params.password
|
password = params.password
|
||||||
)
|
).asMap()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -17,24 +17,28 @@
|
|||||||
package org.matrix.android.sdk.internal.crypto.tasks
|
package org.matrix.android.sdk.internal.crypto.tasks
|
||||||
|
|
||||||
import dagger.Lazy
|
import dagger.Lazy
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
|
import org.matrix.android.sdk.api.failure.Failure
|
||||||
|
import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
||||||
import org.matrix.android.sdk.internal.crypto.MyDeviceInfoHolder
|
import org.matrix.android.sdk.internal.crypto.MyDeviceInfoHolder
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.canonicalSignable
|
import org.matrix.android.sdk.internal.crypto.crosssigning.canonicalSignable
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
|
import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
|
||||||
import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey
|
import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey
|
||||||
import org.matrix.android.sdk.internal.crypto.model.KeyUsage
|
import org.matrix.android.sdk.internal.crypto.model.KeyUsage
|
||||||
|
import org.matrix.android.sdk.internal.crypto.model.rest.UIABaseAuth
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder
|
import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
import org.matrix.android.sdk.internal.di.UserId
|
||||||
import org.matrix.android.sdk.internal.task.Task
|
import org.matrix.android.sdk.internal.task.Task
|
||||||
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
||||||
import org.matrix.olm.OlmPkSigning
|
import org.matrix.olm.OlmPkSigning
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
internal interface InitializeCrossSigningTask : Task<InitializeCrossSigningTask.Params, InitializeCrossSigningTask.Result> {
|
internal interface InitializeCrossSigningTask : Task<InitializeCrossSigningTask.Params, InitializeCrossSigningTask.Result> {
|
||||||
data class Params(
|
data class Params(
|
||||||
val authParams: UserPasswordAuth?
|
val interactiveAuthInterceptor: UserInteractiveAuthInterceptor?
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Result(
|
data class Result(
|
||||||
@ -117,10 +121,18 @@ internal class DefaultInitializeCrossSigningTask @Inject constructor(
|
|||||||
.key(sskPublicKey)
|
.key(sskPublicKey)
|
||||||
.signature(userId, masterPublicKey, signedSSK)
|
.signature(userId, masterPublicKey, signedSSK)
|
||||||
.build(),
|
.build(),
|
||||||
userPasswordAuth = params.authParams
|
userAuthParam = null
|
||||||
|
// userAuthParam = params.authParams
|
||||||
)
|
)
|
||||||
|
|
||||||
uploadSigningKeysTask.execute(uploadSigningKeysParams)
|
try {
|
||||||
|
uploadSigningKeysTask.execute(uploadSigningKeysParams)
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
if (params.interactiveAuthInterceptor == null || !handleUIA(failure, params, uploadSigningKeysParams)) {
|
||||||
|
Timber.d("## UIA: propagate failure")
|
||||||
|
throw failure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Sign the current device with SSK
|
// Sign the current device with SSK
|
||||||
val uploadSignatureQueryBuilder = UploadSignatureQueryBuilder()
|
val uploadSignatureQueryBuilder = UploadSignatureQueryBuilder()
|
||||||
@ -169,4 +181,42 @@ internal class DefaultInitializeCrossSigningTask @Inject constructor(
|
|||||||
selfSigningPkOlm?.releaseSigning()
|
selfSigningPkOlm?.releaseSigning()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun handleUIA(failure: Throwable,
|
||||||
|
params: InitializeCrossSigningTask.Params,
|
||||||
|
uploadSigningKeysParams: UploadSigningKeysTask.Params): Boolean {
|
||||||
|
Timber.d("## UIA: check error initialize xsigning ${failure.message}")
|
||||||
|
if (failure is Failure.OtherServerError && failure.httpCode == 401) {
|
||||||
|
Timber.d("## UIA: error can be passed to interceptor")
|
||||||
|
// give a chance to the reauth helper?
|
||||||
|
val flowResponse = failure.toRegistrationFlowResponse()
|
||||||
|
?: return false.also {
|
||||||
|
Timber.d("## UIA: failed to parse flow response")
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.d("## UIA: type = ${flowResponse.flows}")
|
||||||
|
Timber.d("## UIA: has interceptor = ${params.interactiveAuthInterceptor != null}")
|
||||||
|
|
||||||
|
Timber.d("## UIA: delegate to interceptor...")
|
||||||
|
val authUpdate = try {
|
||||||
|
suspendCoroutine<UIABaseAuth> { continuation ->
|
||||||
|
params.interactiveAuthInterceptor!!.performStage(flowResponse, continuation)
|
||||||
|
}
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
Timber.w(failure, "## UIA: failed to participate")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.d("## UIA: initialize xsigning updated auth $authUpdate")
|
||||||
|
try {
|
||||||
|
uploadSigningKeysTask.execute(uploadSigningKeysParams.copy(userAuthParam = authUpdate))
|
||||||
|
return true
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
return handleUIA(failure, params, uploadSigningKeysParams)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.d("## UIA: not a UIA error")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,14 +16,12 @@
|
|||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.tasks
|
package org.matrix.android.sdk.internal.crypto.tasks
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
|
||||||
import org.matrix.android.sdk.api.failure.Failure
|
import org.matrix.android.sdk.api.failure.Failure
|
||||||
import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse
|
|
||||||
import org.matrix.android.sdk.internal.crypto.api.CryptoApi
|
import org.matrix.android.sdk.internal.crypto.api.CryptoApi
|
||||||
import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey
|
import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse
|
import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse
|
||||||
|
import org.matrix.android.sdk.internal.crypto.model.rest.UIABaseAuth
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UploadSigningKeysBody
|
import org.matrix.android.sdk.internal.crypto.model.rest.UploadSigningKeysBody
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.toRest
|
import org.matrix.android.sdk.internal.crypto.model.toRest
|
||||||
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
||||||
import org.matrix.android.sdk.internal.network.executeRequest
|
import org.matrix.android.sdk.internal.network.executeRequest
|
||||||
@ -39,15 +37,9 @@ internal interface UploadSigningKeysTask : Task<UploadSigningKeysTask.Params, Un
|
|||||||
// the SSK
|
// the SSK
|
||||||
val selfSignedKey: CryptoCrossSigningKey,
|
val selfSignedKey: CryptoCrossSigningKey,
|
||||||
/**
|
/**
|
||||||
* - If null:
|
* Authorisation info (User Interactive flow)
|
||||||
* - no retry will be performed
|
|
||||||
* - If not null, it may or may not contain a sessionId:
|
|
||||||
* - If sessionId is null:
|
|
||||||
* - password should not be null: the task will perform a first request to get a sessionId, and then a second one
|
|
||||||
* - If sessionId is not null:
|
|
||||||
* - password should not be null as well, and no retry will be performed
|
|
||||||
*/
|
*/
|
||||||
val userPasswordAuth: UserPasswordAuth?
|
val userAuthParam: UIABaseAuth?
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,31 +51,13 @@ internal class DefaultUploadSigningKeysTask @Inject constructor(
|
|||||||
) : UploadSigningKeysTask {
|
) : UploadSigningKeysTask {
|
||||||
|
|
||||||
override suspend fun execute(params: UploadSigningKeysTask.Params) {
|
override suspend fun execute(params: UploadSigningKeysTask.Params) {
|
||||||
val paramsHaveSessionId = params.userPasswordAuth?.session != null
|
|
||||||
|
|
||||||
val uploadQuery = UploadSigningKeysBody(
|
val uploadQuery = UploadSigningKeysBody(
|
||||||
masterKey = params.masterKey.toRest(),
|
masterKey = params.masterKey.toRest(),
|
||||||
userSigningKey = params.userKey.toRest(),
|
userSigningKey = params.userKey.toRest(),
|
||||||
selfSigningKey = params.selfSignedKey.toRest(),
|
selfSigningKey = params.selfSignedKey.toRest(),
|
||||||
// If sessionId is provided, use the userPasswordAuth
|
auth = params.userAuthParam?.asMap()
|
||||||
auth = params.userPasswordAuth.takeIf { paramsHaveSessionId }
|
|
||||||
)
|
)
|
||||||
try {
|
doRequest(uploadQuery)
|
||||||
doRequest(uploadQuery)
|
|
||||||
} catch (throwable: Throwable) {
|
|
||||||
val registrationFlowResponse = throwable.toRegistrationFlowResponse()
|
|
||||||
if (registrationFlowResponse != null
|
|
||||||
&& registrationFlowResponse.flows.orEmpty().any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true }
|
|
||||||
&& params.userPasswordAuth?.password != null
|
|
||||||
&& !paramsHaveSessionId
|
|
||||||
) {
|
|
||||||
// Retry with authentication
|
|
||||||
doRequest(uploadQuery.copy(auth = params.userPasswordAuth.copy(session = registrationFlowResponse.session)))
|
|
||||||
} else {
|
|
||||||
// Other error
|
|
||||||
throw throwable
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun doRequest(uploadQuery: UploadSigningKeysBody) {
|
private suspend fun doRequest(uploadQuery: UploadSigningKeysBody) {
|
||||||
|
@ -273,6 +273,19 @@ internal class DefaultSession @Inject constructor(
|
|||||||
return "$myUserId - ${sessionParams.deviceId}"
|
return "$myUserId - ${sessionParams.deviceId}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getUIASsoFallbackUrl(authenticationSessionId: String): String {
|
||||||
|
val hsBas = sessionParams.homeServerConnectionConfig
|
||||||
|
.homeServerUri
|
||||||
|
.toString()
|
||||||
|
.trim { it == '/' }
|
||||||
|
return buildString {
|
||||||
|
append(hsBas)
|
||||||
|
append("/_matrix/client/r0/auth/m.login.sso/fallback/web")
|
||||||
|
append("?session=")
|
||||||
|
append(authenticationSessionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun logDbUsageInfo() {
|
override fun logDbUsageInfo() {
|
||||||
RealmDebugTools(realmConfiguration).logInfo("Session")
|
RealmDebugTools(realmConfiguration).logInfo("Session")
|
||||||
}
|
}
|
||||||
|
@ -305,4 +305,5 @@
|
|||||||
|
|
||||||
<string name="key_verification_request_fallback_message">%s is requesting to verify your key, but your client does not support in-chat key verification. You will need to use legacy key verification to verify keys.</string>
|
<string name="key_verification_request_fallback_message">%s is requesting to verify your key, but your client does not support in-chat key verification. You will need to use legacy key verification to verify keys.</string>
|
||||||
|
|
||||||
|
<string name="failed_to_initialize_cross_signing">Failed to set up Cross Signing</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -242,6 +242,27 @@
|
|||||||
<activity android:name=".features.home.room.detail.search.SearchActivity" />
|
<activity android:name=".features.home.room.detail.search.SearchActivity" />
|
||||||
<activity android:name=".features.usercode.UserCodeActivity" />
|
<activity android:name=".features.usercode.UserCodeActivity" />
|
||||||
|
|
||||||
|
<!-- Single instance is very important for the custom scheme callback-->
|
||||||
|
<activity android:name=".features.auth.ReAuthActivity"
|
||||||
|
android:launchMode="singleInstance"
|
||||||
|
android:exported="false">
|
||||||
|
|
||||||
|
<!-- XXX: UIA SSO has only web fallback, i.e no url redirect, so for now we comment this out
|
||||||
|
hopefully, we would use it when finalyy available
|
||||||
|
-->
|
||||||
|
<!-- 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:host="reauth"-->
|
||||||
|
<!-- android:scheme="element" />-->
|
||||||
|
<!-- </intent-filter>-->
|
||||||
|
</activity>
|
||||||
|
|
||||||
<!-- Services -->
|
<!-- Services -->
|
||||||
|
|
||||||
<service
|
<service
|
||||||
|
@ -25,6 +25,7 @@ import im.vector.app.core.dialogs.UnrecognizedCertificateDialog
|
|||||||
import im.vector.app.core.error.ErrorFormatter
|
import im.vector.app.core.error.ErrorFormatter
|
||||||
import im.vector.app.core.preference.UserAvatarPreference
|
import im.vector.app.core.preference.UserAvatarPreference
|
||||||
import im.vector.app.features.MainActivity
|
import im.vector.app.features.MainActivity
|
||||||
|
import im.vector.app.features.auth.ReAuthActivity
|
||||||
import im.vector.app.features.call.CallControlsBottomSheet
|
import im.vector.app.features.call.CallControlsBottomSheet
|
||||||
import im.vector.app.features.call.VectorCallActivity
|
import im.vector.app.features.call.VectorCallActivity
|
||||||
import im.vector.app.features.call.conference.VectorJitsiActivity
|
import im.vector.app.features.call.conference.VectorJitsiActivity
|
||||||
@ -145,6 +146,7 @@ interface ScreenComponent {
|
|||||||
fun inject(activity: VectorJitsiActivity)
|
fun inject(activity: VectorJitsiActivity)
|
||||||
fun inject(activity: SearchActivity)
|
fun inject(activity: SearchActivity)
|
||||||
fun inject(activity: UserCodeActivity)
|
fun inject(activity: UserCodeActivity)
|
||||||
|
fun inject(activity: ReAuthActivity)
|
||||||
|
|
||||||
/* ==========================================================================================
|
/* ==========================================================================================
|
||||||
* BottomSheets
|
* BottomSheets
|
||||||
|
@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* 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.app.core.ui.list
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
|
import com.google.android.material.button.MaterialButton
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||||
|
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A generic button list item.
|
||||||
|
*/
|
||||||
|
@EpoxyModelClass(layout = R.layout.item_positive_button)
|
||||||
|
abstract class GenericPositiveButtonItem : VectorEpoxyModel<GenericPositiveButtonItem.Holder>() {
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
var text: String? = null
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
var buttonClickAction: View.OnClickListener? = null
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
@ColorInt
|
||||||
|
var textColor: Int? = null
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
@DrawableRes
|
||||||
|
var iconRes: Int? = null
|
||||||
|
|
||||||
|
override fun bind(holder: Holder) {
|
||||||
|
super.bind(holder)
|
||||||
|
holder.button.text = text
|
||||||
|
if (iconRes != null) {
|
||||||
|
holder.button.setIconResource(iconRes!!)
|
||||||
|
} else {
|
||||||
|
holder.button.icon = null
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonClickAction?.let { holder.button.setOnClickListener(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Holder : VectorEpoxyHolder() {
|
||||||
|
val button by bind<MaterialButton>(R.id.itemGenericItemButton)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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.app.features.auth
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import com.airbnb.mvrx.activityViewModel
|
||||||
|
import com.airbnb.mvrx.withState
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.extensions.showPassword
|
||||||
|
import im.vector.app.core.platform.VectorBaseFragment
|
||||||
|
import im.vector.app.databinding.FragmentReauthConfirmBinding
|
||||||
|
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||||
|
|
||||||
|
class PromptFragment : VectorBaseFragment<FragmentReauthConfirmBinding>() {
|
||||||
|
|
||||||
|
private val viewModel: ReAuthViewModel by activityViewModel()
|
||||||
|
|
||||||
|
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) =
|
||||||
|
FragmentReauthConfirmBinding.inflate(layoutInflater, container, false)
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
views.reAuthConfirmButton.debouncedClicks {
|
||||||
|
onButtonClicked()
|
||||||
|
}
|
||||||
|
views.passwordReveal.debouncedClicks {
|
||||||
|
viewModel.handle(ReAuthActions.StartSSOFallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
views.passwordReveal.debouncedClicks {
|
||||||
|
viewModel.handle(ReAuthActions.TogglePassVisibility)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onButtonClicked() = withState(viewModel) { state ->
|
||||||
|
if (state.flowType == LoginFlowTypes.SSO) {
|
||||||
|
viewModel.handle(ReAuthActions.StartSSOFallback)
|
||||||
|
} else if (state.flowType == LoginFlowTypes.PASSWORD) {
|
||||||
|
val password = views.passwordField.text.toString()
|
||||||
|
if (password.isBlank()) {
|
||||||
|
// Prompt to enter something
|
||||||
|
views.passwordFieldTil.error = getString(R.string.error_empty_field_your_password)
|
||||||
|
} else {
|
||||||
|
views.passwordFieldTil.error = null
|
||||||
|
viewModel.handle(ReAuthActions.ReAuthWithPass(password))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// not supported
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invalidate() = withState(viewModel) {
|
||||||
|
when (it.flowType) {
|
||||||
|
LoginFlowTypes.SSO -> {
|
||||||
|
views.passwordContainer.isVisible = false
|
||||||
|
views.reAuthConfirmButton.text = getString(R.string.auth_login_sso)
|
||||||
|
}
|
||||||
|
LoginFlowTypes.PASSWORD -> {
|
||||||
|
views.passwordContainer.isVisible = true
|
||||||
|
views.reAuthConfirmButton.text = getString(R.string._continue)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// This login flow is not supported, you should use web?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
views.passwordField.showPassword(it.passwordVisible)
|
||||||
|
|
||||||
|
if (it.passwordVisible) {
|
||||||
|
views.passwordReveal.setImageResource(R.drawable.ic_eye_closed)
|
||||||
|
views.passwordReveal.contentDescription = getString(R.string.a11y_hide_password)
|
||||||
|
} else {
|
||||||
|
views.passwordReveal.setImageResource(R.drawable.ic_eye)
|
||||||
|
views.passwordReveal.contentDescription = getString(R.string.a11y_show_password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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.app.features.auth
|
||||||
|
|
||||||
|
import im.vector.app.core.platform.VectorViewModelAction
|
||||||
|
|
||||||
|
sealed class ReAuthActions : VectorViewModelAction {
|
||||||
|
object StartSSOFallback : ReAuthActions()
|
||||||
|
object FallBackPageLoaded : ReAuthActions()
|
||||||
|
object FallBackPageClosed : ReAuthActions()
|
||||||
|
object TogglePassVisibility : ReAuthActions()
|
||||||
|
data class ReAuthWithPass(val password: String) : ReAuthActions()
|
||||||
|
}
|
@ -0,0 +1,213 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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.app.features.auth
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.browser.customtabs.CustomTabsCallback
|
||||||
|
import androidx.browser.customtabs.CustomTabsClient
|
||||||
|
import androidx.browser.customtabs.CustomTabsServiceConnection
|
||||||
|
import androidx.browser.customtabs.CustomTabsSession
|
||||||
|
import com.airbnb.mvrx.MvRx
|
||||||
|
import com.airbnb.mvrx.viewModel
|
||||||
|
import com.airbnb.mvrx.withState
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.di.ScreenComponent
|
||||||
|
import im.vector.app.core.extensions.addFragment
|
||||||
|
import im.vector.app.core.platform.SimpleFragmentActivity
|
||||||
|
import im.vector.app.core.utils.openUrlInChromeCustomTab
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import org.matrix.android.sdk.api.auth.AuthenticationService
|
||||||
|
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||||
|
import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class ReAuthActivity : SimpleFragmentActivity(), ReAuthViewModel.Factory {
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class Args(
|
||||||
|
val flowType: String?,
|
||||||
|
val title: String?,
|
||||||
|
val session: String?
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
// For sso
|
||||||
|
private var customTabsServiceConnection: CustomTabsServiceConnection? = null
|
||||||
|
private var customTabsClient: CustomTabsClient? = null
|
||||||
|
private var customTabsSession: CustomTabsSession? = null
|
||||||
|
|
||||||
|
@Inject lateinit var authenticationService: AuthenticationService
|
||||||
|
@Inject lateinit var reAuthViewModelFactory: ReAuthViewModel.Factory
|
||||||
|
|
||||||
|
override fun create(initialState: ReAuthState) = reAuthViewModelFactory.create(initialState)
|
||||||
|
|
||||||
|
override fun injectWith(injector: ScreenComponent) {
|
||||||
|
super.injectWith(injector)
|
||||||
|
injector.inject(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val sharedViewModel: ReAuthViewModel by viewModel()
|
||||||
|
|
||||||
|
// override fun getTitleRes() = R.string.re_authentication_activity_title
|
||||||
|
|
||||||
|
override fun initUiAndData() {
|
||||||
|
super.initUiAndData()
|
||||||
|
|
||||||
|
val title = intent.extras?.getString(EXTRA_REASON_TITLE) ?: getString(R.string.re_authentication_activity_title)
|
||||||
|
supportActionBar?.setTitle(title) ?: run { setTitle(title) }
|
||||||
|
|
||||||
|
// val authArgs = intent.getParcelableExtra<Args>(MvRx.KEY_ARG)
|
||||||
|
|
||||||
|
// For the sso flow we can for now only rely on the fallback flow, that handles all
|
||||||
|
// the UI, due to the sandbox nature of CCT (chrome custom tab) we cannot get much information
|
||||||
|
// on how the process did go :/
|
||||||
|
// so we assume that after the user close the tab we return success and let caller retry the UIA flow :/
|
||||||
|
|
||||||
|
addFragment(
|
||||||
|
R.id.container,
|
||||||
|
PromptFragment::class.java
|
||||||
|
)
|
||||||
|
|
||||||
|
sharedViewModel.observeViewEvents {
|
||||||
|
when (it) {
|
||||||
|
is ReAuthEvents.OpenSsoURl -> {
|
||||||
|
openInCustomTab(it.url)
|
||||||
|
}
|
||||||
|
ReAuthEvents.Dismiss -> {
|
||||||
|
setResult(RESULT_CANCELED)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
is ReAuthEvents.PasswordFinishSuccess -> {
|
||||||
|
setResult(RESULT_OK, Intent().apply {
|
||||||
|
putExtra(RESULT_FLOW_TYPE, LoginFlowTypes.PASSWORD)
|
||||||
|
putExtra(RESULT_VALUE, it.password)
|
||||||
|
})
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
// It's the only way we have to know if sso falback flow was successful
|
||||||
|
withState(sharedViewModel) {
|
||||||
|
if (it.ssoFallbackPageWasShown) {
|
||||||
|
Timber.d("## UIA ssoFallbackPageWasShown tentative success")
|
||||||
|
setResult(RESULT_OK, Intent().apply {
|
||||||
|
putExtra(RESULT_FLOW_TYPE, LoginFlowTypes.SSO)
|
||||||
|
})
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() = withState(sharedViewModel) { state ->
|
||||||
|
super.onStart()
|
||||||
|
|
||||||
|
if (state.ssoFallbackPageWasShown) {
|
||||||
|
sharedViewModel.handle(ReAuthActions.FallBackPageClosed)
|
||||||
|
return@withState
|
||||||
|
}
|
||||||
|
|
||||||
|
val packageName = CustomTabsClient.getPackageName(this, 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) {
|
||||||
|
Timber.d("## CustomTab onCustomTabsServiceConnected($name)")
|
||||||
|
customTabsClient = client
|
||||||
|
.also { it.warmup(0L) }
|
||||||
|
customTabsSession = customTabsClient?.newSession(object : CustomTabsCallback() {
|
||||||
|
// override fun onPostMessage(message: String, extras: Bundle?) {
|
||||||
|
// Timber.v("## CustomTab onPostMessage($message)")
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// override fun onMessageChannelReady(extras: Bundle?) {
|
||||||
|
// Timber.v("## CustomTab onMessageChannelReady()")
|
||||||
|
// }
|
||||||
|
|
||||||
|
override fun onNavigationEvent(navigationEvent: Int, extras: Bundle?) {
|
||||||
|
Timber.v("## CustomTab onNavigationEvent($navigationEvent), $extras")
|
||||||
|
super.onNavigationEvent(navigationEvent, extras)
|
||||||
|
if (navigationEvent == NAVIGATION_FINISHED) {
|
||||||
|
sharedViewModel.handle(ReAuthActions.FallBackPageLoaded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRelationshipValidationResult(relation: Int, requestedOrigin: Uri, result: Boolean, extras: Bundle?) {
|
||||||
|
Timber.v("## CustomTab onRelationshipValidationResult($relation), $requestedOrigin")
|
||||||
|
super.onRelationshipValidationResult(relation, requestedOrigin, result, extras)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
|
Timber.d("## CustomTab onServiceDisconnected($name)")
|
||||||
|
}
|
||||||
|
}.also {
|
||||||
|
CustomTabsClient.bindCustomTabsService(
|
||||||
|
this,
|
||||||
|
// Despite the API, packageName cannot be null
|
||||||
|
packageName,
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
customTabsServiceConnection?.let { this.unbindService(it) }
|
||||||
|
customTabsServiceConnection = null
|
||||||
|
customTabsSession = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openInCustomTab(ssoUrl: String) {
|
||||||
|
openUrlInChromeCustomTab(this, customTabsSession, ssoUrl)
|
||||||
|
val channelOpened = customTabsSession?.requestPostMessageChannel(Uri.parse("https://element.io"))
|
||||||
|
Timber.d("## CustomTab channelOpened: $channelOpened")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val EXTRA_AUTH_TYPE = "EXTRA_AUTH_TYPE"
|
||||||
|
const val EXTRA_REASON_TITLE = "EXTRA_REASON_TITLE"
|
||||||
|
const val RESULT_FLOW_TYPE = "RESULT_FLOW_TYPE"
|
||||||
|
const val RESULT_VALUE = "RESULT_VALUE"
|
||||||
|
|
||||||
|
fun newIntent(context: Context, fromError: RegistrationFlowResponse, reasonTitle: String?): Intent {
|
||||||
|
val authType = if (fromError.flows.orEmpty().any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true }) {
|
||||||
|
LoginFlowTypes.PASSWORD
|
||||||
|
} else if (fromError.flows.orEmpty().any { it.stages?.contains(LoginFlowTypes.SSO) == true }) {
|
||||||
|
LoginFlowTypes.SSO
|
||||||
|
} else {
|
||||||
|
// TODO, support more auth type?
|
||||||
|
null
|
||||||
|
}
|
||||||
|
return Intent(context, ReAuthActivity::class.java).apply {
|
||||||
|
putExtra(MvRx.KEY_ARG, Args(authType, reasonTitle, fromError.session))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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.app.features.auth
|
||||||
|
|
||||||
|
import im.vector.app.core.platform.VectorViewEvents
|
||||||
|
|
||||||
|
sealed class ReAuthEvents : VectorViewEvents {
|
||||||
|
data class OpenSsoURl(val url: String) : ReAuthEvents()
|
||||||
|
object Dismiss : ReAuthEvents()
|
||||||
|
data class PasswordFinishSuccess(val password: String) : ReAuthEvents()
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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.app.features.auth
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.MvRxState
|
||||||
|
|
||||||
|
data class ReAuthState(
|
||||||
|
val title: String? = null,
|
||||||
|
val session: String? = null,
|
||||||
|
val flowType: String? = null,
|
||||||
|
val ssoFallbackPageWasShown: Boolean = false,
|
||||||
|
val passwordVisible: Boolean = false
|
||||||
|
) : MvRxState {
|
||||||
|
constructor(args: ReAuthActivity.Args) : this(
|
||||||
|
args.title,
|
||||||
|
args.session,
|
||||||
|
args.flowType
|
||||||
|
)
|
||||||
|
|
||||||
|
constructor() : this(null, null)
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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.app.features.auth
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.ActivityViewModelContext
|
||||||
|
import com.airbnb.mvrx.FragmentViewModelContext
|
||||||
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
|
import org.matrix.android.sdk.api.auth.AuthenticationService
|
||||||
|
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||||
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
|
||||||
|
class ReAuthViewModel @AssistedInject constructor(
|
||||||
|
@Assisted val initialState: ReAuthState,
|
||||||
|
private val session: Session,
|
||||||
|
private val authenticationService: AuthenticationService
|
||||||
|
) : VectorViewModel<ReAuthState, ReAuthActions, ReAuthEvents>(initialState) {
|
||||||
|
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory {
|
||||||
|
fun create(initialState: ReAuthState): ReAuthViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : MvRxViewModelFactory<ReAuthViewModel, ReAuthState> {
|
||||||
|
|
||||||
|
override fun create(viewModelContext: ViewModelContext, state: ReAuthState): ReAuthViewModel? {
|
||||||
|
val factory = when (viewModelContext) {
|
||||||
|
is FragmentViewModelContext -> viewModelContext.fragment as? Factory
|
||||||
|
is ActivityViewModelContext -> viewModelContext.activity as? Factory
|
||||||
|
}
|
||||||
|
return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handle(action: ReAuthActions) = withState { state ->
|
||||||
|
when (action) {
|
||||||
|
ReAuthActions.StartSSOFallback -> {
|
||||||
|
if (state.flowType == LoginFlowTypes.SSO) {
|
||||||
|
val ssoURL = session.getUIASsoFallbackUrl(initialState.session ?: "")
|
||||||
|
_viewEvents.post(ReAuthEvents.OpenSsoURl(ssoURL))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ReAuthActions.FallBackPageLoaded -> {
|
||||||
|
setState { copy(ssoFallbackPageWasShown = true) }
|
||||||
|
}
|
||||||
|
ReAuthActions.FallBackPageClosed -> {
|
||||||
|
// Should we do something here?
|
||||||
|
}
|
||||||
|
ReAuthActions.TogglePassVisibility -> {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
passwordVisible = !state.passwordVisible
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is ReAuthActions.ReAuthWithPass -> {
|
||||||
|
_viewEvents.post(ReAuthEvents.PasswordFinishSuccess(action.password))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -20,6 +20,7 @@ import im.vector.app.R
|
|||||||
import im.vector.app.core.platform.ViewModelTask
|
import im.vector.app.core.platform.ViewModelTask
|
||||||
import im.vector.app.core.platform.WaitingViewData
|
import im.vector.app.core.platform.WaitingViewData
|
||||||
import im.vector.app.core.resources.StringProvider
|
import im.vector.app.core.resources.StringProvider
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||||
import org.matrix.android.sdk.api.failure.Failure
|
import org.matrix.android.sdk.api.failure.Failure
|
||||||
import org.matrix.android.sdk.api.failure.MatrixError
|
import org.matrix.android.sdk.api.failure.MatrixError
|
||||||
@ -33,16 +34,21 @@ import org.matrix.android.sdk.api.session.securestorage.EmptyKeySigner
|
|||||||
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
|
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
|
||||||
import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo
|
import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo
|
||||||
import org.matrix.android.sdk.api.session.securestorage.SsssKeySpec
|
import org.matrix.android.sdk.api.session.securestorage.SsssKeySpec
|
||||||
|
import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
|
import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
|
import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion
|
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult
|
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
|
import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
|
||||||
|
import org.matrix.android.sdk.internal.crypto.model.rest.UIABaseAuth
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
||||||
import org.matrix.android.sdk.internal.util.awaitCallback
|
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.lang.UnsupportedOperationException
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.coroutines.Continuation
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
sealed class BootstrapResult {
|
sealed class BootstrapResult {
|
||||||
|
|
||||||
@ -101,7 +107,21 @@ class BootstrapCrossSigningTask @Inject constructor(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
awaitCallback<Unit> {
|
awaitCallback<Unit> {
|
||||||
crossSigningService.initializeCrossSigning(params.userPasswordAuth, it)
|
crossSigningService.initializeCrossSigning(object : UserInteractiveAuthInterceptor {
|
||||||
|
override fun performStage(flowResponse: RegistrationFlowResponse, promise: Continuation<UIABaseAuth>) {
|
||||||
|
if (flowResponse.flows?.any { it.type == LoginFlowTypes.PASSWORD } == true) {
|
||||||
|
val updatedAuth = params.userPasswordAuth?.copy(session = flowResponse.session)
|
||||||
|
if (updatedAuth == null) {
|
||||||
|
promise.resumeWith(Result.failure(UnsupportedOperationException()))
|
||||||
|
} else {
|
||||||
|
promise.resume(updatedAuth)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
promise.resumeWith(Result.failure(UnsupportedOperationException()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
it)
|
||||||
}
|
}
|
||||||
if (params.setupMode == SetupMode.CROSS_SIGNING_ONLY) {
|
if (params.setupMode == SetupMode.CROSS_SIGNING_ONLY) {
|
||||||
return BootstrapResult.SuccessCrossSigningOnly
|
return BootstrapResult.SuccessCrossSigningOnly
|
||||||
|
@ -21,8 +21,8 @@ import com.airbnb.mvrx.MvRx
|
|||||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
import com.airbnb.mvrx.ViewModelContext
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
import im.vector.app.core.di.ActiveSessionHolder
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
import im.vector.app.core.extensions.exhaustive
|
import im.vector.app.core.extensions.exhaustive
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
@ -33,17 +33,23 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
import org.matrix.android.sdk.api.NoOpMatrixCallback
|
import org.matrix.android.sdk.api.NoOpMatrixCallback
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
|
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||||
import org.matrix.android.sdk.api.pushrules.RuleIds
|
import org.matrix.android.sdk.api.pushrules.RuleIds
|
||||||
import org.matrix.android.sdk.api.session.InitialSyncProgressService
|
import org.matrix.android.sdk.api.session.InitialSyncProgressService
|
||||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||||
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
|
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
|
||||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||||
|
import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse
|
||||||
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
||||||
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
|
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
|
||||||
|
import org.matrix.android.sdk.internal.crypto.model.rest.UIABaseAuth
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
||||||
import org.matrix.android.sdk.rx.asObservable
|
import org.matrix.android.sdk.rx.asObservable
|
||||||
import org.matrix.android.sdk.rx.rx
|
import org.matrix.android.sdk.rx.rx
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import kotlin.coroutines.Continuation
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
class HomeActivityViewModel @AssistedInject constructor(
|
class HomeActivityViewModel @AssistedInject constructor(
|
||||||
@Assisted initialState: HomeActivityViewState,
|
@Assisted initialState: HomeActivityViewState,
|
||||||
@ -122,7 +128,7 @@ class HomeActivityViewModel @AssistedInject constructor(
|
|||||||
// Schedule a check of the bootstrap when the init sync will be finished
|
// Schedule a check of the bootstrap when the init sync will be finished
|
||||||
checkBootstrap = true
|
checkBootstrap = true
|
||||||
}
|
}
|
||||||
is InitialSyncProgressService.Status.Idle -> {
|
is InitialSyncProgressService.Status.Idle -> {
|
||||||
if (checkBootstrap) {
|
if (checkBootstrap) {
|
||||||
checkBootstrap = false
|
checkBootstrap = false
|
||||||
maybeBootstrapCrossSigning()
|
maybeBootstrapCrossSigning()
|
||||||
@ -152,11 +158,19 @@ class HomeActivityViewModel @AssistedInject constructor(
|
|||||||
// We do not use the viewModel context because we do not want to cancel this action
|
// We do not use the viewModel context because we do not want to cancel this action
|
||||||
Timber.d("Initialize cross signing")
|
Timber.d("Initialize cross signing")
|
||||||
session.cryptoService().crossSigningService().initializeCrossSigning(
|
session.cryptoService().crossSigningService().initializeCrossSigning(
|
||||||
authParams = UserPasswordAuth(
|
object : UserInteractiveAuthInterceptor {
|
||||||
session = null,
|
override fun performStage(flow: RegistrationFlowResponse, promise: Continuation<UIABaseAuth>) {
|
||||||
user = session.myUserId,
|
if (flow.flows?.any { it.type == LoginFlowTypes.PASSWORD } == true) {
|
||||||
password = password
|
promise.resume(
|
||||||
),
|
UserPasswordAuth(
|
||||||
|
session = flow.session,
|
||||||
|
user = session.myUserId,
|
||||||
|
password = password
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else promise.resumeWith(Result.failure(UnsupportedOperationException()))
|
||||||
|
}
|
||||||
|
},
|
||||||
callback = NoOpMatrixCallback()
|
callback = NoOpMatrixCallback()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -236,11 +250,17 @@ class HomeActivityViewModel @AssistedInject constructor(
|
|||||||
// We do not use the viewModel context because we do not want to cancel this action
|
// We do not use the viewModel context because we do not want to cancel this action
|
||||||
Timber.d("Initialize cross signing")
|
Timber.d("Initialize cross signing")
|
||||||
session.cryptoService().crossSigningService().initializeCrossSigning(
|
session.cryptoService().crossSigningService().initializeCrossSigning(
|
||||||
authParams = UserPasswordAuth(
|
object : UserInteractiveAuthInterceptor {
|
||||||
session = null,
|
override fun performStage(flow: RegistrationFlowResponse, promise: Continuation<UIABaseAuth>) {
|
||||||
user = session.myUserId,
|
if (flow.flows?.any { it.type == LoginFlowTypes.PASSWORD } == true) {
|
||||||
password = password
|
UserPasswordAuth(
|
||||||
),
|
session = flow.session,
|
||||||
|
user = session.myUserId,
|
||||||
|
password = password
|
||||||
|
)
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
},
|
||||||
callback = NoOpMatrixCallback()
|
callback = NoOpMatrixCallback()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -311,10 +311,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
mCrossSigningStatePreference.isVisible = true
|
mCrossSigningStatePreference.isVisible = true
|
||||||
if (!vectorPreferences.developerMode()) {
|
|
||||||
// When not in developer mode, intercept click on this preference
|
|
||||||
mCrossSigningStatePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { true }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val saveMegolmStartForActivityResult = registerStartForActivityResult {
|
private val saveMegolmStartForActivityResult = registerStartForActivityResult {
|
||||||
|
@ -18,4 +18,9 @@ package im.vector.app.features.settings.crosssigning
|
|||||||
|
|
||||||
import im.vector.app.core.platform.VectorViewModelAction
|
import im.vector.app.core.platform.VectorViewModelAction
|
||||||
|
|
||||||
sealed class CrossSigningSettingsAction : VectorViewModelAction
|
sealed class CrossSigningSettingsAction : VectorViewModelAction {
|
||||||
|
object InitializeCrossSigning: CrossSigningSettingsAction()
|
||||||
|
object SsoAuthDone: CrossSigningSettingsAction()
|
||||||
|
data class PasswordAuthDone(val password: String): CrossSigningSettingsAction()
|
||||||
|
object ReAuthCancelled: CrossSigningSettingsAction()
|
||||||
|
}
|
||||||
|
@ -19,8 +19,11 @@ import com.airbnb.epoxy.TypedEpoxyController
|
|||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.resources.ColorProvider
|
import im.vector.app.core.resources.ColorProvider
|
||||||
import im.vector.app.core.resources.StringProvider
|
import im.vector.app.core.resources.StringProvider
|
||||||
|
import im.vector.app.core.ui.list.genericButtonItem
|
||||||
import im.vector.app.core.ui.list.genericItem
|
import im.vector.app.core.ui.list.genericItem
|
||||||
import im.vector.app.core.ui.list.genericItemWithValue
|
import im.vector.app.core.ui.list.genericItemWithValue
|
||||||
|
import im.vector.app.core.ui.list.genericPositiveButtonItem
|
||||||
|
import im.vector.app.core.utils.DebouncedClickListener
|
||||||
import im.vector.app.core.utils.DimensionConverter
|
import im.vector.app.core.utils.DimensionConverter
|
||||||
import me.gujun.android.span.span
|
import me.gujun.android.span.span
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -31,7 +34,9 @@ class CrossSigningSettingsController @Inject constructor(
|
|||||||
private val dimensionConverter: DimensionConverter
|
private val dimensionConverter: DimensionConverter
|
||||||
) : TypedEpoxyController<CrossSigningSettingsViewState>() {
|
) : TypedEpoxyController<CrossSigningSettingsViewState>() {
|
||||||
|
|
||||||
interface InteractionListener
|
interface InteractionListener {
|
||||||
|
fun didTapInitializeCrossSigning()
|
||||||
|
}
|
||||||
|
|
||||||
var interactionListener: InteractionListener? = null
|
var interactionListener: InteractionListener? = null
|
||||||
|
|
||||||
@ -44,6 +49,13 @@ class CrossSigningSettingsController @Inject constructor(
|
|||||||
titleIconResourceId(R.drawable.ic_shield_trusted)
|
titleIconResourceId(R.drawable.ic_shield_trusted)
|
||||||
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_complete))
|
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_complete))
|
||||||
}
|
}
|
||||||
|
genericButtonItem {
|
||||||
|
id("Reset")
|
||||||
|
text(stringProvider.getString(R.string.reset_cross_signing))
|
||||||
|
buttonClickAction(DebouncedClickListener({
|
||||||
|
interactionListener?.didTapInitializeCrossSigning()
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
data.xSigningKeysAreTrusted -> {
|
data.xSigningKeysAreTrusted -> {
|
||||||
genericItem {
|
genericItem {
|
||||||
@ -51,6 +63,13 @@ class CrossSigningSettingsController @Inject constructor(
|
|||||||
titleIconResourceId(R.drawable.ic_shield_custom)
|
titleIconResourceId(R.drawable.ic_shield_custom)
|
||||||
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_trusted))
|
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_trusted))
|
||||||
}
|
}
|
||||||
|
genericButtonItem {
|
||||||
|
id("Reset")
|
||||||
|
text(stringProvider.getString(R.string.reset_cross_signing))
|
||||||
|
buttonClickAction(DebouncedClickListener({
|
||||||
|
interactionListener?.didTapInitializeCrossSigning()
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
data.xSigningIsEnableInAccount -> {
|
data.xSigningIsEnableInAccount -> {
|
||||||
genericItem {
|
genericItem {
|
||||||
@ -58,12 +77,27 @@ class CrossSigningSettingsController @Inject constructor(
|
|||||||
titleIconResourceId(R.drawable.ic_shield_black)
|
titleIconResourceId(R.drawable.ic_shield_black)
|
||||||
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_not_trusted))
|
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_not_trusted))
|
||||||
}
|
}
|
||||||
|
genericButtonItem {
|
||||||
|
id("Reset")
|
||||||
|
text(stringProvider.getString(R.string.reset_cross_signing))
|
||||||
|
buttonClickAction(DebouncedClickListener({
|
||||||
|
interactionListener?.didTapInitializeCrossSigning()
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
genericItem {
|
genericItem {
|
||||||
id("not")
|
id("not")
|
||||||
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_disabled))
|
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_disabled))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
genericPositiveButtonItem {
|
||||||
|
id("Initialize")
|
||||||
|
text(stringProvider.getString(R.string.initialize_cross_signing))
|
||||||
|
buttonClickAction(DebouncedClickListener({
|
||||||
|
interactionListener?.didTapInitializeCrossSigning()
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,20 +15,26 @@
|
|||||||
*/
|
*/
|
||||||
package im.vector.app.features.settings.crosssigning
|
package im.vector.app.features.settings.crosssigning
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.cleanup
|
import im.vector.app.core.extensions.cleanup
|
||||||
import im.vector.app.core.extensions.configureWith
|
import im.vector.app.core.extensions.configureWith
|
||||||
import im.vector.app.core.extensions.exhaustive
|
import im.vector.app.core.extensions.exhaustive
|
||||||
|
import im.vector.app.core.extensions.registerStartForActivityResult
|
||||||
|
import im.vector.app.core.extensions.setTextOrHide
|
||||||
import im.vector.app.core.platform.VectorBaseFragment
|
import im.vector.app.core.platform.VectorBaseFragment
|
||||||
import im.vector.app.databinding.FragmentGenericRecyclerBinding
|
import im.vector.app.databinding.FragmentGenericRecyclerBinding
|
||||||
|
import im.vector.app.features.auth.ReAuthActivity
|
||||||
|
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||||
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -47,19 +53,52 @@ class CrossSigningSettingsFragment @Inject constructor(
|
|||||||
|
|
||||||
private val viewModel: CrossSigningSettingsViewModel by fragmentViewModel()
|
private val viewModel: CrossSigningSettingsViewModel by fragmentViewModel()
|
||||||
|
|
||||||
|
private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult ->
|
||||||
|
if (activityResult.resultCode == Activity.RESULT_OK) {
|
||||||
|
when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) {
|
||||||
|
LoginFlowTypes.SSO -> {
|
||||||
|
viewModel.handle(CrossSigningSettingsAction.SsoAuthDone)
|
||||||
|
}
|
||||||
|
LoginFlowTypes.PASSWORD -> {
|
||||||
|
val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: ""
|
||||||
|
viewModel.handle(CrossSigningSettingsAction.PasswordAuthDone(password))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
viewModel.handle(CrossSigningSettingsAction.ReAuthCancelled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// activityResult.data?.extras?.getString(ReAuthActivity.RESULT_TOKEN)?.let { token ->
|
||||||
|
// }
|
||||||
|
} else {
|
||||||
|
viewModel.handle(CrossSigningSettingsAction.ReAuthCancelled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
setupRecyclerView()
|
setupRecyclerView()
|
||||||
viewModel.observeViewEvents {
|
viewModel.observeViewEvents { event ->
|
||||||
when (it) {
|
when (event) {
|
||||||
is CrossSigningSettingsViewEvents.Failure -> {
|
is CrossSigningSettingsViewEvents.Failure -> {
|
||||||
AlertDialog.Builder(requireContext())
|
AlertDialog.Builder(requireContext())
|
||||||
.setTitle(R.string.dialog_title_error)
|
.setTitle(R.string.dialog_title_error)
|
||||||
.setMessage(errorFormatter.toHumanReadable(it.throwable))
|
.setMessage(errorFormatter.toHumanReadable(event.throwable))
|
||||||
.setPositiveButton(R.string.ok, null)
|
.setPositiveButton(R.string.ok, null)
|
||||||
.show()
|
.show()
|
||||||
Unit
|
Unit
|
||||||
}
|
}
|
||||||
|
is CrossSigningSettingsViewEvents.RequestReAuth -> {
|
||||||
|
ReAuthActivity.newIntent(requireContext(), event.registrationFlowResponse, getString(R.string.initialize_cross_signing)).let { intent ->
|
||||||
|
reAuthActivityResultLauncher.launch(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is CrossSigningSettingsViewEvents.ShowModalWaitingView -> {
|
||||||
|
views.waitingView.waitingView.isVisible = true
|
||||||
|
views.waitingView.waitingStatusText.setTextOrHide(event.status)
|
||||||
|
}
|
||||||
|
CrossSigningSettingsViewEvents.HideModalWaitingView -> {
|
||||||
|
views.waitingView.waitingView.isVisible = false
|
||||||
|
}
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -83,4 +122,8 @@ class CrossSigningSettingsFragment @Inject constructor(
|
|||||||
controller.interactionListener = null
|
controller.interactionListener = null
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun didTapInitializeCrossSigning() {
|
||||||
|
viewModel.handle(CrossSigningSettingsAction.InitializeCrossSigning)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,10 +17,14 @@
|
|||||||
package im.vector.app.features.settings.crosssigning
|
package im.vector.app.features.settings.crosssigning
|
||||||
|
|
||||||
import im.vector.app.core.platform.VectorViewEvents
|
import im.vector.app.core.platform.VectorViewEvents
|
||||||
|
import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transient events for cross signing settings screen
|
* Transient events for cross signing settings screen
|
||||||
*/
|
*/
|
||||||
sealed class CrossSigningSettingsViewEvents : VectorViewEvents {
|
sealed class CrossSigningSettingsViewEvents : VectorViewEvents {
|
||||||
data class Failure(val throwable: Throwable) : CrossSigningSettingsViewEvents()
|
data class Failure(val throwable: Throwable) : CrossSigningSettingsViewEvents()
|
||||||
|
data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse) : CrossSigningSettingsViewEvents()
|
||||||
|
data class ShowModalWaitingView(val status: String?) : CrossSigningSettingsViewEvents()
|
||||||
|
object HideModalWaitingView : CrossSigningSettingsViewEvents()
|
||||||
}
|
}
|
||||||
|
@ -15,25 +15,45 @@
|
|||||||
*/
|
*/
|
||||||
package im.vector.app.features.settings.crosssigning
|
package im.vector.app.features.settings.crosssigning
|
||||||
|
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.airbnb.mvrx.FragmentViewModelContext
|
import com.airbnb.mvrx.FragmentViewModelContext
|
||||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
import com.airbnb.mvrx.ViewModelContext
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.extensions.exhaustive
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
|
import im.vector.app.core.resources.StringProvider
|
||||||
|
import im.vector.app.features.login.ReAuthHelper
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import io.reactivex.functions.BiFunction
|
import io.reactivex.functions.BiFunction
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
|
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||||
import org.matrix.android.sdk.api.util.Optional
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
|
import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.isVerified
|
import org.matrix.android.sdk.internal.crypto.crosssigning.isVerified
|
||||||
|
import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
|
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
|
||||||
|
import org.matrix.android.sdk.internal.crypto.model.rest.UIABaseAuth
|
||||||
|
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
||||||
|
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||||
import org.matrix.android.sdk.rx.rx
|
import org.matrix.android.sdk.rx.rx
|
||||||
|
import timber.log.Timber
|
||||||
|
import kotlin.coroutines.Continuation
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted private val initialState: CrossSigningSettingsViewState,
|
class CrossSigningSettingsViewModel @AssistedInject constructor(
|
||||||
private val session: Session)
|
@Assisted private val initialState: CrossSigningSettingsViewState,
|
||||||
: VectorViewModel<CrossSigningSettingsViewState, CrossSigningSettingsAction, CrossSigningSettingsViewEvents>(initialState) {
|
private val session: Session,
|
||||||
|
private val reAuthHelper: ReAuthHelper,
|
||||||
|
private val stringProvider: StringProvider
|
||||||
|
) : VectorViewModel<CrossSigningSettingsViewState, CrossSigningSettingsAction, CrossSigningSettingsViewEvents>(initialState) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Observable.combineLatest<List<DeviceInfo>, Optional<MXCrossSigningInfo>, Pair<List<DeviceInfo>, Optional<MXCrossSigningInfo>>>(
|
Observable.combineLatest<List<DeviceInfo>, Optional<MXCrossSigningInfo>, Pair<List<DeviceInfo>, Optional<MXCrossSigningInfo>>>(
|
||||||
@ -58,15 +78,82 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted privat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var uiaContinuation: Continuation<UIABaseAuth>? = null
|
||||||
|
var pendingAuth: UIABaseAuth? = null
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(initialState: CrossSigningSettingsViewState): CrossSigningSettingsViewModel
|
fun create(initialState: CrossSigningSettingsViewState): CrossSigningSettingsViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handle(action: CrossSigningSettingsAction) {
|
override fun handle(action: CrossSigningSettingsAction) = withState { state ->
|
||||||
// No op for the moment
|
when (action) {
|
||||||
// when (action) {
|
CrossSigningSettingsAction.InitializeCrossSigning -> {
|
||||||
// }.exhaustive
|
_viewEvents.post(CrossSigningSettingsViewEvents.ShowModalWaitingView(null))
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
awaitCallback<Unit> {
|
||||||
|
session.cryptoService().crossSigningService().initializeCrossSigning(
|
||||||
|
object : UserInteractiveAuthInterceptor {
|
||||||
|
override fun performStage(flow: RegistrationFlowResponse, promise: Continuation<UIABaseAuth>) {
|
||||||
|
Timber.d("## UIA : initializeCrossSigning UIA")
|
||||||
|
if (flow.flows?.any { it.type == LoginFlowTypes.PASSWORD } == true && reAuthHelper.data != null) {
|
||||||
|
UserPasswordAuth(
|
||||||
|
session = null,
|
||||||
|
user = session.myUserId,
|
||||||
|
password = reAuthHelper.data
|
||||||
|
).let { promise.resume(it) }
|
||||||
|
} else {
|
||||||
|
Timber.d("## UIA : initializeCrossSigning UIA > start reauth activity")
|
||||||
|
_viewEvents.post(CrossSigningSettingsViewEvents.RequestReAuth(flow))
|
||||||
|
pendingAuth = DefaultBaseAuth(session = flow.session)
|
||||||
|
uiaContinuation = promise
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, it)
|
||||||
|
}
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
handleInitializeXSigningError(failure)
|
||||||
|
} finally {
|
||||||
|
_viewEvents.post(CrossSigningSettingsViewEvents.HideModalWaitingView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Unit
|
||||||
|
}
|
||||||
|
is CrossSigningSettingsAction.SsoAuthDone -> {
|
||||||
|
// we should use token based auth
|
||||||
|
// _viewEvents.post(CrossSigningSettingsViewEvents.ShowModalWaitingView(null))
|
||||||
|
// will release the interactive auth interceptor
|
||||||
|
Timber.d("## UIA - FallBack success $pendingAuth , continuation: $uiaContinuation")
|
||||||
|
if (pendingAuth != null) {
|
||||||
|
uiaContinuation?.resume(pendingAuth!!)
|
||||||
|
} else {
|
||||||
|
uiaContinuation?.resumeWith(Result.failure((IllegalArgumentException())))
|
||||||
|
}
|
||||||
|
Unit
|
||||||
|
}
|
||||||
|
is CrossSigningSettingsAction.PasswordAuthDone -> {
|
||||||
|
uiaContinuation?.resume(
|
||||||
|
UserPasswordAuth(
|
||||||
|
session = pendingAuth?.session,
|
||||||
|
password = action.password,
|
||||||
|
user = session.myUserId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
CrossSigningSettingsAction.ReAuthCancelled -> {
|
||||||
|
Timber.d("## UIA - Reauth cancelled")
|
||||||
|
_viewEvents.post(CrossSigningSettingsViewEvents.HideModalWaitingView)
|
||||||
|
uiaContinuation?.resumeWith(Result.failure((Exception())))
|
||||||
|
uiaContinuation = null
|
||||||
|
pendingAuth = null
|
||||||
|
}
|
||||||
|
}.exhaustive
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleInitializeXSigningError(failure: Throwable) {
|
||||||
|
Timber.e(failure, "## CrossSigning - Failed to initialize cross signing")
|
||||||
|
_viewEvents.post(CrossSigningSettingsViewEvents.Failure(Exception(stringProvider.getString(R.string.failed_to_initialize_cross_signing))))
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object : MvRxViewModelFactory<CrossSigningSettingsViewModel, CrossSigningSettingsViewState> {
|
companion object : MvRxViewModelFactory<CrossSigningSettingsViewModel, CrossSigningSettingsViewState> {
|
||||||
|
@ -23,5 +23,6 @@ data class CrossSigningSettingsViewState(
|
|||||||
val crossSigningInfo: MXCrossSigningInfo? = null,
|
val crossSigningInfo: MXCrossSigningInfo? = null,
|
||||||
val xSigningIsEnableInAccount: Boolean = false,
|
val xSigningIsEnableInAccount: Boolean = false,
|
||||||
val xSigningKeysAreTrusted: Boolean = false,
|
val xSigningKeysAreTrusted: Boolean = false,
|
||||||
val xSigningKeyCanSign: Boolean = true
|
val xSigningKeyCanSign: Boolean = true,
|
||||||
|
// val pendingAuthSession: String? = null
|
||||||
) : MvRxState
|
) : MvRxState
|
||||||
|
@ -22,7 +22,7 @@ import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
|||||||
sealed class DevicesAction : VectorViewModelAction {
|
sealed class DevicesAction : VectorViewModelAction {
|
||||||
object Refresh : DevicesAction()
|
object Refresh : DevicesAction()
|
||||||
data class Delete(val deviceId: String) : DevicesAction()
|
data class Delete(val deviceId: String) : DevicesAction()
|
||||||
data class Password(val password: String) : DevicesAction()
|
// data class Password(val password: String) : DevicesAction()
|
||||||
data class Rename(val deviceId: String, val newName: String) : DevicesAction()
|
data class Rename(val deviceId: String, val newName: String) : DevicesAction()
|
||||||
|
|
||||||
data class PromptRename(val deviceId: String) : DevicesAction()
|
data class PromptRename(val deviceId: String) : DevicesAction()
|
||||||
@ -30,4 +30,8 @@ sealed class DevicesAction : VectorViewModelAction {
|
|||||||
data class VerifyMyDeviceManually(val deviceId: String) : DevicesAction()
|
data class VerifyMyDeviceManually(val deviceId: String) : DevicesAction()
|
||||||
object CompleteSecurity : DevicesAction()
|
object CompleteSecurity : DevicesAction()
|
||||||
data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction()
|
data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction()
|
||||||
|
|
||||||
|
object SsoAuthDone: DevicesAction()
|
||||||
|
data class PasswordAuthDone(val password: String): DevicesAction()
|
||||||
|
object ReAuthCancelled: DevicesAction()
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ package im.vector.app.features.settings.devices
|
|||||||
|
|
||||||
import im.vector.app.core.platform.VectorViewEvents
|
import im.vector.app.core.platform.VectorViewEvents
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse
|
||||||
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
|
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
|
||||||
|
|
||||||
@ -27,9 +28,12 @@ import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
|
|||||||
*/
|
*/
|
||||||
sealed class DevicesViewEvents : VectorViewEvents {
|
sealed class DevicesViewEvents : VectorViewEvents {
|
||||||
data class Loading(val message: CharSequence? = null) : DevicesViewEvents()
|
data class Loading(val message: CharSequence? = null) : DevicesViewEvents()
|
||||||
|
// object HideLoading : DevicesViewEvents()
|
||||||
data class Failure(val throwable: Throwable) : DevicesViewEvents()
|
data class Failure(val throwable: Throwable) : DevicesViewEvents()
|
||||||
|
|
||||||
object RequestPassword : DevicesViewEvents()
|
// object RequestPassword : DevicesViewEvents()
|
||||||
|
|
||||||
|
data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse) : DevicesViewEvents()
|
||||||
|
|
||||||
data class PromptRenameDevice(val deviceInfo: DeviceInfo) : DevicesViewEvents()
|
data class PromptRenameDevice(val deviceInfo: DeviceInfo) : DevicesViewEvents()
|
||||||
|
|
||||||
|
@ -27,16 +27,20 @@ import com.airbnb.mvrx.Success
|
|||||||
import com.airbnb.mvrx.Uninitialized
|
import com.airbnb.mvrx.Uninitialized
|
||||||
import com.airbnb.mvrx.ViewModelContext
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import im.vector.app.core.error.SsoFlowNotSupportedYet
|
import dagger.assisted.AssistedInject
|
||||||
|
import im.vector.app.R
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
|
import im.vector.app.core.resources.StringProvider
|
||||||
|
import im.vector.app.features.login.ReAuthHelper
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import io.reactivex.functions.BiFunction
|
import io.reactivex.functions.BiFunction
|
||||||
import io.reactivex.subjects.PublishSubject
|
import io.reactivex.subjects.PublishSubject
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
import org.matrix.android.sdk.api.NoOpMatrixCallback
|
import org.matrix.android.sdk.api.NoOpMatrixCallback
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||||
import org.matrix.android.sdk.api.failure.Failure
|
import org.matrix.android.sdk.api.failure.Failure
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
@ -44,13 +48,20 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
|||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
||||||
|
import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel
|
import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel
|
||||||
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
||||||
|
import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
|
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
|
||||||
|
import org.matrix.android.sdk.internal.crypto.model.rest.UIABaseAuth
|
||||||
|
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
||||||
import org.matrix.android.sdk.internal.util.awaitCallback
|
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||||
import org.matrix.android.sdk.rx.rx
|
import org.matrix.android.sdk.rx.rx
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.net.ssl.HttpsURLConnection
|
||||||
|
import kotlin.coroutines.Continuation
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
data class DevicesViewState(
|
data class DevicesViewState(
|
||||||
val myDeviceId: String = "",
|
val myDeviceId: String = "",
|
||||||
@ -70,9 +81,14 @@ data class DeviceFullInfo(
|
|||||||
|
|
||||||
class DevicesViewModel @AssistedInject constructor(
|
class DevicesViewModel @AssistedInject constructor(
|
||||||
@Assisted initialState: DevicesViewState,
|
@Assisted initialState: DevicesViewState,
|
||||||
private val session: Session
|
private val session: Session,
|
||||||
|
private val reAuthHelper: ReAuthHelper,
|
||||||
|
private val stringProvider: StringProvider
|
||||||
) : VectorViewModel<DevicesViewState, DevicesAction, DevicesViewEvents>(initialState), VerificationService.Listener {
|
) : VectorViewModel<DevicesViewState, DevicesAction, DevicesViewEvents>(initialState), VerificationService.Listener {
|
||||||
|
|
||||||
|
var uiaContinuation: Continuation<UIABaseAuth>? = null
|
||||||
|
var pendingAuth: UIABaseAuth? = null
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(initialState: DevicesViewState): DevicesViewModel
|
fun create(initialState: DevicesViewState): DevicesViewModel
|
||||||
@ -87,10 +103,6 @@ class DevicesViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// temp storage when we ask for the user password
|
|
||||||
private var _currentDeviceId: String? = null
|
|
||||||
private var _currentSession: String? = null
|
|
||||||
|
|
||||||
private val refreshPublisher: PublishSubject<Unit> = PublishSubject.create()
|
private val refreshPublisher: PublishSubject<Unit> = PublishSubject.create()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@ -187,15 +199,44 @@ class DevicesViewModel @AssistedInject constructor(
|
|||||||
|
|
||||||
override fun handle(action: DevicesAction) {
|
override fun handle(action: DevicesAction) {
|
||||||
return when (action) {
|
return when (action) {
|
||||||
is DevicesAction.Refresh -> queryRefreshDevicesList()
|
is DevicesAction.Refresh -> queryRefreshDevicesList()
|
||||||
is DevicesAction.Delete -> handleDelete(action)
|
is DevicesAction.Delete -> handleDelete(action)
|
||||||
is DevicesAction.Password -> handlePassword(action)
|
is DevicesAction.Rename -> handleRename(action)
|
||||||
is DevicesAction.Rename -> handleRename(action)
|
is DevicesAction.PromptRename -> handlePromptRename(action)
|
||||||
is DevicesAction.PromptRename -> handlePromptRename(action)
|
is DevicesAction.VerifyMyDevice -> handleInteractiveVerification(action)
|
||||||
is DevicesAction.VerifyMyDevice -> handleInteractiveVerification(action)
|
is DevicesAction.CompleteSecurity -> handleCompleteSecurity()
|
||||||
is DevicesAction.CompleteSecurity -> handleCompleteSecurity()
|
|
||||||
is DevicesAction.MarkAsManuallyVerified -> handleVerifyManually(action)
|
is DevicesAction.MarkAsManuallyVerified -> handleVerifyManually(action)
|
||||||
is DevicesAction.VerifyMyDeviceManually -> handleShowDeviceCryptoInfo(action)
|
is DevicesAction.VerifyMyDeviceManually -> handleShowDeviceCryptoInfo(action)
|
||||||
|
is DevicesAction.SsoAuthDone -> {
|
||||||
|
// we should use token based auth
|
||||||
|
// _viewEvents.post(CrossSigningSettingsViewEvents.ShowModalWaitingView(null))
|
||||||
|
// will release the interactive auth interceptor
|
||||||
|
Timber.d("## UIA - FallBack success $pendingAuth , continuation: $uiaContinuation")
|
||||||
|
if (pendingAuth != null) {
|
||||||
|
uiaContinuation?.resume(pendingAuth!!)
|
||||||
|
} else {
|
||||||
|
uiaContinuation?.resumeWith(Result.failure((IllegalArgumentException())))
|
||||||
|
}
|
||||||
|
Unit
|
||||||
|
}
|
||||||
|
is DevicesAction.PasswordAuthDone -> {
|
||||||
|
uiaContinuation?.resume(
|
||||||
|
UserPasswordAuth(
|
||||||
|
session = pendingAuth?.session,
|
||||||
|
password = action.password,
|
||||||
|
user = session.myUserId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Unit
|
||||||
|
}
|
||||||
|
DevicesAction.ReAuthCancelled -> {
|
||||||
|
Timber.d("## UIA - Reauth cancelled")
|
||||||
|
// _viewEvents.post(DevicesViewEvents.Loading)
|
||||||
|
uiaContinuation?.resumeWith(Result.failure((Exception())))
|
||||||
|
uiaContinuation = null
|
||||||
|
pendingAuth = null
|
||||||
|
Unit
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -285,95 +326,48 @@ class DevicesViewModel @AssistedInject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
session.cryptoService().deleteDevice(deviceId, object : MatrixCallback<Unit> {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
override fun onFailure(failure: Throwable) {
|
try {
|
||||||
var isPasswordRequestFound = false
|
awaitCallback<Unit> {
|
||||||
|
session.cryptoService().deleteDevice(deviceId, object : UserInteractiveAuthInterceptor {
|
||||||
if (failure is Failure.RegistrationFlowError) {
|
override fun performStage(flow: RegistrationFlowResponse, promise: Continuation<UIABaseAuth>) {
|
||||||
// We only support LoginFlowTypes.PASSWORD
|
Timber.d("## UIA : deleteDevice UIA")
|
||||||
// Check if we can provide the user password
|
if (flow.flows?.any { it.type == LoginFlowTypes.PASSWORD } == true && reAuthHelper.data != null) {
|
||||||
failure.registrationFlowResponse.flows?.forEach { interactiveAuthenticationFlow ->
|
UserPasswordAuth(
|
||||||
isPasswordRequestFound = isPasswordRequestFound || interactiveAuthenticationFlow.stages?.any { it == LoginFlowTypes.PASSWORD } == true
|
session = null,
|
||||||
}
|
user = session.myUserId,
|
||||||
|
password = reAuthHelper.data
|
||||||
if (isPasswordRequestFound) {
|
).let { promise.resume(it) }
|
||||||
_currentDeviceId = deviceId
|
} else {
|
||||||
_currentSession = failure.registrationFlowResponse.session
|
Timber.d("## UIA : deleteDevice UIA > start reauth activity")
|
||||||
|
_viewEvents.post(DevicesViewEvents.RequestReAuth(flow))
|
||||||
setState {
|
pendingAuth = DefaultBaseAuth(session = flow.session)
|
||||||
copy(
|
uiaContinuation = promise
|
||||||
request = Success(Unit)
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
}, it)
|
||||||
_viewEvents.post(DevicesViewEvents.RequestPassword)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isPasswordRequestFound) {
|
|
||||||
// LoginFlowTypes.PASSWORD not supported, and this is the only one Element supports so far...
|
|
||||||
setState {
|
|
||||||
copy(
|
|
||||||
request = Fail(failure)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
_viewEvents.post(DevicesViewEvents.Failure(SsoFlowNotSupportedYet()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSuccess(data: Unit) {
|
|
||||||
setState {
|
setState {
|
||||||
copy(
|
copy(
|
||||||
request = Success(data)
|
request = Success(Unit)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// force settings update
|
// force settings update
|
||||||
queryRefreshDevicesList()
|
queryRefreshDevicesList()
|
||||||
}
|
} catch (failure: Throwable) {
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handlePassword(action: DevicesAction.Password) {
|
|
||||||
val currentDeviceId = _currentDeviceId
|
|
||||||
if (currentDeviceId.isNullOrBlank()) {
|
|
||||||
// Abort
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setState {
|
|
||||||
copy(
|
|
||||||
request = Loading()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
session.cryptoService().deleteDeviceWithUserPassword(currentDeviceId, _currentSession, action.password, object : MatrixCallback<Unit> {
|
|
||||||
override fun onSuccess(data: Unit) {
|
|
||||||
_currentDeviceId = null
|
|
||||||
_currentSession = null
|
|
||||||
|
|
||||||
setState {
|
|
||||||
copy(
|
|
||||||
request = Success(data)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// force settings update
|
|
||||||
queryRefreshDevicesList()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
|
||||||
_currentDeviceId = null
|
|
||||||
_currentSession = null
|
|
||||||
|
|
||||||
// Password is maybe not good
|
|
||||||
setState {
|
setState {
|
||||||
copy(
|
copy(
|
||||||
request = Fail(failure)
|
request = Fail(failure)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (failure is Failure.OtherServerError && failure.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED) {
|
||||||
_viewEvents.post(DevicesViewEvents.Failure(failure))
|
_viewEvents.post(DevicesViewEvents.Failure(Exception(stringProvider.getString(R.string.authentication_error))))
|
||||||
|
} else {
|
||||||
|
_viewEvents.post(DevicesViewEvents.Failure(Exception(stringProvider.getString(R.string.matrix_error))))
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
Timber.e(failure, "failed to delete session")
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package im.vector.app.features.settings.devices
|
package im.vector.app.features.settings.devices
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@ -29,14 +30,16 @@ import com.airbnb.mvrx.fragmentViewModel
|
|||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.dialogs.ManuallyVerifyDialog
|
import im.vector.app.core.dialogs.ManuallyVerifyDialog
|
||||||
import im.vector.app.core.dialogs.PromptPasswordDialog
|
|
||||||
import im.vector.app.core.extensions.cleanup
|
import im.vector.app.core.extensions.cleanup
|
||||||
import im.vector.app.core.extensions.configureWith
|
import im.vector.app.core.extensions.configureWith
|
||||||
import im.vector.app.core.extensions.exhaustive
|
import im.vector.app.core.extensions.exhaustive
|
||||||
|
import im.vector.app.core.extensions.registerStartForActivityResult
|
||||||
import im.vector.app.core.platform.VectorBaseFragment
|
import im.vector.app.core.platform.VectorBaseFragment
|
||||||
import im.vector.app.databinding.DialogBaseEditTextBinding
|
import im.vector.app.databinding.DialogBaseEditTextBinding
|
||||||
import im.vector.app.databinding.FragmentGenericRecyclerBinding
|
import im.vector.app.databinding.FragmentGenericRecyclerBinding
|
||||||
|
import im.vector.app.features.auth.ReAuthActivity
|
||||||
import im.vector.app.features.crypto.verification.VerificationBottomSheet
|
import im.vector.app.features.crypto.verification.VerificationBottomSheet
|
||||||
|
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||||
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
|
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -52,7 +55,7 @@ class VectorSettingsDevicesFragment @Inject constructor(
|
|||||||
|
|
||||||
// used to avoid requesting to enter the password for each deletion
|
// used to avoid requesting to enter the password for each deletion
|
||||||
// Note: Sonar does not like to use password for member name.
|
// Note: Sonar does not like to use password for member name.
|
||||||
private var mAccountPass: String = ""
|
// private var mAccountPass: String = ""
|
||||||
|
|
||||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentGenericRecyclerBinding {
|
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentGenericRecyclerBinding {
|
||||||
return FragmentGenericRecyclerBinding.inflate(inflater, container, false)
|
return FragmentGenericRecyclerBinding.inflate(inflater, container, false)
|
||||||
@ -71,7 +74,7 @@ class VectorSettingsDevicesFragment @Inject constructor(
|
|||||||
when (it) {
|
when (it) {
|
||||||
is DevicesViewEvents.Loading -> showLoading(it.message)
|
is DevicesViewEvents.Loading -> showLoading(it.message)
|
||||||
is DevicesViewEvents.Failure -> showFailure(it.throwable)
|
is DevicesViewEvents.Failure -> showFailure(it.throwable)
|
||||||
is DevicesViewEvents.RequestPassword -> maybeShowDeleteDeviceWithPasswordDialog()
|
is DevicesViewEvents.RequestReAuth -> maybeShowDeleteDeviceWithPasswordDialog(it)
|
||||||
is DevicesViewEvents.PromptRenameDevice -> displayDeviceRenameDialog(it.deviceInfo)
|
is DevicesViewEvents.PromptRenameDevice -> displayDeviceRenameDialog(it.deviceInfo)
|
||||||
is DevicesViewEvents.ShowVerifyDevice -> {
|
is DevicesViewEvents.ShowVerifyDevice -> {
|
||||||
VerificationBottomSheet.withArgs(
|
VerificationBottomSheet.withArgs(
|
||||||
@ -93,13 +96,6 @@ class VectorSettingsDevicesFragment @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showFailure(throwable: Throwable) {
|
|
||||||
super.showFailure(throwable)
|
|
||||||
|
|
||||||
// Password is maybe not good, for safety measure, reset it here
|
|
||||||
mAccountPass = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
devicesController.callback = null
|
devicesController.callback = null
|
||||||
views.genericRecyclerView.cleanup()
|
views.genericRecyclerView.cleanup()
|
||||||
@ -154,17 +150,31 @@ class VectorSettingsDevicesFragment @Inject constructor(
|
|||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult ->
|
||||||
|
if (activityResult.resultCode == Activity.RESULT_OK) {
|
||||||
|
when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) {
|
||||||
|
LoginFlowTypes.SSO -> {
|
||||||
|
viewModel.handle(DevicesAction.SsoAuthDone)
|
||||||
|
}
|
||||||
|
LoginFlowTypes.PASSWORD -> {
|
||||||
|
val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: ""
|
||||||
|
viewModel.handle(DevicesAction.PasswordAuthDone(password))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
viewModel.handle(DevicesAction.ReAuthCancelled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
viewModel.handle(DevicesAction.ReAuthCancelled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show a dialog to ask for user password, or use a previously entered password.
|
* Show a dialog to ask for user password, or use a previously entered password.
|
||||||
*/
|
*/
|
||||||
private fun maybeShowDeleteDeviceWithPasswordDialog() {
|
private fun maybeShowDeleteDeviceWithPasswordDialog(reAuthReq: DevicesViewEvents.RequestReAuth) {
|
||||||
if (mAccountPass.isNotEmpty()) {
|
ReAuthActivity.newIntent(requireContext(), reAuthReq.registrationFlowResponse, getString(R.string.devices_delete_dialog_title)).let { intent ->
|
||||||
viewModel.handle(DevicesAction.Password(mAccountPass))
|
reAuthActivityResultLauncher.launch(intent)
|
||||||
} else {
|
|
||||||
PromptPasswordDialog().show(requireActivity()) { password ->
|
|
||||||
mAccountPass = password
|
|
||||||
viewModel.handle(DevicesAction.Password(mAccountPass))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
99
vector/src/main/res/layout/fragment_reauth_confirm.xml
Normal file
99
vector/src/main/res/layout/fragment_reauth_confirm.xml
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/reAuthConfirmText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:text="@string/re_authentication_default_confirm_text"
|
||||||
|
android:textColor="?riotx_text_primary"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/reAuthConfirmButton"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_bias="0"
|
||||||
|
app:layout_constraintVertical_chainStyle="packed" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/passwordContainer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/loginPasswordNotice"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/reAuthConfirmText">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/passwordFieldTil"
|
||||||
|
style="@style/VectorTextInputLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/login_signup_password_hint"
|
||||||
|
app:errorEnabled="true"
|
||||||
|
app:errorIconDrawable="@null">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/passwordField"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ems="10"
|
||||||
|
android:imeOptions="actionDone"
|
||||||
|
android:inputType="textPassword"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:paddingEnd="48dp"
|
||||||
|
tools:ignore="RtlSymmetry" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/passwordReveal"
|
||||||
|
android:layout_width="@dimen/layout_touch_size"
|
||||||
|
android:layout_height="@dimen/layout_touch_size"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:src="@drawable/ic_eye"
|
||||||
|
app:tint="?attr/colorAccent"
|
||||||
|
tools:contentDescription="@string/a11y_show_password"
|
||||||
|
tools:ignore="MissingPrefix" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/loginPasswordNotice"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="start"
|
||||||
|
android:text="@string/login_signin_matrix_id_password_notice"
|
||||||
|
android:textAppearance="@style/TextAppearance.Vector.Login.Text.Small"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/passwordContainer"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/reAuthConfirmButton"
|
||||||
|
style="@style/VectorButtonStylePositive"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:layout_marginBottom="20dp"
|
||||||
|
android:text="@string/_continue"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/loginPasswordNotice" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</ScrollView>
|
19
vector/src/main/res/layout/item_positive_button.xml
Normal file
19
vector/src/main/res/layout/item_positive_button.xml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/itemGenericItemButton"
|
||||||
|
style="@style/VectorButtonStylePositive"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
app:iconGravity="textStart"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:text="Action Name" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
@ -2792,4 +2792,8 @@
|
|||||||
<string name="warning_unsaved_change_discard">Discard changes</string>
|
<string name="warning_unsaved_change_discard">Discard changes</string>
|
||||||
|
|
||||||
<string name="matrix_to_card_title">Matrix Link</string>
|
<string name="matrix_to_card_title">Matrix Link</string>
|
||||||
|
|
||||||
|
<string name="re_authentication_activity_title">Re-Authentication Needed</string>
|
||||||
|
<string name="re_authentication_default_confirm_text">Element requires you to enter your credentials to perform this action.</string>
|
||||||
|
<string name="authentication_error">Failed to authenticate</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user