mirror of
https://github.com/element-hq/element-android
synced 2024-11-28 21:48:50 +03:00
Merge pull request #793 from vector-im/outgoing_dm_verif
BottomSheet UX for verification
This commit is contained in:
commit
06b41af467
75 changed files with 2911 additions and 477 deletions
|
@ -26,3 +26,8 @@ fun Throwable.is401() =
|
|||
fun Throwable.isTokenError() =
|
||||
this is Failure.ServerError
|
||||
&& (error.code == MatrixError.M_UNKNOWN_TOKEN || error.code == MatrixError.M_MISSING_TOKEN)
|
||||
|
||||
fun Throwable.shouldBeRetried(): Boolean {
|
||||
return this is Failure.NetworkConnection
|
||||
|| (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED)
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package im.vector.matrix.android.api.session.crypto.sas
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest
|
||||
|
||||
/**
|
||||
* https://matrix.org/docs/spec/client_server/r0.5.0#key-verification-framework
|
||||
|
@ -39,6 +40,10 @@ interface SasVerificationService {
|
|||
|
||||
fun getExistingTransaction(otherUser: String, tid: String): SasVerificationTransaction?
|
||||
|
||||
fun getExistingVerificationRequest(otherUser: String): List<PendingVerificationRequest>?
|
||||
|
||||
fun getExistingVerificationRequest(otherUser: String, tid: String?): PendingVerificationRequest?
|
||||
|
||||
/**
|
||||
* Shortcut for KeyVerificationStart.VERIF_METHOD_SAS
|
||||
* @see beginKeyVerification
|
||||
|
@ -50,7 +55,9 @@ interface SasVerificationService {
|
|||
*/
|
||||
fun beginKeyVerification(method: String, userId: String, deviceID: String): String?
|
||||
|
||||
fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback<String>?)
|
||||
fun requestKeyVerificationInDMs(userId: String, roomId: String): PendingVerificationRequest
|
||||
|
||||
fun declineVerificationRequestInDMs(otherUserId: String, otherDeviceId: String, transactionId: String, roomId: String)
|
||||
|
||||
fun beginKeyVerificationInDMs(method: String,
|
||||
transactionId: String,
|
||||
|
@ -59,11 +66,33 @@ interface SasVerificationService {
|
|||
otherDeviceId: String,
|
||||
callback: MatrixCallback<String>?): String?
|
||||
|
||||
/**
|
||||
* Returns false if the request is unknwown
|
||||
*/
|
||||
fun readyPendingVerificationInDMs(otherUserId: String, roomId: String, transactionId: String): Boolean
|
||||
|
||||
// fun transactionUpdated(tx: SasVerificationTransaction)
|
||||
|
||||
interface SasVerificationListener {
|
||||
fun transactionCreated(tx: SasVerificationTransaction)
|
||||
fun transactionUpdated(tx: SasVerificationTransaction)
|
||||
fun markedAsManuallyVerified(userId: String, deviceId: String)
|
||||
fun markedAsManuallyVerified(userId: String, deviceId: String) {}
|
||||
|
||||
fun verificationRequestCreated(pr: PendingVerificationRequest) {}
|
||||
fun verificationRequestUpdated(pr: PendingVerificationRequest) {}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TEN_MINTUTES_IN_MILLIS = 10 * 60 * 1000
|
||||
private const val FIVE_MINTUTES_IN_MILLIS = 5 * 60 * 1000
|
||||
|
||||
fun isValidRequest(age: Long?): Boolean {
|
||||
if (age == null) return false
|
||||
val now = System.currentTimeMillis()
|
||||
val tooInThePast = now - TEN_MINTUTES_IN_MILLIS
|
||||
val tooInTheFuture = now + FIVE_MINTUTES_IN_MILLIS
|
||||
return age in tooInThePast..tooInTheFuture
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,4 +47,6 @@ interface SasVerificationTransaction {
|
|||
* both short codes do match
|
||||
*/
|
||||
fun userHasVerifiedShortCode()
|
||||
|
||||
fun shortCodeDoesNotMatch()
|
||||
}
|
||||
|
|
|
@ -73,6 +73,7 @@ object EventType {
|
|||
const val KEY_VERIFICATION_MAC = "m.key.verification.mac"
|
||||
const val KEY_VERIFICATION_CANCEL = "m.key.verification.cancel"
|
||||
const val KEY_VERIFICATION_DONE = "m.key.verification.done"
|
||||
const val KEY_VERIFICATION_READY = "m.key.verification.ready"
|
||||
|
||||
// Relation Events
|
||||
const val REACTION = "m.reaction"
|
||||
|
|
|
@ -89,4 +89,6 @@ interface RoomService {
|
|||
fun getRoomIdByAlias(roomAlias: String,
|
||||
searchOnServer: Boolean,
|
||||
callback: MatrixCallback<Optional<String>>): Cancelable
|
||||
|
||||
fun getExistingDirectRoomWithUser(otherUserId: String) : Room?
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ internal data class MessageVerificationAcceptContent(
|
|||
return true
|
||||
}
|
||||
|
||||
override fun toEventContent() = this.toContent()
|
||||
override fun toEventContent() = toContent()
|
||||
|
||||
companion object : VerificationInfoAcceptFactory {
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ data class MessageVerificationCancelContent(
|
|||
override val transactionID: String?
|
||||
get() = relatesTo?.eventId
|
||||
|
||||
override fun toEventContent() = this.toContent()
|
||||
override fun toEventContent() = toContent()
|
||||
|
||||
override fun isValid(): Boolean {
|
||||
if (transactionID.isNullOrBlank() || code.isNullOrBlank()) {
|
||||
|
|
|
@ -17,6 +17,8 @@ package im.vector.matrix.android.api.session.room.model.message
|
|||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.api.session.events.model.Content
|
||||
import im.vector.matrix.android.api.session.events.model.toContent
|
||||
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
|
||||
import im.vector.matrix.android.internal.crypto.verification.VerificationInfo
|
||||
|
||||
|
@ -24,5 +26,11 @@ import im.vector.matrix.android.internal.crypto.verification.VerificationInfo
|
|||
internal data class MessageVerificationDoneContent(
|
||||
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
|
||||
) : VerificationInfo {
|
||||
override fun isValid() = true
|
||||
|
||||
override val transactionID: String?
|
||||
get() = relatesTo?.eventId
|
||||
|
||||
override fun isValid() = transactionID?.isNotEmpty() == true
|
||||
|
||||
override fun toEventContent(): Content? = toContent()
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ internal data class MessageVerificationKeyContent(
|
|||
return true
|
||||
}
|
||||
|
||||
override fun toEventContent() = this.toContent()
|
||||
override fun toEventContent() = toContent()
|
||||
|
||||
companion object : VerificationInfoKeyFactory {
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ internal data class MessageVerificationMacContent(
|
|||
override val transactionID: String?
|
||||
get() = relatesTo?.eventId
|
||||
|
||||
override fun toEventContent() = this.toContent()
|
||||
override fun toEventContent() = toContent()
|
||||
|
||||
override fun isValid(): Boolean {
|
||||
if (transactionID.isNullOrBlank() || keys.isNullOrBlank() || mac.isNullOrEmpty()) {
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.matrix.android.api.session.room.model.message
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.api.session.events.model.RelationType
|
||||
import im.vector.matrix.android.api.session.events.model.toContent
|
||||
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
|
||||
import im.vector.matrix.android.internal.crypto.verification.MessageVerificationReadyFactory
|
||||
import im.vector.matrix.android.internal.crypto.verification.VerificationInfoReady
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class MessageVerificationReadyContent(
|
||||
@Json(name = "from_device") override val fromDevice: String? = null,
|
||||
@Json(name = "methods") override val methods: List<String>? = null,
|
||||
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
|
||||
) : VerificationInfoReady {
|
||||
|
||||
override val transactionID: String?
|
||||
get() = relatesTo?.eventId
|
||||
|
||||
override fun toEventContent() = toContent()
|
||||
|
||||
override fun isValid(): Boolean {
|
||||
if (transactionID.isNullOrBlank() || methods.isNullOrEmpty() || fromDevice.isNullOrEmpty()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
companion object : MessageVerificationReadyFactory {
|
||||
override fun create(tid: String, methods: List<String>, fromDevice: String): VerificationInfoReady {
|
||||
return MessageVerificationReadyContent(
|
||||
fromDevice = fromDevice,
|
||||
methods = methods,
|
||||
relatesTo = RelationDefaultContent(
|
||||
RelationType.REFERENCE,
|
||||
tid
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -62,5 +62,5 @@ internal data class MessageVerificationStartContent(
|
|||
return true
|
||||
}
|
||||
|
||||
override fun toEventContent() = this.toContent()
|
||||
override fun toEventContent() = toContent()
|
||||
}
|
||||
|
|
|
@ -180,6 +180,9 @@ internal abstract class CryptoModule {
|
|||
@Binds
|
||||
abstract fun bindEncryptEventTask(encryptEventTask: DefaultEncryptEventTask): EncryptEventTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindSendVerificationMessageTask(sendDefaultSendVerificationMessageTask: DefaultSendVerificationMessageTask): SendVerificationMessageTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindClaimOneTimeKeysForUsersDeviceTask(claimOneTimeKeysForUsersDevice: DefaultClaimOneTimeKeysForUsersDevice)
|
||||
: ClaimOneTimeKeysForUsersDeviceTask
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.matrix.android.internal.crypto.model.rest
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.internal.crypto.verification.VerificationInfoReady
|
||||
|
||||
/**
|
||||
* Requests a key verification with another user's devices.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class KeyVerificationReady(
|
||||
@Json(name = "from_device") override val fromDevice: String?,
|
||||
// TODO add qr?
|
||||
@Json(name = "methods") override val methods: List<String>? = listOf(KeyVerificationStart.VERIF_METHOD_SAS),
|
||||
@Json(name = "transaction_id") override var transactionID: String? = null
|
||||
) : SendToDeviceObject, VerificationInfoReady {
|
||||
|
||||
override fun toSendToDeviceObject() = this
|
||||
|
||||
override fun isValid(): Boolean {
|
||||
return !transactionID.isNullOrBlank() && !fromDevice.isNullOrBlank() && !methods.isNullOrEmpty()
|
||||
}
|
||||
}
|
|
@ -37,7 +37,7 @@ internal data class KeyVerificationRequest(
|
|||
val timestamp: Int,
|
||||
|
||||
@Json(name = "transaction_id")
|
||||
var transactionID: String? = null
|
||||
override var transactionID: String? = null
|
||||
|
||||
) : SendToDeviceObject, VerificationInfo {
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@ data class KeyVerificationStart(
|
|||
|
||||
companion object {
|
||||
const val VERIF_METHOD_SAS = "m.sas.v1"
|
||||
const val VERIF_METHOD_SCAN = "m.qr_code.scan.v1"
|
||||
}
|
||||
|
||||
override fun isValid(): Boolean {
|
||||
|
|
|
@ -1,95 +0,0 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.matrix.android.internal.crypto.tasks
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.session.room.RoomAPI
|
||||
import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
|
||||
import im.vector.matrix.android.internal.session.room.send.LocalEchoUpdater
|
||||
import im.vector.matrix.android.internal.session.room.send.SendResponse
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface RequestVerificationDMTask : Task<RequestVerificationDMTask.Params, SendResponse> {
|
||||
data class Params(
|
||||
val event: Event,
|
||||
val cryptoService: CryptoService
|
||||
)
|
||||
|
||||
fun createParamsAndLocalEcho(roomId: String, from: String, methods: List<String>, to: String, cryptoService: CryptoService): Params
|
||||
}
|
||||
|
||||
internal class DefaultRequestVerificationDMTask @Inject constructor(
|
||||
private val localEchoUpdater: LocalEchoUpdater,
|
||||
private val localEchoEventFactory: LocalEchoEventFactory,
|
||||
private val encryptEventTask: DefaultEncryptEventTask,
|
||||
private val monarchy: Monarchy,
|
||||
private val roomAPI: RoomAPI)
|
||||
: RequestVerificationDMTask {
|
||||
|
||||
override fun createParamsAndLocalEcho(roomId: String, from: String, methods: List<String>, to: String, cryptoService: CryptoService)
|
||||
: RequestVerificationDMTask.Params {
|
||||
val event = localEchoEventFactory.createVerificationRequest(roomId, from, to, methods)
|
||||
.also { localEchoEventFactory.saveLocalEcho(monarchy, it) }
|
||||
return RequestVerificationDMTask.Params(
|
||||
event,
|
||||
cryptoService
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun execute(params: RequestVerificationDMTask.Params): SendResponse {
|
||||
val event = handleEncryption(params)
|
||||
val localID = event.eventId!!
|
||||
|
||||
try {
|
||||
localEchoUpdater.updateSendState(localID, SendState.SENDING)
|
||||
val executeRequest = executeRequest<SendResponse> {
|
||||
apiCall = roomAPI.send(
|
||||
localID,
|
||||
roomId = event.roomId ?: "",
|
||||
content = event.content,
|
||||
eventType = event.type // message or room.encrypted
|
||||
)
|
||||
}
|
||||
localEchoUpdater.updateSendState(localID, SendState.SENT)
|
||||
return executeRequest
|
||||
} catch (e: Throwable) {
|
||||
localEchoUpdater.updateSendState(localID, SendState.UNDELIVERED)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleEncryption(params: RequestVerificationDMTask.Params): Event {
|
||||
val roomId = params.event.roomId ?: ""
|
||||
if (params.cryptoService.isRoomEncrypted(roomId)) {
|
||||
try {
|
||||
return encryptEventTask.execute(EncryptEventTask.Params(
|
||||
roomId,
|
||||
params.event,
|
||||
listOf("m.relates_to"),
|
||||
params.cryptoService
|
||||
))
|
||||
} catch (throwable: Throwable) {
|
||||
// We said it's ok to send verification request in clear
|
||||
}
|
||||
}
|
||||
return params.event
|
||||
}
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.tasks
|
||||
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.message.*
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService
|
||||
import im.vector.matrix.android.internal.di.DeviceId
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface RoomVerificationUpdateTask : Task<RoomVerificationUpdateTask.Params, Unit> {
|
||||
data class Params(
|
||||
val events: List<Event>,
|
||||
val sasVerificationService: DefaultSasVerificationService,
|
||||
val cryptoService: CryptoService
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultRoomVerificationUpdateTask @Inject constructor(
|
||||
@UserId private val userId: String,
|
||||
@DeviceId private val deviceId: String?,
|
||||
private val cryptoService: CryptoService) : RoomVerificationUpdateTask {
|
||||
|
||||
companion object {
|
||||
// XXX what about multi-account?
|
||||
private val transactionsHandledByOtherDevice = ArrayList<String>()
|
||||
}
|
||||
|
||||
override suspend fun execute(params: RoomVerificationUpdateTask.Params) {
|
||||
// TODO ignore initial sync or back pagination?
|
||||
|
||||
params.events.forEach { event ->
|
||||
Timber.d("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}")
|
||||
Timber.v("## SAS Verification live observer: received msgId: $event")
|
||||
|
||||
// If the request is in the future by more than 5 minutes or more than 10 minutes in the past,
|
||||
// the message should be ignored by the receiver.
|
||||
|
||||
if (!SasVerificationService.isValidRequest(event.ageLocalTs
|
||||
?: event.originServerTs)) return@forEach Unit.also {
|
||||
Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated")
|
||||
}
|
||||
|
||||
// decrypt if needed?
|
||||
if (event.isEncrypted() && event.mxDecryptionResult == null) {
|
||||
// TODO use a global event decryptor? attache to session and that listen to new sessionId?
|
||||
// for now decrypt sync
|
||||
try {
|
||||
val result = cryptoService.decryptEvent(event, event.roomId + UUID.randomUUID().toString())
|
||||
event.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||
)
|
||||
} catch (e: MXCryptoError) {
|
||||
Timber.e("## SAS Failed to decrypt event: ${event.eventId}")
|
||||
}
|
||||
}
|
||||
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}")
|
||||
|
||||
if (event.senderId == userId) {
|
||||
// If it's send from me, we need to keep track of Requests or Start
|
||||
// done from another device of mine
|
||||
|
||||
if (EventType.MESSAGE == event.type) {
|
||||
val msgType = event.getClearContent().toModel<MessageContent>()?.type
|
||||
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) {
|
||||
event.getClearContent().toModel<MessageVerificationRequestContent>()?.let {
|
||||
if (it.fromDevice != deviceId) {
|
||||
// The verification is requested from another device
|
||||
Timber.v("## SAS Verification live observer: Transaction requested from other device tid:${event.eventId} ")
|
||||
event.eventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (EventType.KEY_VERIFICATION_START == event.type) {
|
||||
event.getClearContent().toModel<MessageVerificationStartContent>()?.let {
|
||||
if (it.fromDevice != deviceId) {
|
||||
// The verification is started from another device
|
||||
Timber.v("## SAS Verification live observer: Transaction started by other device tid:${it.transactionID} ")
|
||||
it.transactionID?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
|
||||
}
|
||||
}
|
||||
} else if (EventType.KEY_VERIFICATION_READY == event.type) {
|
||||
event.getClearContent().toModel<MessageVerificationReadyContent>()?.let {
|
||||
if (it.fromDevice != deviceId) {
|
||||
// The verification is started from another device
|
||||
Timber.v("## SAS Verification live observer: Transaction started by other device tid:${it.transactionID} ")
|
||||
it.transactionID?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
|
||||
}
|
||||
}
|
||||
} else if (EventType.KEY_VERIFICATION_CANCEL == event.type || EventType.KEY_VERIFICATION_DONE == event.type) {
|
||||
event.getClearContent().toModel<MessageRelationContent>()?.relatesTo?.eventId?.let {
|
||||
transactionsHandledByOtherDevice.remove(it)
|
||||
}
|
||||
}
|
||||
|
||||
Timber.v("## SAS Verification ignoring message sent by me: ${event.eventId} type: ${event.getClearType()}")
|
||||
return@forEach
|
||||
}
|
||||
|
||||
val relatesTo = event.getClearContent().toModel<MessageRelationContent>()?.relatesTo?.eventId
|
||||
if (relatesTo != null && transactionsHandledByOtherDevice.contains(relatesTo)) {
|
||||
// Ignore this event, it is directed to another of my devices
|
||||
Timber.v("## SAS Verification live observer: Ignore Transaction handled by other device tid:$relatesTo ")
|
||||
return@forEach
|
||||
}
|
||||
when (event.getClearType()) {
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
EventType.KEY_VERIFICATION_KEY,
|
||||
EventType.KEY_VERIFICATION_MAC,
|
||||
EventType.KEY_VERIFICATION_CANCEL,
|
||||
EventType.KEY_VERIFICATION_READY,
|
||||
EventType.KEY_VERIFICATION_DONE -> {
|
||||
params.sasVerificationService.onRoomEvent(event)
|
||||
}
|
||||
EventType.MESSAGE -> {
|
||||
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.type) {
|
||||
params.sasVerificationService.onRoomRequestReceived(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,64 +15,29 @@
|
|||
*/
|
||||
package im.vector.matrix.android.internal.crypto.tasks
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.events.model.Content
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||
import im.vector.matrix.android.api.session.events.model.UnsignedData
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.session.room.RoomAPI
|
||||
import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
|
||||
import im.vector.matrix.android.internal.session.room.send.LocalEchoUpdater
|
||||
import im.vector.matrix.android.internal.session.room.send.SendResponse
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface SendVerificationMessageTask : Task<SendVerificationMessageTask.Params, SendResponse> {
|
||||
internal interface SendVerificationMessageTask : Task<SendVerificationMessageTask.Params, String> {
|
||||
data class Params(
|
||||
val type: String,
|
||||
val event: Event,
|
||||
val cryptoService: CryptoService?
|
||||
)
|
||||
|
||||
fun createParamsAndLocalEcho(type: String,
|
||||
roomId: String,
|
||||
content: Content,
|
||||
cryptoService: CryptoService?) : Params
|
||||
}
|
||||
|
||||
internal class DefaultSendVerificationMessageTask @Inject constructor(
|
||||
private val localEchoUpdater: LocalEchoUpdater,
|
||||
private val localEchoEventFactory: LocalEchoEventFactory,
|
||||
private val encryptEventTask: DefaultEncryptEventTask,
|
||||
private val monarchy: Monarchy,
|
||||
@UserId private val userId: String,
|
||||
private val roomAPI: RoomAPI) : SendVerificationMessageTask {
|
||||
|
||||
override fun createParamsAndLocalEcho(type: String, roomId: String, content: Content, cryptoService: CryptoService?): SendVerificationMessageTask.Params {
|
||||
val localID = LocalEcho.createLocalEchoId()
|
||||
val event = Event(
|
||||
roomId = roomId,
|
||||
originServerTs = System.currentTimeMillis(),
|
||||
senderId = userId,
|
||||
eventId = localID,
|
||||
type = type,
|
||||
content = content,
|
||||
unsignedData = UnsignedData(age = null, transactionId = localID)
|
||||
).also {
|
||||
localEchoEventFactory.saveLocalEcho(monarchy, it)
|
||||
}
|
||||
return SendVerificationMessageTask.Params(
|
||||
type,
|
||||
event,
|
||||
cryptoService
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun execute(params: SendVerificationMessageTask.Params): SendResponse {
|
||||
override suspend fun execute(params: SendVerificationMessageTask.Params): String {
|
||||
val event = handleEncryption(params)
|
||||
val localID = event.eventId!!
|
||||
|
||||
|
@ -87,7 +52,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
|
|||
)
|
||||
}
|
||||
localEchoUpdater.updateSendState(localID, SendState.SENT)
|
||||
return executeRequest
|
||||
return executeRequest.eventId
|
||||
} catch (e: Throwable) {
|
||||
localEchoUpdater.updateSendState(localID, SendState.UNDELIVERED)
|
||||
throw e
|
||||
|
|
|
@ -33,7 +33,8 @@ internal class DefaultIncomingSASVerificationTransaction(
|
|||
private val cryptoStore: IMXCryptoStore,
|
||||
deviceFingerprint: String,
|
||||
transactionId: String,
|
||||
otherUserID: String
|
||||
otherUserID: String,
|
||||
val autoAccept: Boolean = false
|
||||
) : SASVerificationTransaction(
|
||||
setDeviceVerificationAction,
|
||||
credentials,
|
||||
|
@ -76,6 +77,10 @@ internal class DefaultIncomingSASVerificationTransaction(
|
|||
this.startReq = startReq
|
||||
state = SasVerificationTxState.OnStarted
|
||||
this.otherDeviceId = startReq.fromDevice
|
||||
|
||||
if (autoAccept) {
|
||||
performAccept()
|
||||
}
|
||||
}
|
||||
|
||||
override fun performAccept() {
|
||||
|
|
|
@ -28,6 +28,7 @@ import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
|
|||
import im.vector.matrix.android.api.session.crypto.sas.safeValueOf
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.message.*
|
||||
import im.vector.matrix.android.internal.crypto.DeviceListManager
|
||||
|
@ -37,12 +38,7 @@ import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
|||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.*
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultRequestVerificationDMTask
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.session.room.send.SendResponse
|
||||
import im.vector.matrix.android.internal.task.TaskConstraints
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -60,11 +56,9 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
|
||||
private val deviceListManager: DeviceListManager,
|
||||
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
||||
private val requestVerificationDMTask: DefaultRequestVerificationDMTask,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val sasTransportRoomMessageFactory: SasTransportRoomMessageFactory,
|
||||
private val sasTransportToDeviceFactory: SasTransportToDeviceFactory,
|
||||
private val taskExecutor: TaskExecutor
|
||||
private val sasTransportToDeviceFactory: SasTransportToDeviceFactory
|
||||
) : VerificationTransaction.Listener, SasVerificationService {
|
||||
|
||||
private val uiHandler = Handler(Looper.getMainLooper())
|
||||
|
@ -75,6 +69,12 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
// map [sender : [transaction]]
|
||||
private val txMap = HashMap<String, HashMap<String, VerificationTransaction>>()
|
||||
|
||||
/**
|
||||
* Map [sender: [PendingVerificationRequest]]
|
||||
* For now we keep all requests (even terminated ones) during the lifetime of the app.
|
||||
*/
|
||||
private val pendingRequests = HashMap<String, ArrayList<PendingVerificationRequest>>()
|
||||
|
||||
// Event received from the sync
|
||||
fun onToDeviceEvent(event: Event) {
|
||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||
|
@ -120,8 +120,11 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
EventType.KEY_VERIFICATION_MAC -> {
|
||||
onRoomMacReceived(event)
|
||||
}
|
||||
EventType.KEY_VERIFICATION_READY -> {
|
||||
onRoomReadyReceived(event)
|
||||
}
|
||||
EventType.KEY_VERIFICATION_DONE -> {
|
||||
// TODO?
|
||||
onRoomDoneReceived(event)
|
||||
}
|
||||
EventType.MESSAGE -> {
|
||||
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.type) {
|
||||
|
@ -175,6 +178,30 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun dispatchRequestAdded(tx: PendingVerificationRequest) {
|
||||
uiHandler.post {
|
||||
listeners.forEach {
|
||||
try {
|
||||
it.verificationRequestCreated(tx)
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "## Error while notifying listeners")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun dispatchRequestUpdated(tx: PendingVerificationRequest) {
|
||||
uiHandler.post {
|
||||
listeners.forEach {
|
||||
try {
|
||||
it.verificationRequestUpdated(tx)
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "## Error while notifying listeners")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) {
|
||||
setDeviceVerificationAction.handle(MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED,
|
||||
deviceID,
|
||||
|
@ -190,8 +217,50 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
}
|
||||
|
||||
fun onRoomRequestReceived(event: Event) {
|
||||
// TODO
|
||||
Timber.v("## SAS Verification request from ${event.senderId} in room ${event.roomId}")
|
||||
val requestInfo = event.getClearContent().toModel<MessageVerificationRequestContent>()
|
||||
?: return
|
||||
val senderId = event.senderId ?: return
|
||||
|
||||
if (requestInfo.toUserId != credentials.userId) {
|
||||
// I should ignore this, it's not for me
|
||||
Timber.w("## SAS Verification ignoring request from ${event.senderId}, not sent to me")
|
||||
return
|
||||
}
|
||||
|
||||
// We don't want to block here
|
||||
GlobalScope.launch {
|
||||
if (checkKeysAreDownloaded(senderId, requestInfo.fromDevice) == null) {
|
||||
Timber.e("## SAS Verification device ${requestInfo.fromDevice} is not knwon")
|
||||
}
|
||||
}
|
||||
|
||||
// Remember this request
|
||||
val requestsForUser = pendingRequests[senderId]
|
||||
?: ArrayList<PendingVerificationRequest>().also {
|
||||
pendingRequests[event.senderId] = it
|
||||
}
|
||||
|
||||
val pendingVerificationRequest = PendingVerificationRequest(
|
||||
ageLocalTs = event.ageLocalTs ?: System.currentTimeMillis(),
|
||||
isIncoming = true,
|
||||
otherUserId = senderId, // requestInfo.toUserId,
|
||||
transactionId = event.eventId,
|
||||
requestInfo = requestInfo
|
||||
)
|
||||
requestsForUser.add(pendingVerificationRequest)
|
||||
dispatchRequestAdded(pendingVerificationRequest)
|
||||
|
||||
/*
|
||||
* After the m.key.verification.ready event is sent, either party can send an m.key.verification.start event
|
||||
* to begin the verification.
|
||||
* If both parties send an m.key.verification.start event, and they both specify the same verification method,
|
||||
* then the event sent by the user whose user ID is the smallest is used, and the other m.key.verification.start
|
||||
* event is ignored.
|
||||
* In the case of a single user verifying two of their devices, the device ID is compared instead.
|
||||
* If both parties send an m.key.verification.start event, but they specify different verification methods,
|
||||
* the verification should be cancelled with a code of m.unexpected_message.
|
||||
*/
|
||||
}
|
||||
|
||||
private suspend fun onRoomStartRequestReceived(event: Event) {
|
||||
|
@ -205,8 +274,9 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
if (startReq?.isValid()?.not() == true) {
|
||||
Timber.e("## received invalid verification request")
|
||||
if (startReq.transactionID != null) {
|
||||
sasTransportRoomMessageFactory.createTransport(event.roomId
|
||||
?: "", cryptoService, null).cancelTransaction(
|
||||
sasTransportRoomMessageFactory.createTransport(credentials.userId, credentials.deviceId
|
||||
?: "", event.roomId
|
||||
?: "", null).cancelTransaction(
|
||||
startReq.transactionID ?: "",
|
||||
otherUserId!!,
|
||||
startReq.fromDevice ?: event.getSenderKey()!!,
|
||||
|
@ -217,11 +287,13 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
}
|
||||
|
||||
handleStart(otherUserId, startReq as VerificationInfoStart) {
|
||||
it.transport = sasTransportRoomMessageFactory.createTransport(event.roomId
|
||||
?: "", cryptoService, it)
|
||||
it.transport = sasTransportRoomMessageFactory.createTransport(credentials.userId, credentials.deviceId
|
||||
?: "", event.roomId
|
||||
?: "", it)
|
||||
}?.let {
|
||||
sasTransportRoomMessageFactory.createTransport(event.roomId
|
||||
?: "", cryptoService, null).cancelTransaction(
|
||||
sasTransportRoomMessageFactory.createTransport(credentials.userId, credentials.deviceId
|
||||
?: "", event.roomId
|
||||
?: "", null).cancelTransaction(
|
||||
startReq.transactionID ?: "",
|
||||
otherUserId!!,
|
||||
startReq.fromDevice ?: event.getSenderKey()!!,
|
||||
|
@ -263,7 +335,7 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
|
||||
private suspend fun handleStart(otherUserId: String?, startReq: VerificationInfoStart, txConfigure: (SASVerificationTransaction) -> Unit): CancelCode? {
|
||||
Timber.d("## SAS onStartRequestReceived ${startReq.transactionID!!}")
|
||||
if (checkKeysAreDownloaded(otherUserId!!, startReq) != null) {
|
||||
if (checkKeysAreDownloaded(otherUserId!!, startReq.fromDevice ?: "") != null) {
|
||||
Timber.v("## SAS onStartRequestReceived $startReq")
|
||||
val tid = startReq.transactionID!!
|
||||
val existing = getExistingTransaction(otherUserId, tid)
|
||||
|
@ -286,6 +358,10 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
// Ok we can create
|
||||
if (KeyVerificationStart.VERIF_METHOD_SAS == startReq.method) {
|
||||
Timber.v("## SAS onStartRequestReceived - request accepted ${startReq.transactionID!!}")
|
||||
// If there is a corresponding request, we can auto accept
|
||||
// as we are the one requesting in first place (or we accepted the request)
|
||||
val autoAccept = getExistingVerificationRequest(otherUserId)?.any { it.transactionId == startReq.transactionID }
|
||||
?: false
|
||||
val tx = DefaultIncomingSASVerificationTransaction(
|
||||
// this,
|
||||
setDeviceVerificationAction,
|
||||
|
@ -293,7 +369,8 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
cryptoStore,
|
||||
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
||||
startReq.transactionID!!,
|
||||
otherUserId).also { txConfigure(it) }
|
||||
otherUserId,
|
||||
autoAccept).also { txConfigure(it) }
|
||||
addTransaction(tx)
|
||||
tx.acceptVerificationEvent(otherUserId, startReq)
|
||||
} else {
|
||||
|
@ -311,11 +388,16 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun checkKeysAreDownloaded(otherUserId: String,
|
||||
startReq: VerificationInfoStart): MXUsersDevicesMap<MXDeviceInfo>? {
|
||||
fromDevice: String): MXUsersDevicesMap<MXDeviceInfo>? {
|
||||
return try {
|
||||
val keys = deviceListManager.downloadKeys(listOf(otherUserId), true)
|
||||
val deviceIds = keys.getUserDeviceIds(otherUserId) ?: return null
|
||||
keys.takeIf { deviceIds.contains(startReq.fromDevice) }
|
||||
var keys = deviceListManager.downloadKeys(listOf(otherUserId), false)
|
||||
if (keys.getUserDeviceIds(otherUserId)?.contains(fromDevice) == true) {
|
||||
return keys
|
||||
} else {
|
||||
// force download
|
||||
keys = deviceListManager.downloadKeys(listOf(otherUserId), true)
|
||||
return keys.takeIf { keys.getUserDeviceIds(otherUserId)?.contains(fromDevice) == true }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
@ -333,6 +415,10 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
// TODO should we cancel?
|
||||
return
|
||||
}
|
||||
getExistingVerificationRequest(event.senderId ?: "", cancelReq.transactionID)?.let {
|
||||
updatePendingRequest(it.copy(cancelConclusion = safeValueOf(cancelReq.code)))
|
||||
// Should we remove it from the list?
|
||||
}
|
||||
handleOnCancel(event.senderId!!, cancelReq)
|
||||
}
|
||||
|
||||
|
@ -352,14 +438,20 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
|
||||
private fun handleOnCancel(otherUserId: String, cancelReq: VerificationInfoCancel) {
|
||||
Timber.v("## SAS onCancelReceived otherUser:$otherUserId reason:${cancelReq.reason}")
|
||||
val existing = getExistingTransaction(otherUserId, cancelReq.transactionID!!)
|
||||
if (existing == null) {
|
||||
Timber.e("## Received invalid cancel request")
|
||||
return
|
||||
|
||||
val existingTransaction = getExistingTransaction(otherUserId, cancelReq.transactionID!!)
|
||||
val existingRequest = getExistingVerificationRequest(otherUserId, cancelReq.transactionID!!)
|
||||
|
||||
if (existingRequest != null) {
|
||||
// Mark this request as cancelled
|
||||
updatePendingRequest(existingRequest.copy(
|
||||
cancelConclusion = safeValueOf(cancelReq.code)
|
||||
))
|
||||
}
|
||||
if (existing is SASVerificationTransaction) {
|
||||
existing.cancelledReason = safeValueOf(cancelReq.code)
|
||||
existing.state = SasVerificationTxState.OnCancelled
|
||||
|
||||
if (existingTransaction is SASVerificationTransaction) {
|
||||
existingTransaction.cancelledReason = safeValueOf(cancelReq.code)
|
||||
existingTransaction.state = SasVerificationTxState.OnCancelled
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -456,6 +548,44 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
handleMacReceived(event.senderId, macReq)
|
||||
}
|
||||
|
||||
private suspend fun onRoomReadyReceived(event: Event) {
|
||||
val readyReq = event.getClearContent().toModel<MessageVerificationReadyContent>()
|
||||
?.copy(
|
||||
// relates_to is in clear in encrypted payload
|
||||
relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo
|
||||
)
|
||||
if (readyReq == null || readyReq.isValid().not() || event.senderId == null) {
|
||||
// ignore
|
||||
Timber.e("## SAS Received invalid ready request")
|
||||
// TODO should we cancel?
|
||||
return
|
||||
}
|
||||
if (checkKeysAreDownloaded(event.senderId, readyReq.fromDevice ?: "") == null) {
|
||||
Timber.e("## SAS Verification device ${readyReq.fromDevice} is not knwown")
|
||||
// TODO cancel?
|
||||
return
|
||||
}
|
||||
|
||||
handleReadyReceived(event.senderId, readyReq)
|
||||
}
|
||||
|
||||
private fun onRoomDoneReceived(event: Event) {
|
||||
val doneReq = event.getClearContent().toModel<MessageVerificationDoneContent>()
|
||||
?.copy(
|
||||
// relates_to is in clear in encrypted payload
|
||||
relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo
|
||||
)
|
||||
|
||||
if (doneReq == null || doneReq.isValid().not() || event.senderId == null) {
|
||||
// ignore
|
||||
Timber.e("## SAS Received invalid Done request")
|
||||
// TODO should we cancel?
|
||||
return
|
||||
}
|
||||
|
||||
handleDoneReceived(event.senderId, doneReq)
|
||||
}
|
||||
|
||||
private fun onMacReceived(event: Event) {
|
||||
val macReq = event.getClearContent().toModel<KeyVerificationMac>()!!
|
||||
|
||||
|
@ -481,12 +611,42 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleReadyReceived(senderId: String, readyReq: VerificationInfoReady) {
|
||||
val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == readyReq.transactionID }
|
||||
if (existingRequest == null) {
|
||||
Timber.e("## SAS Received Ready for unknown request txId:${readyReq.transactionID} fromDevice ${readyReq.fromDevice}")
|
||||
return
|
||||
}
|
||||
updatePendingRequest(existingRequest.copy(readyInfo = readyReq))
|
||||
}
|
||||
|
||||
private fun handleDoneReceived(senderId: String, doneInfo: VerificationInfo) {
|
||||
val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == doneInfo.transactionID }
|
||||
if (existingRequest == null) {
|
||||
Timber.e("## SAS Received Done for unknown request txId:${doneInfo.transactionID}")
|
||||
return
|
||||
}
|
||||
updatePendingRequest(existingRequest.copy(isSuccessful = true))
|
||||
}
|
||||
|
||||
override fun getExistingTransaction(otherUser: String, tid: String): VerificationTransaction? {
|
||||
synchronized(lock = txMap) {
|
||||
return txMap[otherUser]?.get(tid)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getExistingVerificationRequest(otherUser: String): List<PendingVerificationRequest>? {
|
||||
synchronized(lock = pendingRequests) {
|
||||
return pendingRequests[otherUser]
|
||||
}
|
||||
}
|
||||
|
||||
override fun getExistingVerificationRequest(otherUser: String, tid: String?): PendingVerificationRequest? {
|
||||
synchronized(lock = pendingRequests) {
|
||||
return tid?.let { tid -> pendingRequests[otherUser]?.firstOrNull { it.transactionId == tid } }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getExistingTransactionsForUser(otherUser: String): Collection<VerificationTransaction>? {
|
||||
synchronized(txMap) {
|
||||
return txMap[otherUser]?.values
|
||||
|
@ -536,28 +696,76 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback<String>?) {
|
||||
requestVerificationDMTask.configureWith(
|
||||
requestVerificationDMTask.createParamsAndLocalEcho(
|
||||
roomId = roomId,
|
||||
from = credentials.deviceId ?: "",
|
||||
methods = listOf(KeyVerificationStart.VERIF_METHOD_SAS),
|
||||
to = userId,
|
||||
cryptoService = cryptoService
|
||||
)
|
||||
) {
|
||||
this.callback = object : MatrixCallback<SendResponse> {
|
||||
override fun onSuccess(data: SendResponse) {
|
||||
callback?.onSuccess(data.eventId)
|
||||
override fun requestKeyVerificationInDMs(userId: String, roomId: String)
|
||||
: PendingVerificationRequest {
|
||||
Timber.i("## SAS Requesting verification to user: $userId in room $roomId")
|
||||
|
||||
val requestsForUser = pendingRequests[userId]
|
||||
?: ArrayList<PendingVerificationRequest>().also {
|
||||
pendingRequests[userId] = it
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
callback?.onFailure(failure)
|
||||
val transport = sasTransportRoomMessageFactory.createTransport(credentials.userId, credentials.deviceId
|
||||
?: "", roomId, null)
|
||||
|
||||
// Cancel existing pending requests?
|
||||
requestsForUser.forEach { existingRequest ->
|
||||
existingRequest.transactionId?.let { tid ->
|
||||
if (!existingRequest.isFinished) {
|
||||
Timber.d("## SAS, cancelling pending requests to start a new one")
|
||||
transport.cancelTransaction(tid, existingRequest.otherUserId, "", CancelCode.User)
|
||||
}
|
||||
}
|
||||
constraints = TaskConstraints(true)
|
||||
retryCount = 3
|
||||
}.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
val localID = LocalEcho.createLocalEchoId()
|
||||
|
||||
val verificationRequest = PendingVerificationRequest(
|
||||
ageLocalTs = System.currentTimeMillis(),
|
||||
isIncoming = false,
|
||||
localID = localID,
|
||||
otherUserId = userId
|
||||
)
|
||||
|
||||
transport.sendVerificationRequest(localID, userId, roomId) { syncedId, info ->
|
||||
// We need to update with the syncedID
|
||||
updatePendingRequest(verificationRequest.copy(
|
||||
transactionId = syncedId,
|
||||
requestInfo = info
|
||||
))
|
||||
}
|
||||
|
||||
requestsForUser.add(verificationRequest)
|
||||
dispatchRequestAdded(verificationRequest)
|
||||
|
||||
return verificationRequest
|
||||
}
|
||||
|
||||
override fun declineVerificationRequestInDMs(otherUserId: String, otherDeviceId: String, transactionId: String, roomId: String) {
|
||||
sasTransportRoomMessageFactory.createTransport(credentials.userId, credentials.deviceId
|
||||
?: "", roomId, null).cancelTransaction(transactionId, otherUserId, otherDeviceId, CancelCode.User)
|
||||
|
||||
getExistingVerificationRequest(otherUserId, transactionId)?.let {
|
||||
updatePendingRequest(it.copy(
|
||||
cancelConclusion = CancelCode.User
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePendingRequest(updated: PendingVerificationRequest) {
|
||||
val requestsForUser = pendingRequests[updated.otherUserId]
|
||||
?: ArrayList<PendingVerificationRequest>().also {
|
||||
pendingRequests[updated.otherUserId] = it
|
||||
}
|
||||
val index = requestsForUser.indexOfFirst {
|
||||
it.transactionId == updated.transactionId
|
||||
|| it.transactionId == null && it.localID == updated.localID
|
||||
}
|
||||
if (index != -1) {
|
||||
requestsForUser.removeAt(index)
|
||||
}
|
||||
requestsForUser.add(updated)
|
||||
dispatchRequestUpdated(updated)
|
||||
}
|
||||
|
||||
override fun beginKeyVerificationInDMs(method: String, transactionId: String, roomId: String,
|
||||
|
@ -572,7 +780,8 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
transactionId,
|
||||
otherUserId,
|
||||
otherDeviceId)
|
||||
tx.transport = sasTransportRoomMessageFactory.createTransport(roomId, cryptoService, tx)
|
||||
tx.transport = sasTransportRoomMessageFactory.createTransport(credentials.userId, credentials.deviceId
|
||||
?: "", roomId, tx)
|
||||
addTransaction(tx)
|
||||
|
||||
tx.start()
|
||||
|
@ -582,6 +791,36 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun readyPendingVerificationInDMs(otherUserId: String, roomId: String, transactionId: String): Boolean {
|
||||
Timber.v("## SAS readyPendingVerificationInDMs $otherUserId room:$roomId tx:$transactionId")
|
||||
// Let's find the related request
|
||||
val existingRequest = getExistingVerificationRequest(otherUserId, transactionId)
|
||||
if (existingRequest != null) {
|
||||
// we need to send a ready event, with matching methods
|
||||
val transport = sasTransportRoomMessageFactory.createTransport(credentials.userId, credentials.deviceId
|
||||
?: "", roomId, null)
|
||||
val methods = existingRequest.requestInfo?.methods?.intersect(listOf(KeyVerificationStart.VERIF_METHOD_SAS))?.toList()
|
||||
if (methods.isNullOrEmpty()) {
|
||||
Timber.i("Cannot ready this request, no common methods found txId:$transactionId")
|
||||
// TODO buttons should not be shown in this case?
|
||||
return false
|
||||
}
|
||||
// TODO this is not yet related to a transaction, maybe we should use another method like for cancel?
|
||||
val readyMsg = transport.createReady(transactionId, credentials.deviceId ?: "", methods)
|
||||
transport.sendToOther(EventType.KEY_VERIFICATION_READY, readyMsg,
|
||||
SasVerificationTxState.None,
|
||||
CancelCode.User,
|
||||
null // TODO handle error?
|
||||
)
|
||||
updatePendingRequest(existingRequest.copy(readyInfo = readyMsg))
|
||||
return true
|
||||
} else {
|
||||
Timber.e("## SAS readyPendingVerificationInDMs Verification not found")
|
||||
// :/ should not be possible... unless live observer very slow
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This string must be unique for the pair of users performing verification for the duration that the transaction is valid
|
||||
*/
|
||||
|
@ -606,28 +845,4 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
this.removeTransaction(tx.otherUserId, tx.transactionId)
|
||||
}
|
||||
}
|
||||
//
|
||||
// fun cancelTransaction(transactionId: String, userId: String, userDevice: String, code: CancelCode, roomId: String? = null) {
|
||||
// val cancelMessage = KeyVerificationCancel.create(transactionId, code)
|
||||
// val contentMap = MXUsersDevicesMap<Any>()
|
||||
// contentMap.setObject(userId, userDevice, cancelMessage)
|
||||
//
|
||||
// if (roomId != null) {
|
||||
//
|
||||
// } else {
|
||||
// sendToDeviceTask
|
||||
// .configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap, transactionId)) {
|
||||
// this.callback = object : MatrixCallback<Unit> {
|
||||
// override fun onSuccess(data: Unit) {
|
||||
// Timber.v("## SAS verification [$transactionId] canceled for reason ${code.value}")
|
||||
// }
|
||||
//
|
||||
// override fun onFailure(failure: Throwable) {
|
||||
// Timber.e(failure, "## SAS verification [$transactionId] failed to cancel.")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .executeBy(taskExecutor)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.matrix.android.internal.crypto.verification
|
||||
|
||||
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Stores current pending verification requests
|
||||
*/
|
||||
data class PendingVerificationRequest(
|
||||
val ageLocalTs : Long,
|
||||
val isIncoming: Boolean = false,
|
||||
val localID: String = UUID.randomUUID().toString(),
|
||||
val otherUserId: String,
|
||||
val transactionId: String? = null,
|
||||
val requestInfo: MessageVerificationRequestContent? = null,
|
||||
val readyInfo: VerificationInfoReady? = null,
|
||||
val cancelConclusion: CancelCode? = null,
|
||||
val isSuccessful : Boolean = false
|
||||
|
||||
) {
|
||||
|
||||
val isReady: Boolean = readyInfo != null
|
||||
val isSent: Boolean = transactionId != null
|
||||
|
||||
val isFinished: Boolean = isSuccessful || cancelConclusion != null
|
||||
}
|
|
@ -169,6 +169,11 @@ internal abstract class SASVerificationTransaction(
|
|||
} // if not wait for it
|
||||
}
|
||||
|
||||
override fun shortCodeDoesNotMatch() {
|
||||
Timber.v("## SAS short code do not match for id:$transactionId")
|
||||
cancel(CancelCode.MismatchedSas)
|
||||
}
|
||||
|
||||
override fun acceptVerificationEvent(senderId: String, info: VerificationInfo) {
|
||||
when (info) {
|
||||
is VerificationInfoStart -> onVerificationStart(info)
|
||||
|
@ -222,13 +227,14 @@ internal abstract class SASVerificationTransaction(
|
|||
val keyIDNoPrefix = if (it.startsWith("ed25519:")) it.substring("ed25519:".length) else it
|
||||
val otherDeviceKey = otherUserKnownDevices?.get(keyIDNoPrefix)?.fingerprint()
|
||||
if (otherDeviceKey == null) {
|
||||
Timber.e("Verification: Could not find device $keyIDNoPrefix to verify")
|
||||
Timber.e("## SAS Verification: Could not find device $keyIDNoPrefix to verify")
|
||||
// just ignore and continue
|
||||
return@forEach
|
||||
}
|
||||
val mac = macUsingAgreedMethod(otherDeviceKey, baseInfo + it)
|
||||
if (mac != theirMac?.mac?.get(it)) {
|
||||
// WRONG!
|
||||
Timber.e("## SAS Verification: mac mismatch for $otherDeviceKey with id $keyIDNoPrefix")
|
||||
cancel(CancelCode.MismatchedKeys)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.crypto.verification
|
|||
|
||||
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
|
||||
|
||||
/**
|
||||
* SAS verification can be performed using toDevice events or via DM.
|
||||
|
@ -33,7 +34,9 @@ internal interface SasTransport {
|
|||
onErrorReason: CancelCode,
|
||||
onDone: (() -> Unit)?)
|
||||
|
||||
fun cancelTransaction(transactionId: String, userId: String, userDevice: String, code: CancelCode)
|
||||
fun sendVerificationRequest(localID: String, otherUserId: String, roomId: String, callback: (String?, MessageVerificationRequestContent?) -> Unit)
|
||||
|
||||
fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDevice: String, code: CancelCode)
|
||||
|
||||
fun done(transactionId: String)
|
||||
/**
|
||||
|
@ -58,4 +61,6 @@ internal interface SasTransport {
|
|||
shortAuthenticationStrings: List<String>) : VerificationInfoStart
|
||||
|
||||
fun createMac(tid: String, mac: Map<String, String>, keys: String): VerificationInfoMac
|
||||
|
||||
fun createReady(tid: String, fromDevice: String, methods: List<String>): VerificationInfoReady
|
||||
}
|
||||
|
|
|
@ -15,31 +15,36 @@
|
|||
*/
|
||||
package im.vector.matrix.android.internal.crypto.verification
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.work.*
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.R
|
||||
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.RelationType
|
||||
import im.vector.matrix.android.api.session.events.model.toContent
|
||||
import im.vector.matrix.android.api.session.events.model.*
|
||||
import im.vector.matrix.android.api.session.room.model.message.*
|
||||
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultSendVerificationMessageTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SendVerificationMessageTask
|
||||
import im.vector.matrix.android.internal.session.room.send.SendResponse
|
||||
import im.vector.matrix.android.internal.task.TaskConstraints
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.TaskThread
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart
|
||||
import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
|
||||
import im.vector.matrix.android.internal.worker.WorkManagerUtil
|
||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class SasTransportRoomMessage(
|
||||
private val context: Context,
|
||||
private val userId: String,
|
||||
private val userDevice: String,
|
||||
private val roomId: String,
|
||||
private val cryptoService: CryptoService,
|
||||
private val tx: SASVerificationTransaction?,
|
||||
private val sendVerificationMessageTask: SendVerificationMessageTask,
|
||||
private val taskExecutor: TaskExecutor
|
||||
private val monarchy: Monarchy,
|
||||
private val localEchoEventFactory: LocalEchoEventFactory,
|
||||
private val tx: SASVerificationTransaction?
|
||||
) : SasTransport {
|
||||
|
||||
override fun sendToOther(type: String,
|
||||
|
@ -49,83 +54,167 @@ internal class SasTransportRoomMessage(
|
|||
onDone: (() -> Unit)?) {
|
||||
Timber.d("## SAS sending msg type $type")
|
||||
Timber.v("## SAS sending msg info $verificationInfo")
|
||||
sendVerificationMessageTask.configureWith(
|
||||
sendVerificationMessageTask.createParamsAndLocalEcho(
|
||||
type,
|
||||
roomId,
|
||||
verificationInfo.toEventContent()!!,
|
||||
cryptoService
|
||||
)
|
||||
) {
|
||||
callbackThread = TaskThread.DM_VERIF
|
||||
executionThread = TaskThread.DM_VERIF
|
||||
constraints = TaskConstraints(true)
|
||||
callback = object : MatrixCallback<SendResponse> {
|
||||
override fun onSuccess(data: SendResponse) {
|
||||
if (onDone != null) {
|
||||
onDone()
|
||||
} else {
|
||||
tx?.state = nextState
|
||||
}
|
||||
}
|
||||
val event = createEventAndLocalEcho(
|
||||
type = type,
|
||||
roomId = roomId,
|
||||
content = verificationInfo.toEventContent()!!
|
||||
)
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
Timber.e("## SAS verification [${tx?.transactionId}] failed to send toDevice in state : ${tx?.state}")
|
||||
tx?.cancel(onErrorReason)
|
||||
}
|
||||
val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params(
|
||||
userId = userId,
|
||||
event = event
|
||||
))
|
||||
val enqueueInfo = enqueueSendWork(workerParams)
|
||||
|
||||
// I cannot just listen to the given work request, because when used in a uniqueWork,
|
||||
// The callback is called while it is still Running ...
|
||||
|
||||
// Futures.addCallback(enqueueInfo.first.result, object : FutureCallback<Operation.State.SUCCESS> {
|
||||
// override fun onSuccess(result: Operation.State.SUCCESS?) {
|
||||
// if (onDone != null) {
|
||||
// onDone()
|
||||
// } else {
|
||||
// tx?.state = nextState
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override fun onFailure(t: Throwable) {
|
||||
// Timber.e("## SAS verification [${tx?.transactionId}] failed to send toDevice in state : ${tx?.state}, reason: ${t.localizedMessage}")
|
||||
// tx?.cancel(onErrorReason)
|
||||
// }
|
||||
// }, listenerExecutor)
|
||||
|
||||
val workLiveData = WorkManager.getInstance(context).getWorkInfosForUniqueWorkLiveData("${roomId}_VerificationWork")
|
||||
|
||||
val observer = object : Observer<List<WorkInfo>> {
|
||||
override fun onChanged(workInfoList: List<WorkInfo>?) {
|
||||
workInfoList
|
||||
?.filter { it.state == WorkInfo.State.SUCCEEDED }
|
||||
?.firstOrNull { it.id == enqueueInfo.second }
|
||||
?.let { wInfo ->
|
||||
if (wInfo.outputData.getBoolean("failed", false)) {
|
||||
Timber.e("## SAS verification [${tx?.transactionId}] failed to send verification message in state : ${tx?.state}")
|
||||
tx?.cancel(onErrorReason)
|
||||
} else {
|
||||
if (onDone != null) {
|
||||
onDone()
|
||||
} else {
|
||||
tx?.state = nextState
|
||||
}
|
||||
}
|
||||
workLiveData.removeObserver(this)
|
||||
}
|
||||
}
|
||||
retryCount = 3
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
|
||||
// TODO listen to DB to get synced info
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
workLiveData.observeForever(observer)
|
||||
}
|
||||
}
|
||||
|
||||
override fun cancelTransaction(transactionId: String, userId: String, userDevice: String, code: CancelCode) {
|
||||
Timber.d("## SAS canceling transaction $transactionId for reason $code")
|
||||
sendVerificationMessageTask.configureWith(
|
||||
sendVerificationMessageTask.createParamsAndLocalEcho(
|
||||
EventType.KEY_VERIFICATION_CANCEL,
|
||||
roomId,
|
||||
MessageVerificationCancelContent.create(transactionId, code).toContent(),
|
||||
cryptoService
|
||||
)
|
||||
) {
|
||||
callbackThread = TaskThread.DM_VERIF
|
||||
executionThread = TaskThread.DM_VERIF
|
||||
constraints = TaskConstraints(true)
|
||||
retryCount = 3
|
||||
callback = object : MatrixCallback<SendResponse> {
|
||||
override fun onSuccess(data: SendResponse) {
|
||||
Timber.v("## SAS verification [$transactionId] canceled for reason ${code.value}")
|
||||
}
|
||||
override fun sendVerificationRequest(localID: String, otherUserId: String, roomId: String,
|
||||
callback: (String?, MessageVerificationRequestContent?) -> Unit) {
|
||||
val info = MessageVerificationRequestContent(
|
||||
body = context.getString(R.string.key_verification_request_fallback_message, userId),
|
||||
fromDevice = userDevice,
|
||||
toUserId = otherUserId,
|
||||
methods = listOf(KeyVerificationStart.VERIF_METHOD_SAS)
|
||||
)
|
||||
val content = info.toContent()
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
Timber.e(failure, "## SAS verification [$transactionId] failed to cancel.")
|
||||
}
|
||||
val event = createEventAndLocalEcho(
|
||||
localID,
|
||||
EventType.MESSAGE,
|
||||
roomId,
|
||||
content
|
||||
)
|
||||
|
||||
val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params(
|
||||
userId = userId,
|
||||
event = event
|
||||
))
|
||||
|
||||
val workRequest = WorkManagerUtil.matrixOneTimeWorkRequestBuilder<SendVerificationMessageWorker>()
|
||||
.setConstraints(WorkManagerUtil.workConstraints)
|
||||
.setInputData(workerParams)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(context)
|
||||
.beginUniqueWork("${roomId}_VerificationWork", ExistingWorkPolicy.APPEND, workRequest)
|
||||
.enqueue()
|
||||
|
||||
// I cannot just listen to the given work request, because when used in a uniqueWork,
|
||||
// The callback is called while it is still Running ...
|
||||
|
||||
val workLiveData = WorkManager.getInstance(context).getWorkInfosForUniqueWorkLiveData("${roomId}_VerificationWork")
|
||||
|
||||
val observer = object : Observer<List<WorkInfo>> {
|
||||
override fun onChanged(workInfoList: List<WorkInfo>?) {
|
||||
workInfoList
|
||||
?.filter { it.state == WorkInfo.State.SUCCEEDED }
|
||||
?.firstOrNull { it.id == workRequest.id }
|
||||
?.let { wInfo ->
|
||||
if (wInfo.outputData.getBoolean("failed", false)) {
|
||||
callback(null, null)
|
||||
} else if (wInfo.outputData.getString(localID) != null) {
|
||||
callback(wInfo.outputData.getString(localID), info)
|
||||
} else {
|
||||
callback(null, null)
|
||||
}
|
||||
workLiveData.removeObserver(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
|
||||
// TODO listen to DB to get synced info
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
workLiveData.observeForever(observer)
|
||||
}
|
||||
}
|
||||
|
||||
override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDevice: String, code: CancelCode) {
|
||||
Timber.d("## SAS canceling transaction $transactionId for reason $code")
|
||||
val event = createEventAndLocalEcho(
|
||||
type = EventType.KEY_VERIFICATION_CANCEL,
|
||||
roomId = roomId,
|
||||
content = MessageVerificationCancelContent.create(transactionId, code).toContent()
|
||||
)
|
||||
val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params(
|
||||
userId = this.userId,
|
||||
event = event
|
||||
))
|
||||
enqueueSendWork(workerParams)
|
||||
}
|
||||
|
||||
override fun done(transactionId: String) {
|
||||
sendVerificationMessageTask.configureWith(
|
||||
sendVerificationMessageTask.createParamsAndLocalEcho(
|
||||
EventType.KEY_VERIFICATION_DONE,
|
||||
roomId,
|
||||
MessageVerificationDoneContent(
|
||||
relatesTo = RelationDefaultContent(
|
||||
RelationType.REFERENCE,
|
||||
transactionId
|
||||
)
|
||||
).toContent(),
|
||||
cryptoService
|
||||
)
|
||||
) {
|
||||
callbackThread = TaskThread.DM_VERIF
|
||||
executionThread = TaskThread.DM_VERIF
|
||||
constraints = TaskConstraints(true)
|
||||
retryCount = 3
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
val event = createEventAndLocalEcho(
|
||||
type = EventType.KEY_VERIFICATION_DONE,
|
||||
roomId = roomId,
|
||||
content = MessageVerificationDoneContent(
|
||||
relatesTo = RelationDefaultContent(
|
||||
RelationType.REFERENCE,
|
||||
transactionId
|
||||
)
|
||||
).toContent()
|
||||
)
|
||||
val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params(
|
||||
userId = userId,
|
||||
event = event
|
||||
))
|
||||
enqueueSendWork(workerParams)
|
||||
}
|
||||
|
||||
private fun enqueueSendWork(workerParams: Data): Pair<Operation, UUID> {
|
||||
val workRequest = WorkManagerUtil.matrixOneTimeWorkRequestBuilder<SendVerificationMessageWorker>()
|
||||
.setConstraints(WorkManagerUtil.workConstraints)
|
||||
.setInputData(workerParams)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
return WorkManager.getInstance(context)
|
||||
.beginUniqueWork("${roomId}_VerificationWork", ExistingWorkPolicy.APPEND, workRequest)
|
||||
.enqueue() to workRequest.id
|
||||
}
|
||||
|
||||
override fun createAccept(tid: String,
|
||||
|
@ -167,16 +256,40 @@ internal class SasTransportRoomMessage(
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun createReady(tid: String, fromDevice: String, methods: List<String>): VerificationInfoReady {
|
||||
return MessageVerificationReadyContent(
|
||||
fromDevice = fromDevice,
|
||||
relatesTo = RelationDefaultContent(
|
||||
type = RelationType.REFERENCE,
|
||||
eventId = tid
|
||||
),
|
||||
methods = methods
|
||||
)
|
||||
}
|
||||
|
||||
private fun createEventAndLocalEcho(localID: String = LocalEcho.createLocalEchoId(), type: String, roomId: String, content: Content): Event {
|
||||
return Event(
|
||||
roomId = roomId,
|
||||
originServerTs = System.currentTimeMillis(),
|
||||
senderId = userId,
|
||||
eventId = localID,
|
||||
type = type,
|
||||
content = content,
|
||||
unsignedData = UnsignedData(age = null, transactionId = localID)
|
||||
).also {
|
||||
localEchoEventFactory.saveLocalEcho(monarchy, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class SasTransportRoomMessageFactory @Inject constructor(
|
||||
private val sendVerificationMessageTask: DefaultSendVerificationMessageTask,
|
||||
private val taskExecutor: TaskExecutor) {
|
||||
private val context: Context,
|
||||
private val monarchy: Monarchy,
|
||||
private val localEchoEventFactory: LocalEchoEventFactory) {
|
||||
|
||||
fun createTransport(roomId: String,
|
||||
cryptoService: CryptoService,
|
||||
tx: SASVerificationTransaction?
|
||||
fun createTransport(userId: String, userDevice: String, roomId: String, tx: SASVerificationTransaction?
|
||||
): SasTransportRoomMessage {
|
||||
return SasTransportRoomMessage(roomId, cryptoService, tx, sendVerificationMessageTask, taskExecutor)
|
||||
return SasTransportRoomMessage(context, userId, userDevice, roomId, monarchy, localEchoEventFactory, tx)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import im.vector.matrix.android.api.MatrixCallback
|
|||
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.*
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||
|
@ -33,6 +34,11 @@ internal class SasTransportToDevice(
|
|||
private var taskExecutor: TaskExecutor
|
||||
) : SasTransport {
|
||||
|
||||
override fun sendVerificationRequest(localID: String, otherUserId: String, roomId: String,
|
||||
callback: (String?, MessageVerificationRequestContent?) -> Unit) {
|
||||
// TODO "not implemented"
|
||||
}
|
||||
|
||||
override fun sendToOther(type: String,
|
||||
verificationInfo: VerificationInfo,
|
||||
nextState: SasVerificationTxState,
|
||||
|
@ -72,11 +78,11 @@ internal class SasTransportToDevice(
|
|||
// To device do not do anything here
|
||||
}
|
||||
|
||||
override fun cancelTransaction(transactionId: String, userId: String, userDevice: String, code: CancelCode) {
|
||||
override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDevice: String, code: CancelCode) {
|
||||
Timber.d("## SAS canceling transaction $transactionId for reason $code")
|
||||
val cancelMessage = KeyVerificationCancel.create(transactionId, code)
|
||||
val contentMap = MXUsersDevicesMap<Any>()
|
||||
contentMap.setObject(userId, userDevice, cancelMessage)
|
||||
contentMap.setObject(otherUserId, otherUserDevice, cancelMessage)
|
||||
sendToDeviceTask
|
||||
.configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap, transactionId)) {
|
||||
this.callback = object : MatrixCallback<Unit> {
|
||||
|
@ -126,6 +132,14 @@ internal class SasTransportToDevice(
|
|||
messageAuthenticationCodes,
|
||||
shortAuthenticationStrings)
|
||||
}
|
||||
|
||||
override fun createReady(tid: String, fromDevice: String, methods: List<String>): VerificationInfoReady {
|
||||
return KeyVerificationReady(
|
||||
transactionID = tid,
|
||||
fromDevice = fromDevice,
|
||||
methods = methods
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal class SasTransportToDeviceFactory @Inject constructor(
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.matrix.android.internal.crypto.verification
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.Data
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.api.failure.shouldBeRetried
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SendVerificationMessageTask
|
||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||
import im.vector.matrix.android.internal.worker.getSessionComponent
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class SendVerificationMessageWorker constructor(context: Context, params: WorkerParameters)
|
||||
: CoroutineWorker(context, params) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class Params(
|
||||
val userId: String,
|
||||
val event: Event
|
||||
)
|
||||
|
||||
@Inject
|
||||
lateinit var sendVerificationMessageTask: SendVerificationMessageTask
|
||||
|
||||
@Inject
|
||||
lateinit var cryptoService: CryptoService
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val errorOutputData = Data.Builder().putBoolean("failed", true).build()
|
||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||
?: return Result.success(errorOutputData)
|
||||
|
||||
val sessionComponent = getSessionComponent(params.userId)
|
||||
?: return Result.success(errorOutputData).also {
|
||||
// TODO, can this happen? should I update local echo?
|
||||
Timber.e("Unknown Session, cannot send message, userId:${params.userId}")
|
||||
}
|
||||
sessionComponent.inject(this)
|
||||
val localId = params.event.eventId ?: ""
|
||||
return try {
|
||||
val eventId = sendVerificationMessageTask.execute(
|
||||
SendVerificationMessageTask.Params(
|
||||
event = params.event,
|
||||
cryptoService = cryptoService
|
||||
)
|
||||
)
|
||||
|
||||
Result.success(Data.Builder().putString(localId, eventId).build())
|
||||
} catch (exception: Throwable) {
|
||||
if (exception.shouldBeRetried()) {
|
||||
Result.retry()
|
||||
} else {
|
||||
Result.success(errorOutputData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,8 +18,9 @@ package im.vector.matrix.android.internal.crypto.verification
|
|||
import im.vector.matrix.android.api.session.events.model.Content
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.SendToDeviceObject
|
||||
|
||||
internal interface VerificationInfo {
|
||||
interface VerificationInfo {
|
||||
fun toEventContent(): Content? = null
|
||||
fun toSendToDeviceObject(): SendToDeviceObject? = null
|
||||
fun isValid() : Boolean
|
||||
val transactionID: String?
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ package im.vector.matrix.android.internal.crypto.verification
|
|||
|
||||
internal interface VerificationInfoAccept : VerificationInfo {
|
||||
|
||||
val transactionID: String?
|
||||
override val transactionID: String?
|
||||
|
||||
/**
|
||||
* The key agreement protocol that Bob’s device has selected to use, out of the list proposed by Alice’s device
|
||||
|
|
|
@ -17,7 +17,7 @@ package im.vector.matrix.android.internal.crypto.verification
|
|||
|
||||
internal interface VerificationInfoCancel : VerificationInfo {
|
||||
|
||||
val transactionID: String?
|
||||
override val transactionID: String?
|
||||
/**
|
||||
* machine-readable reason for cancelling, see [CancelCode]
|
||||
*/
|
||||
|
|
|
@ -20,7 +20,7 @@ package im.vector.matrix.android.internal.crypto.verification
|
|||
*/
|
||||
internal interface VerificationInfoKey : VerificationInfo {
|
||||
|
||||
val transactionID: String?
|
||||
override val transactionID: String?
|
||||
/**
|
||||
* The device’s ephemeral public key, as an unpadded base64 string
|
||||
*/
|
||||
|
|
|
@ -17,7 +17,7 @@ package im.vector.matrix.android.internal.crypto.verification
|
|||
|
||||
internal interface VerificationInfoMac : VerificationInfo {
|
||||
|
||||
val transactionID: String?
|
||||
override val transactionID: String?
|
||||
|
||||
/**
|
||||
* A map of key ID to the MAC of the key, as an unpadded base64 string, calculated using the MAC key
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.matrix.android.internal.crypto.verification
|
||||
|
||||
/**
|
||||
* A new event type is added to the key verification framework: m.key.verification.ready,
|
||||
* which may be sent by the target of the m.key.verification.request message, upon receipt of the m.key.verification.request event.
|
||||
*
|
||||
* The m.key.verification.ready event is optional; the recipient of the m.key.verification.request event may respond directly
|
||||
* with a m.key.verification.start event instead.
|
||||
*/
|
||||
interface VerificationInfoReady : VerificationInfo {
|
||||
|
||||
override val transactionID: String?
|
||||
|
||||
/**
|
||||
* The ID of the device that sent the m.key.verification.ready message
|
||||
*/
|
||||
val fromDevice: String?
|
||||
|
||||
/**
|
||||
* An array of verification methods that the device supports
|
||||
*/
|
||||
val methods: List<String>?
|
||||
}
|
||||
|
||||
internal interface MessageVerificationReadyFactory {
|
||||
fun create(tid: String, methods: List<String>, fromDevice: String): VerificationInfoReady
|
||||
}
|
|
@ -28,7 +28,7 @@ internal interface VerificationInfoStart : VerificationInfo {
|
|||
* This string must be unique for the pair of users performing verification for the duration that the transaction is valid.
|
||||
* Alice’s device should record this ID and use it in future messages in this transaction.
|
||||
*/
|
||||
val transactionID: String?
|
||||
override val transactionID: String?
|
||||
|
||||
/**
|
||||
* An array of key agreement protocols that Alice’s client understands.
|
||||
|
|
|
@ -17,38 +17,31 @@ package im.vector.matrix.android.internal.crypto.verification
|
|||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.message.*
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultRoomVerificationUpdateTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.RoomVerificationUpdateTask
|
||||
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
|
||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.query.types
|
||||
import im.vector.matrix.android.internal.di.DeviceId
|
||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import io.realm.OrderedCollectionChangeSet
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.RealmResults
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
internal class VerificationMessageLiveObserver @Inject constructor(
|
||||
@SessionDatabase realmConfiguration: RealmConfiguration,
|
||||
@UserId private val userId: String,
|
||||
@DeviceId private val deviceId: String?,
|
||||
private val roomVerificationUpdateTask: DefaultRoomVerificationUpdateTask,
|
||||
private val cryptoService: CryptoService,
|
||||
private val sasVerificationService: DefaultSasVerificationService,
|
||||
private val taskExecutor: TaskExecutor
|
||||
) : RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
|
||||
|
||||
override val query = Monarchy.Query<EventEntity> {
|
||||
override val query = Monarchy.Query {
|
||||
EventEntity.types(it, listOf(
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
|
@ -56,16 +49,14 @@ internal class VerificationMessageLiveObserver @Inject constructor(
|
|||
EventType.KEY_VERIFICATION_MAC,
|
||||
EventType.KEY_VERIFICATION_CANCEL,
|
||||
EventType.KEY_VERIFICATION_DONE,
|
||||
EventType.KEY_VERIFICATION_READY,
|
||||
EventType.MESSAGE,
|
||||
EventType.ENCRYPTED)
|
||||
)
|
||||
}
|
||||
|
||||
val transactionsHandledByOtherDevice = ArrayList<String>()
|
||||
|
||||
override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
|
||||
// TODO do that in a task
|
||||
// TODO how to ignore when it's an initial sync?
|
||||
// Should we ignore when it's an initial sync?
|
||||
val events = changeSet.insertions
|
||||
.asSequence()
|
||||
.mapNotNull { results[it]?.asDomain() }
|
||||
|
@ -75,102 +66,8 @@ internal class VerificationMessageLiveObserver @Inject constructor(
|
|||
}
|
||||
.toList()
|
||||
|
||||
// TODO ignore initial sync or back pagination?
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
val tooInThePast = now - (10 * 60 * 1000)
|
||||
val fiveMinInMs = 5 * 60 * 1000
|
||||
val tooInTheFuture = System.currentTimeMillis() + fiveMinInMs
|
||||
|
||||
events.forEach { event ->
|
||||
Timber.d("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}")
|
||||
Timber.v("## SAS Verification live observer: received msgId: $event")
|
||||
|
||||
// If the request is in the future by more than 5 minutes or more than 10 minutes in the past,
|
||||
// the message should be ignored by the receiver.
|
||||
val ageLocalTs = event.ageLocalTs
|
||||
if (ageLocalTs != null && (now - ageLocalTs) > fiveMinInMs) {
|
||||
Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is too old (age: ${(now - ageLocalTs)})")
|
||||
return@forEach
|
||||
} else {
|
||||
val eventOrigin = event.originServerTs ?: -1
|
||||
if (eventOrigin < tooInThePast || eventOrigin > tooInTheFuture) {
|
||||
Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is too old (ts: $eventOrigin")
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
|
||||
// decrypt if needed?
|
||||
if (event.isEncrypted() && event.mxDecryptionResult == null) {
|
||||
// TODO use a global event decryptor? attache to session and that listen to new sessionId?
|
||||
// for now decrypt sync
|
||||
try {
|
||||
val result = cryptoService.decryptEvent(event, event.roomId + UUID.randomUUID().toString())
|
||||
event.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||
)
|
||||
} catch (e: MXCryptoError) {
|
||||
Timber.e("## SAS Failed to decrypt event: ${event.eventId}")
|
||||
}
|
||||
}
|
||||
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}")
|
||||
|
||||
if (event.senderId == userId) {
|
||||
// If it's send from me, we need to keep track of Requests or Start
|
||||
// done from another device of mine
|
||||
|
||||
if (EventType.MESSAGE == event.type) {
|
||||
val msgType = event.getClearContent().toModel<MessageContent>()?.type
|
||||
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) {
|
||||
event.getClearContent().toModel<MessageVerificationRequestContent>()?.let {
|
||||
if (it.fromDevice != deviceId) {
|
||||
// The verification is requested from another device
|
||||
Timber.v("## SAS Verification live observer: Transaction requested from other device tid:${event.eventId} ")
|
||||
event.eventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (EventType.KEY_VERIFICATION_START == event.type) {
|
||||
event.getClearContent().toModel<MessageVerificationStartContent>()?.let {
|
||||
if (it.fromDevice != deviceId) {
|
||||
// The verification is started from another device
|
||||
Timber.v("## SAS Verification live observer: Transaction started by other device tid:${it.transactionID} ")
|
||||
it.transactionID?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
|
||||
}
|
||||
}
|
||||
} else if (EventType.KEY_VERIFICATION_CANCEL == event.type || EventType.KEY_VERIFICATION_DONE == event.type) {
|
||||
event.getClearContent().toModel<MessageRelationContent>()?.relatesTo?.eventId?.let {
|
||||
transactionsHandledByOtherDevice.remove(it)
|
||||
}
|
||||
}
|
||||
|
||||
return@forEach
|
||||
}
|
||||
|
||||
val relatesTo = event.getClearContent().toModel<MessageRelationContent>()?.relatesTo?.eventId
|
||||
if (relatesTo != null && transactionsHandledByOtherDevice.contains(relatesTo)) {
|
||||
// Ignore this event, it is directed to another of my devices
|
||||
Timber.v("## SAS Verification live observer: Ignore Transaction handled by other device tid:$relatesTo ")
|
||||
return@forEach
|
||||
}
|
||||
when (event.getClearType()) {
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
EventType.KEY_VERIFICATION_KEY,
|
||||
EventType.KEY_VERIFICATION_MAC,
|
||||
EventType.KEY_VERIFICATION_CANCEL,
|
||||
EventType.KEY_VERIFICATION_DONE -> {
|
||||
sasVerificationService.onRoomEvent(event)
|
||||
}
|
||||
EventType.MESSAGE -> {
|
||||
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.type) {
|
||||
sasVerificationService.onRoomRequestReceived(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
roomVerificationUpdateTask.configureWith(
|
||||
RoomVerificationUpdateTask.Params(events, sasVerificationService, cryptoService)
|
||||
).executeBy(taskExecutor)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import dagger.Component
|
|||
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.internal.crypto.CryptoModule
|
||||
import im.vector.matrix.android.internal.crypto.verification.SendVerificationMessageWorker
|
||||
import im.vector.matrix.android.internal.di.MatrixComponent
|
||||
import im.vector.matrix.android.internal.di.SessionAssistedInjectModule
|
||||
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
|
||||
|
@ -98,6 +99,8 @@ internal interface SessionComponent {
|
|||
|
||||
fun inject(addHttpPusherWorker: AddHttpPusherWorker)
|
||||
|
||||
fun inject(sendVerificationMessageWorker: SendVerificationMessageWorker)
|
||||
|
||||
@Component.Factory
|
||||
interface Factory {
|
||||
fun create(
|
||||
|
|
|
@ -71,6 +71,20 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
|
|||
}
|
||||
}
|
||||
|
||||
override fun getExistingDirectRoomWithUser(otherUserId: String): Room? {
|
||||
Realm.getInstance(monarchy.realmConfiguration).use { realm ->
|
||||
val roomId = RoomSummaryEntity.where(realm)
|
||||
.equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
|
||||
.findAll()?.let { dms ->
|
||||
dms.firstOrNull {
|
||||
it.otherMemberIds.contains(otherUserId)
|
||||
}
|
||||
}
|
||||
?.roomId ?: return null
|
||||
return RoomEntity.where(realm, roomId).findFirst()?.let { roomFactory.create(roomId) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRoomSummary(roomIdOrAlias: String): RoomSummary? {
|
||||
return monarchy
|
||||
.fetchCopyMap({
|
||||
|
|
|
@ -20,8 +20,7 @@ import android.content.Context
|
|||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.api.failure.MatrixError
|
||||
import im.vector.matrix.android.api.failure.shouldBeRetried
|
||||
import im.vector.matrix.android.api.session.events.model.Content
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
|
@ -77,11 +76,6 @@ internal class SendEventWorker constructor(context: Context, params: WorkerParam
|
|||
}
|
||||
}
|
||||
|
||||
private fun Throwable.shouldBeRetried(): Boolean {
|
||||
return this is Failure.NetworkConnection
|
||||
|| (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED)
|
||||
}
|
||||
|
||||
private suspend fun sendEvent(eventId: String, eventType: String, content: Content?, roomId: String) {
|
||||
localEchoUpdater.updateSendState(eventId, SendState.SENDING)
|
||||
executeRequest<SendResponse> {
|
||||
|
|
|
@ -23,10 +23,7 @@ import dagger.Binds
|
|||
import dagger.Module
|
||||
import dagger.multibindings.IntoMap
|
||||
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment
|
||||
import im.vector.riotx.features.crypto.verification.SASVerificationIncomingFragment
|
||||
import im.vector.riotx.features.crypto.verification.SASVerificationShortCodeFragment
|
||||
import im.vector.riotx.features.crypto.verification.SASVerificationStartFragment
|
||||
import im.vector.riotx.features.crypto.verification.SASVerificationVerifiedFragment
|
||||
import im.vector.riotx.features.crypto.verification.*
|
||||
import im.vector.riotx.features.home.HomeDetailFragment
|
||||
import im.vector.riotx.features.home.HomeDrawerFragment
|
||||
import im.vector.riotx.features.home.LoadingFragment
|
||||
|
@ -272,4 +269,24 @@ interface FragmentModule {
|
|||
@IntoMap
|
||||
@FragmentKey(SoftLogoutFragment::class)
|
||||
fun bindSoftLogoutFragment(fragment: SoftLogoutFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(VerificationRequestFragment::class)
|
||||
fun bindVerificationRequestFragment(fragment: VerificationRequestFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(VerificationChooseMethodFragment::class)
|
||||
fun bindVerificationMethodChooserFragment(fragment: VerificationChooseMethodFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(SASVerificationCodeFragment::class)
|
||||
fun bindVerificationSasCodeFragment(fragment: SASVerificationCodeFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(VerificationConclusionFragment::class)
|
||||
fun bindVerificationSasConclusionFragment(fragment: VerificationConclusionFragment): Fragment
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import im.vector.riotx.core.error.ErrorFormatter
|
|||
import im.vector.riotx.core.preference.UserAvatarPreference
|
||||
import im.vector.riotx.features.MainActivity
|
||||
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
||||
import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
|
||||
import im.vector.riotx.features.home.HomeActivity
|
||||
import im.vector.riotx.features.home.HomeModule
|
||||
import im.vector.riotx.features.home.createdirect.CreateDirectRoomActivity
|
||||
|
@ -133,6 +134,8 @@ interface ScreenComponent {
|
|||
|
||||
fun inject(activity: SoftLogoutActivity)
|
||||
|
||||
fun inject(verificationBottomSheet: VerificationBottomSheet)
|
||||
|
||||
fun inject(permalinkHandlerActivity: PermalinkHandlerActivity)
|
||||
|
||||
@Component.Factory
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotx.core.utils
|
||||
|
||||
import android.text.Spannable
|
||||
import android.text.style.BulletSpan
|
||||
import android.text.style.ClickableSpan
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.StyleSpan
|
||||
import androidx.annotation.ColorInt
|
||||
import me.gujun.android.span.Span
|
||||
|
||||
fun Spannable.styleMatchingText(match: String, typeFace: Int): Spannable {
|
||||
if (match.isEmpty()) return this
|
||||
indexOf(match).takeIf { it != -1 }?.let { start ->
|
||||
this.setSpan(StyleSpan(typeFace), start, start + match.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun Spannable.colorizeMatchingText(match: String, @ColorInt color: Int): Spannable {
|
||||
if (match.isEmpty()) return this
|
||||
indexOf(match).takeIf { it != -1 }?.let { start ->
|
||||
this.setSpan(ForegroundColorSpan(color), start, start + match.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun Spannable.tappableMatchingText(match: String, clickSpan: ClickableSpan): Spannable {
|
||||
if (match.isEmpty()) return this
|
||||
indexOf(match).takeIf { it != -1 }?.let { start ->
|
||||
this.setSpan(clickSpan, start, start + match.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun Span.bullet(text: CharSequence = "",
|
||||
init: Span.() -> Unit = {}): Span = apply {
|
||||
append(Span(parent = this).apply {
|
||||
this.text = text
|
||||
this.spans.add(BulletSpan())
|
||||
init()
|
||||
build()
|
||||
})
|
||||
}
|
|
@ -31,6 +31,7 @@ import im.vector.riotx.core.extensions.observeEvent
|
|||
import im.vector.riotx.core.platform.SimpleFragmentActivity
|
||||
import im.vector.riotx.core.platform.WaitingViewData
|
||||
|
||||
// TODO Deprecated("replaced by bottomsheet UX")
|
||||
class SASVerificationActivity : SimpleFragmentActivity() {
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotx.features.crypto.verification
|
||||
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import butterknife.BindView
|
||||
import butterknife.OnClick
|
||||
import com.airbnb.mvrx.*
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import kotlinx.android.synthetic.main.fragment_bottom_sas_verification_code.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class SASVerificationCodeFragment @Inject constructor(
|
||||
val viewModelFactory: SASVerificationCodeViewModel.Factory
|
||||
) : VectorBaseFragment() {
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_bottom_sas_verification_code
|
||||
|
||||
@BindView(R.id.sas_emoji_grid)
|
||||
lateinit var emojiGrid: ViewGroup
|
||||
|
||||
@BindView(R.id.sas_decimal_code)
|
||||
lateinit var decimalTextView: TextView
|
||||
|
||||
@BindView(R.id.emoji0)
|
||||
lateinit var emoji0View: ViewGroup
|
||||
@BindView(R.id.emoji1)
|
||||
lateinit var emoji1View: ViewGroup
|
||||
@BindView(R.id.emoji2)
|
||||
lateinit var emoji2View: ViewGroup
|
||||
@BindView(R.id.emoji3)
|
||||
lateinit var emoji3View: ViewGroup
|
||||
@BindView(R.id.emoji4)
|
||||
lateinit var emoji4View: ViewGroup
|
||||
@BindView(R.id.emoji5)
|
||||
lateinit var emoji5View: ViewGroup
|
||||
@BindView(R.id.emoji6)
|
||||
lateinit var emoji6View: ViewGroup
|
||||
|
||||
private val viewModel by fragmentViewModel(SASVerificationCodeViewModel::class)
|
||||
|
||||
private val sharedViewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class)
|
||||
|
||||
override fun invalidate() = withState(viewModel) { state ->
|
||||
|
||||
if (state.supportsEmoji) {
|
||||
decimalTextView.isVisible = false
|
||||
when (val emojiDescription = state.emojiDescription) {
|
||||
is Success -> {
|
||||
sasLoadingProgress.isVisible = false
|
||||
emojiGrid.isVisible = true
|
||||
ButtonsVisibilityGroup.isVisible = true
|
||||
emojiDescription.invoke().forEachIndexed { index, emojiRepresentation ->
|
||||
when (index) {
|
||||
0 -> {
|
||||
emoji0View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
|
||||
emoji0View.findViewById<TextView>(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
|
||||
}
|
||||
1 -> {
|
||||
emoji1View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
|
||||
emoji1View.findViewById<TextView>(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
|
||||
}
|
||||
2 -> {
|
||||
emoji2View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
|
||||
emoji2View.findViewById<TextView>(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
|
||||
}
|
||||
3 -> {
|
||||
emoji3View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
|
||||
emoji3View.findViewById<TextView>(R.id.item_emoji_name_tv)?.setText(emojiRepresentation.nameResId)
|
||||
}
|
||||
4 -> {
|
||||
emoji4View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
|
||||
emoji4View.findViewById<TextView>(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
|
||||
}
|
||||
5 -> {
|
||||
emoji5View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
|
||||
emoji5View.findViewById<TextView>(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
|
||||
}
|
||||
6 -> {
|
||||
emoji6View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
|
||||
emoji6View.findViewById<TextView>(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.isWaitingFromOther) {
|
||||
// hide buttons
|
||||
ButtonsVisibilityGroup.isInvisible = true
|
||||
sasCodeWaitingPartnerText.isVisible = true
|
||||
} else {
|
||||
ButtonsVisibilityGroup.isVisible = true
|
||||
sasCodeWaitingPartnerText.isVisible = false
|
||||
}
|
||||
}
|
||||
is Fail -> {
|
||||
sasLoadingProgress.isVisible = false
|
||||
emojiGrid.isInvisible = true
|
||||
ButtonsVisibilityGroup.isInvisible = true
|
||||
// TODO?
|
||||
}
|
||||
else -> {
|
||||
sasLoadingProgress.isVisible = true
|
||||
emojiGrid.isInvisible = true
|
||||
ButtonsVisibilityGroup.isInvisible = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Decimal
|
||||
emojiGrid.isInvisible = true
|
||||
decimalTextView.isVisible = true
|
||||
val decimalCode = state.decimalDescription.invoke()
|
||||
decimalTextView.text = decimalCode
|
||||
|
||||
// TODO
|
||||
if (state.isWaitingFromOther) {
|
||||
// hide buttons
|
||||
ButtonsVisibilityGroup.isInvisible = true
|
||||
sasCodeWaitingPartnerText.isVisible = true
|
||||
} else {
|
||||
ButtonsVisibilityGroup.isVisible = decimalCode != null
|
||||
sasCodeWaitingPartnerText.isVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OnClick(R.id.sas_request_continue_button)
|
||||
fun onMatchButtonTapped() = withState(viewModel) { state ->
|
||||
val otherUserId = state.otherUser?.id ?: return@withState
|
||||
val txId = state.transactionId ?: return@withState
|
||||
// UX echo
|
||||
ButtonsVisibilityGroup.isInvisible = true
|
||||
sasCodeWaitingPartnerText.isVisible = true
|
||||
sharedViewModel.handle(VerificationAction.SASMatchAction(otherUserId, txId))
|
||||
}
|
||||
|
||||
@OnClick(R.id.sas_request_cancel_button)
|
||||
fun onDoNotMatchButtonTapped() = withState(viewModel) { state ->
|
||||
val otherUserId = state.otherUser?.id ?: return@withState
|
||||
val txId = state.transactionId ?: return@withState
|
||||
// UX echo
|
||||
ButtonsVisibilityGroup.isInvisible = true
|
||||
sasCodeWaitingPartnerText.isVisible = true
|
||||
sharedViewModel.handle(VerificationAction.SASDoNotMatchAction(otherUserId, txId))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotx.features.crypto.verification
|
||||
|
||||
import com.airbnb.mvrx.*
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.crypto.sas.EmojiRepresentation
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
|
||||
import im.vector.matrix.android.api.util.MatrixItem
|
||||
import im.vector.matrix.android.api.util.toMatrixItem
|
||||
import im.vector.riotx.core.di.HasScreenInjector
|
||||
import im.vector.riotx.core.platform.EmptyAction
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
|
||||
data class SASVerificationCodeViewState(
|
||||
val transactionId: String?,
|
||||
val otherUser: MatrixItem? = null,
|
||||
val supportsEmoji: Boolean = true,
|
||||
val emojiDescription: Async<List<EmojiRepresentation>> = Uninitialized,
|
||||
val decimalDescription: Async<String> = Uninitialized,
|
||||
val isWaitingFromOther: Boolean = false
|
||||
) : MvRxState
|
||||
|
||||
class SASVerificationCodeViewModel @AssistedInject constructor(
|
||||
@Assisted initialState: SASVerificationCodeViewState,
|
||||
private val session: Session
|
||||
) : VectorViewModel<SASVerificationCodeViewState, EmptyAction>(initialState), SasVerificationService.SasVerificationListener {
|
||||
|
||||
init {
|
||||
withState { state ->
|
||||
refreshStateFromTx(session.getSasVerificationService()
|
||||
.getExistingTransaction(state.otherUser?.id ?: "", state.transactionId
|
||||
?: ""))
|
||||
}
|
||||
|
||||
session.getSasVerificationService().addListener(this)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
session.getSasVerificationService().removeListener(this)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
private fun refreshStateFromTx(sasTx: SasVerificationTransaction?) {
|
||||
when (sasTx?.state) {
|
||||
SasVerificationTxState.None,
|
||||
SasVerificationTxState.SendingStart,
|
||||
SasVerificationTxState.Started,
|
||||
SasVerificationTxState.OnStarted,
|
||||
SasVerificationTxState.SendingAccept,
|
||||
SasVerificationTxState.Accepted,
|
||||
SasVerificationTxState.OnAccepted,
|
||||
SasVerificationTxState.SendingKey,
|
||||
SasVerificationTxState.KeySent,
|
||||
SasVerificationTxState.OnKeyReceived -> {
|
||||
setState {
|
||||
copy(
|
||||
isWaitingFromOther = false,
|
||||
supportsEmoji = sasTx.supportsEmoji(),
|
||||
emojiDescription = Loading<List<EmojiRepresentation>>()
|
||||
.takeIf { sasTx.supportsEmoji() }
|
||||
?: Uninitialized,
|
||||
decimalDescription = Loading<String>()
|
||||
.takeIf { sasTx.supportsEmoji().not() }
|
||||
?: Uninitialized
|
||||
)
|
||||
}
|
||||
}
|
||||
SasVerificationTxState.ShortCodeReady -> {
|
||||
setState {
|
||||
copy(
|
||||
isWaitingFromOther = false,
|
||||
supportsEmoji = sasTx.supportsEmoji(),
|
||||
emojiDescription = if (sasTx.supportsEmoji()) Success(sasTx.getEmojiCodeRepresentation())
|
||||
else Uninitialized,
|
||||
decimalDescription = if (!sasTx.supportsEmoji()) Success(sasTx.getDecimalCodeRepresentation())
|
||||
else Uninitialized
|
||||
)
|
||||
}
|
||||
}
|
||||
SasVerificationTxState.ShortCodeAccepted,
|
||||
SasVerificationTxState.SendingMac,
|
||||
SasVerificationTxState.MacSent,
|
||||
SasVerificationTxState.Verifying,
|
||||
SasVerificationTxState.Verified -> {
|
||||
setState {
|
||||
copy(isWaitingFromOther = true)
|
||||
}
|
||||
}
|
||||
SasVerificationTxState.Cancelled,
|
||||
SasVerificationTxState.OnCancelled -> {
|
||||
// The fragment should not be rendered in this state,
|
||||
// it should have been replaced by a conclusion fragment
|
||||
setState {
|
||||
copy(
|
||||
isWaitingFromOther = false,
|
||||
supportsEmoji = sasTx.supportsEmoji(),
|
||||
emojiDescription = Fail(Throwable("Transaction Cancelled")),
|
||||
decimalDescription = Fail(Throwable("Transaction Cancelled"))
|
||||
)
|
||||
}
|
||||
}
|
||||
null -> {
|
||||
setState {
|
||||
copy(
|
||||
isWaitingFromOther = false,
|
||||
emojiDescription = Fail(Throwable("Unknown Transaction")),
|
||||
decimalDescription = Fail(Throwable("Unknown Transaction"))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun transactionCreated(tx: SasVerificationTransaction) {
|
||||
transactionUpdated(tx)
|
||||
}
|
||||
|
||||
override fun transactionUpdated(tx: SasVerificationTransaction) = withState { state ->
|
||||
if (tx.transactionId == state.transactionId) {
|
||||
refreshStateFromTx(tx)
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(initialState: SASVerificationCodeViewState): SASVerificationCodeViewModel
|
||||
}
|
||||
|
||||
companion object : MvRxViewModelFactory<SASVerificationCodeViewModel, SASVerificationCodeViewState> {
|
||||
|
||||
override fun create(viewModelContext: ViewModelContext, state: SASVerificationCodeViewState): SASVerificationCodeViewModel? {
|
||||
val factory = (viewModelContext as FragmentViewModelContext).fragment<SASVerificationCodeFragment>().viewModelFactory
|
||||
return factory.create(state)
|
||||
}
|
||||
|
||||
override fun initialState(viewModelContext: ViewModelContext): SASVerificationCodeViewState? {
|
||||
val args = viewModelContext.args<VerificationBottomSheet.VerificationArgs>()
|
||||
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
|
||||
val matrixItem = session.getUser(args.otherUserId)?.toMatrixItem()
|
||||
|
||||
return SASVerificationCodeViewState(
|
||||
transactionId = args.verificationId,
|
||||
otherUser = matrixItem
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handle(action: EmptyAction) {
|
||||
}
|
||||
}
|
|
@ -29,6 +29,7 @@ import im.vector.riotx.core.platform.VectorBaseFragment
|
|||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import javax.inject.Inject
|
||||
|
||||
// TODO Deprecated("replaced by bottomsheet UX")
|
||||
class SASVerificationIncomingFragment @Inject constructor(
|
||||
private var avatarRenderer: AvatarRenderer
|
||||
) : VectorBaseFragment() {
|
||||
|
|
|
@ -29,6 +29,7 @@ import im.vector.riotx.R
|
|||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import javax.inject.Inject
|
||||
|
||||
// TODO Deprecated("replaced by bottomsheet UX")
|
||||
class SASVerificationShortCodeFragment @Inject constructor(): VectorBaseFragment() {
|
||||
|
||||
private lateinit var viewModel: SasVerificationViewModel
|
||||
|
|
|
@ -32,6 +32,7 @@ import im.vector.riotx.core.platform.VectorBaseActivity
|
|||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import javax.inject.Inject
|
||||
|
||||
// TODO Deprecated("replaced by bottomsheet UX")
|
||||
class SASVerificationStartFragment @Inject constructor(): VectorBaseFragment() {
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_sas_verification_start
|
||||
|
@ -91,7 +92,7 @@ class SASVerificationStartFragment @Inject constructor(): VectorBaseFragment() {
|
|||
(requireActivity() as VectorBaseActivity).notImplemented()
|
||||
|
||||
/*
|
||||
viewModel.session.crypto?.getDeviceInfo(viewModel.otherUserId ?: "", viewModel.otherDeviceId
|
||||
viewModel.session.crypto?.getDeviceInfo(viewModel.otherUserMxItem ?: "", viewModel.otherDeviceId
|
||||
?: "", object : SimpleApiCallback<MXDeviceInfo>() {
|
||||
override fun onSuccess(info: MXDeviceInfo?) {
|
||||
info?.let {
|
||||
|
|
|
@ -21,6 +21,7 @@ import im.vector.riotx.R
|
|||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import javax.inject.Inject
|
||||
|
||||
// TODO Deprecated("replaced by bottomsheet UX")
|
||||
class SASVerificationVerifiedFragment @Inject constructor() : VectorBaseFragment() {
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_sas_verification_verified
|
||||
|
|
|
@ -27,6 +27,7 @@ import im.vector.matrix.android.api.session.user.model.User
|
|||
import im.vector.riotx.core.utils.LiveEvent
|
||||
import javax.inject.Inject
|
||||
|
||||
// TODO Deprecated("replaced by bottomsheet UX")
|
||||
class SasVerificationViewModel @Inject constructor() : ViewModel(),
|
||||
SasVerificationService.SasVerificationListener {
|
||||
|
||||
|
|
|
@ -0,0 +1,235 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotx.features.crypto.verification
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.text.toSpannable
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.transition.AutoTransition
|
||||
import androidx.transition.TransitionManager
|
||||
import butterknife.BindView
|
||||
import butterknife.ButterKnife
|
||||
import butterknife.Unbinder
|
||||
import com.airbnb.mvrx.MvRx
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
import im.vector.riotx.core.extensions.commitTransactionNow
|
||||
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
|
||||
import im.vector.riotx.core.utils.colorizeMatchingText
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import im.vector.riotx.features.themes.ThemeUtils
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.bottom_sheet_verification.*
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
|
||||
@Parcelize
|
||||
data class VerificationArgs(
|
||||
val otherUserId: String,
|
||||
val verificationId: String? = null,
|
||||
val roomId: String? = null
|
||||
) : Parcelable
|
||||
|
||||
@Inject
|
||||
lateinit var verificationRequestViewModelFactory: VerificationRequestViewModel.Factory
|
||||
@Inject
|
||||
lateinit var verificationViewModelFactory: VerificationBottomSheetViewModel.Factory
|
||||
@Inject
|
||||
lateinit var avatarRenderer: AvatarRenderer
|
||||
|
||||
private val viewModel by fragmentViewModel(VerificationBottomSheetViewModel::class)
|
||||
|
||||
override fun injectWith(injector: ScreenComponent) {
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
@BindView(R.id.verificationRequestName)
|
||||
lateinit var otherUserNameText: TextView
|
||||
|
||||
@BindView(R.id.verificationRequestAvatar)
|
||||
lateinit var otherUserAvatarImageView: ImageView
|
||||
|
||||
private var unBinder: Unbinder? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val view = inflater.inflate(R.layout.bottom_sheet_verification, container, false)
|
||||
unBinder = ButterKnife.bind(this, view)
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
unBinder?.unbind()
|
||||
unBinder = null
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
viewModel.requestLiveData.observe(viewLifecycleOwner, Observer {
|
||||
it.peekContent().let { va ->
|
||||
when (va) {
|
||||
is Success -> {
|
||||
if (va.invoke() is VerificationAction.GotItConclusion) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(viewModel) {
|
||||
it.otherUserMxItem?.let { matrixItem ->
|
||||
val displayName = matrixItem.displayName ?: ""
|
||||
otherUserNameText.text = getString(R.string.verification_request_alert_title, displayName)
|
||||
.toSpannable()
|
||||
.colorizeMatchingText(displayName, ThemeUtils.getColor(requireContext(), R.attr.vctr_notice_text_color))
|
||||
|
||||
avatarRenderer.render(matrixItem, otherUserAvatarImageView)
|
||||
}
|
||||
|
||||
// Did the request result in a SAS transaction?
|
||||
if (it.sasTransactionState != null) {
|
||||
when (it.sasTransactionState) {
|
||||
SasVerificationTxState.None,
|
||||
SasVerificationTxState.SendingStart,
|
||||
SasVerificationTxState.Started,
|
||||
SasVerificationTxState.OnStarted,
|
||||
SasVerificationTxState.SendingAccept,
|
||||
SasVerificationTxState.Accepted,
|
||||
SasVerificationTxState.OnAccepted,
|
||||
SasVerificationTxState.SendingKey,
|
||||
SasVerificationTxState.KeySent,
|
||||
SasVerificationTxState.OnKeyReceived,
|
||||
SasVerificationTxState.ShortCodeReady,
|
||||
SasVerificationTxState.ShortCodeAccepted,
|
||||
SasVerificationTxState.SendingMac,
|
||||
SasVerificationTxState.MacSent,
|
||||
SasVerificationTxState.Verifying -> {
|
||||
showFragment(SASVerificationCodeFragment::class, Bundle().apply {
|
||||
putParcelable(MvRx.KEY_ARG, VerificationArgs(
|
||||
it.otherUserMxItem?.id ?: "",
|
||||
it.pendingRequest?.transactionId))
|
||||
})
|
||||
}
|
||||
SasVerificationTxState.Verified,
|
||||
SasVerificationTxState.Cancelled,
|
||||
SasVerificationTxState.OnCancelled -> {
|
||||
showFragment(VerificationConclusionFragment::class, Bundle().apply {
|
||||
putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(
|
||||
it.sasTransactionState == SasVerificationTxState.Verified,
|
||||
it.cancelCode?.value))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return@withState
|
||||
}
|
||||
|
||||
// At this point there is no transaction for this request
|
||||
|
||||
// Transaction has not yet started
|
||||
if (it.pendingRequest?.cancelConclusion != null) {
|
||||
// The request has been declined, we should dismiss
|
||||
dismiss()
|
||||
}
|
||||
|
||||
// If it's an outgoing
|
||||
if (it.pendingRequest == null || !it.pendingRequest.isIncoming) {
|
||||
Timber.v("## SAS show bottom sheet for outgoing request")
|
||||
if (it.pendingRequest?.isReady == true) {
|
||||
Timber.v("## SAS show bottom sheet for outgoing and ready request")
|
||||
// Show choose method fragment with waiting
|
||||
showFragment(VerificationChooseMethodFragment::class, Bundle().apply {
|
||||
putParcelable(MvRx.KEY_ARG, VerificationArgs(it.otherUserMxItem?.id
|
||||
?: "", it.pendingRequest.transactionId))
|
||||
})
|
||||
} else {
|
||||
// Stay on the start fragment
|
||||
showFragment(VerificationRequestFragment::class, Bundle().apply {
|
||||
putParcelable(MvRx.KEY_ARG, VerificationArgs(
|
||||
it.otherUserMxItem?.id ?: "",
|
||||
it.pendingRequest?.transactionId,
|
||||
it.roomId))
|
||||
})
|
||||
}
|
||||
} else if (it.pendingRequest.isIncoming) {
|
||||
Timber.v("## SAS show bottom sheet for Incoming request")
|
||||
// For incoming we can switch to choose method because ready is being sent or already sent
|
||||
showFragment(VerificationChooseMethodFragment::class, Bundle().apply {
|
||||
putParcelable(MvRx.KEY_ARG, VerificationArgs(it.otherUserMxItem?.id
|
||||
?: "", it.pendingRequest.transactionId))
|
||||
})
|
||||
}
|
||||
super.invalidate()
|
||||
}
|
||||
|
||||
private fun showFragment(fragmentClass: KClass<out Fragment>, bundle: Bundle) {
|
||||
if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) {
|
||||
// We want to animate the bottomsheet bound changes
|
||||
bottomSheetFragmentContainer.getParentCoordinatorLayout()?.let { coordinatorLayout ->
|
||||
TransitionManager.beginDelayedTransition(coordinatorLayout, AutoTransition().apply { duration = 150 })
|
||||
}
|
||||
// Commit now, to ensure changes occurs before next rendering frame (or bottomsheet want animate)
|
||||
childFragmentManager.commitTransactionNow {
|
||||
replace(R.id.bottomSheetFragmentContainer,
|
||||
fragmentClass.java,
|
||||
bundle,
|
||||
fragmentClass.simpleName
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun withArgs(roomId: String, otherUserId: String, transactionId: String? = null): VerificationBottomSheet {
|
||||
return VerificationBottomSheet().apply {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable(MvRx.KEY_ARG, VerificationBottomSheet.VerificationArgs(
|
||||
otherUserId = otherUserId,
|
||||
roomId = roomId,
|
||||
verificationId = transactionId
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun View.getParentCoordinatorLayout(): CoordinatorLayout? {
|
||||
var current = this as? View
|
||||
while (current != null) {
|
||||
if (current is CoordinatorLayout) return current
|
||||
current = current.parent as? View
|
||||
}
|
||||
return null
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotx.features.crypto.verification
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.airbnb.mvrx.*
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
|
||||
import im.vector.matrix.android.api.util.MatrixItem
|
||||
import im.vector.matrix.android.api.util.toMatrixItem
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart
|
||||
import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest
|
||||
import im.vector.riotx.core.di.HasScreenInjector
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
import im.vector.riotx.core.platform.VectorViewModelAction
|
||||
import im.vector.riotx.core.utils.LiveEvent
|
||||
|
||||
data class VerificationBottomSheetViewState(
|
||||
val otherUserMxItem: MatrixItem? = null,
|
||||
val roomId: String? = null,
|
||||
val pendingRequest: PendingVerificationRequest? = null,
|
||||
val sasTransactionState: SasVerificationTxState? = null,
|
||||
val cancelCode: CancelCode? = null
|
||||
) : MvRxState
|
||||
|
||||
sealed class VerificationAction : VectorViewModelAction {
|
||||
data class RequestVerificationByDM(val userID: String, val roomId: String?) : VerificationAction()
|
||||
data class StartSASVerification(val userID: String, val pendingRequestTransactionId: String) : VerificationAction()
|
||||
data class SASMatchAction(val userID: String, val sasTransactionId: String) : VerificationAction()
|
||||
data class SASDoNotMatchAction(val userID: String, val sasTransactionId: String) : VerificationAction()
|
||||
object GotItConclusion : VerificationAction()
|
||||
}
|
||||
|
||||
class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: VerificationBottomSheetViewState,
|
||||
private val session: Session)
|
||||
: VectorViewModel<VerificationBottomSheetViewState, VerificationAction>(initialState),
|
||||
SasVerificationService.SasVerificationListener {
|
||||
|
||||
// Can be used for several actions, for a one shot result
|
||||
private val _requestLiveData = MutableLiveData<LiveEvent<Async<VerificationAction>>>()
|
||||
val requestLiveData: LiveData<LiveEvent<Async<VerificationAction>>>
|
||||
get() = _requestLiveData
|
||||
|
||||
init {
|
||||
session.getSasVerificationService().addListener(this)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
session.getSasVerificationService().removeListener(this)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(initialState: VerificationBottomSheetViewState): VerificationBottomSheetViewModel
|
||||
}
|
||||
|
||||
companion object : MvRxViewModelFactory<VerificationBottomSheetViewModel, VerificationBottomSheetViewState> {
|
||||
|
||||
override fun create(viewModelContext: ViewModelContext, state: VerificationBottomSheetViewState): VerificationBottomSheetViewModel? {
|
||||
val fragment: VerificationBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
|
||||
val args: VerificationBottomSheet.VerificationArgs = viewModelContext.args()
|
||||
|
||||
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
|
||||
|
||||
val userItem = session.getUser(args.otherUserId)
|
||||
|
||||
val pr = session.getSasVerificationService().getExistingVerificationRequest(args.otherUserId, args.verificationId)
|
||||
|
||||
val sasTx = pr?.transactionId?.let {
|
||||
session.getSasVerificationService().getExistingTransaction(args.otherUserId, it)
|
||||
}
|
||||
|
||||
return fragment.verificationViewModelFactory.create(VerificationBottomSheetViewState(
|
||||
otherUserMxItem = userItem?.toMatrixItem(),
|
||||
sasTransactionState = sasTx?.state,
|
||||
pendingRequest = pr,
|
||||
roomId = args.roomId)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handle(action: VerificationAction) = withState { state ->
|
||||
val otherUserId = state.otherUserMxItem?.id ?: return@withState
|
||||
val roomId = state.roomId
|
||||
?: session.getExistingDirectRoomWithUser(otherUserId)?.roomId
|
||||
?: return@withState
|
||||
when (action) {
|
||||
is VerificationAction.RequestVerificationByDM -> {
|
||||
// session
|
||||
setState {
|
||||
copy(pendingRequest = session.getSasVerificationService().requestKeyVerificationInDMs(otherUserId, roomId))
|
||||
}
|
||||
}
|
||||
is VerificationAction.StartSASVerification -> {
|
||||
val request = session.getSasVerificationService().getExistingVerificationRequest(otherUserId, action.pendingRequestTransactionId)
|
||||
?: return@withState
|
||||
|
||||
val otherDevice = if (request.isIncoming) request.requestInfo?.fromDevice else request.readyInfo?.fromDevice
|
||||
session.getSasVerificationService().beginKeyVerificationInDMs(
|
||||
KeyVerificationStart.VERIF_METHOD_SAS,
|
||||
transactionId = action.pendingRequestTransactionId,
|
||||
roomId = roomId,
|
||||
otherUserId = request.otherUserId,
|
||||
otherDeviceId = otherDevice ?: "",
|
||||
callback = null
|
||||
)
|
||||
}
|
||||
is VerificationAction.SASMatchAction -> {
|
||||
session.getSasVerificationService()
|
||||
.getExistingTransaction(action.userID, action.sasTransactionId)
|
||||
?.userHasVerifiedShortCode()
|
||||
}
|
||||
is VerificationAction.SASDoNotMatchAction -> {
|
||||
session.getSasVerificationService()
|
||||
.getExistingTransaction(action.userID, action.sasTransactionId)
|
||||
?.shortCodeDoesNotMatch()
|
||||
}
|
||||
is VerificationAction.GotItConclusion -> {
|
||||
_requestLiveData.postValue(LiveEvent(Success(action)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun transactionCreated(tx: SasVerificationTransaction) {
|
||||
transactionUpdated(tx)
|
||||
}
|
||||
|
||||
override fun transactionUpdated(tx: SasVerificationTransaction) = withState { state ->
|
||||
if (tx.transactionId == state.pendingRequest?.transactionId) {
|
||||
// A SAS tx has been started following this request
|
||||
setState {
|
||||
copy(
|
||||
sasTransactionState = tx.state,
|
||||
cancelCode = tx.cancelledReason
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun verificationRequestCreated(pr: PendingVerificationRequest) {
|
||||
verificationRequestUpdated(pr)
|
||||
}
|
||||
|
||||
override fun verificationRequestUpdated(pr: PendingVerificationRequest) = withState { state ->
|
||||
|
||||
if (pr.localID == state.pendingRequest?.localID || state.pendingRequest?.transactionId == pr.transactionId) {
|
||||
setState {
|
||||
copy(pendingRequest = pr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotx.features.crypto.verification
|
||||
|
||||
import android.text.style.ClickableSpan
|
||||
import android.view.View
|
||||
import androidx.core.text.toSpannable
|
||||
import androidx.core.view.isVisible
|
||||
import butterknife.OnClick
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.parentFragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import im.vector.riotx.core.utils.tappableMatchingText
|
||||
import kotlinx.android.synthetic.main.fragment_verification_choose_method.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class VerificationChooseMethodFragment @Inject constructor(
|
||||
val verificationChooseMethodViewModelFactory: VerificationChooseMethodViewModel.Factory
|
||||
) : VectorBaseFragment() {
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_verification_choose_method
|
||||
|
||||
private val sharedViewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class)
|
||||
|
||||
private val viewModel by fragmentViewModel(VerificationChooseMethodViewModel::class)
|
||||
|
||||
override fun invalidate() = withState(viewModel) { state ->
|
||||
if (state.QRModeAvailable) {
|
||||
val cSpan = object : ClickableSpan() {
|
||||
override fun onClick(widget: View) {
|
||||
}
|
||||
}
|
||||
val openLink = getString(R.string.verify_open_camera_link)
|
||||
val descCharSequence =
|
||||
getString(R.string.verify_by_scanning_description, openLink)
|
||||
.toSpannable()
|
||||
.tappableMatchingText(openLink, cSpan)
|
||||
verifyQRDescription.text = descCharSequence
|
||||
verifyQRGroup.isVisible = true
|
||||
} else {
|
||||
verifyQRGroup.isVisible = false
|
||||
}
|
||||
|
||||
verifyEmojiGroup.isVisible = state.SASMOdeAvailable
|
||||
}
|
||||
|
||||
@OnClick(R.id.verificationByEmojiButton)
|
||||
fun doVerifyBySas() = withState(sharedViewModel) {
|
||||
sharedViewModel.handle(VerificationAction.StartSASVerification(it.otherUserMxItem?.id ?: "", it.pendingRequest?.transactionId
|
||||
?: ""))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotx.features.crypto.verification
|
||||
|
||||
import com.airbnb.mvrx.FragmentViewModelContext
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart
|
||||
import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest
|
||||
import im.vector.riotx.core.di.HasScreenInjector
|
||||
import im.vector.riotx.core.platform.EmptyAction
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
|
||||
data class VerificationChooseMethodViewState(
|
||||
val otherUserId: String = "",
|
||||
val transactionId: String = "",
|
||||
val QRModeAvailable: Boolean = false,
|
||||
val SASMOdeAvailable: Boolean = false
|
||||
) : MvRxState
|
||||
|
||||
class VerificationChooseMethodViewModel @AssistedInject constructor(
|
||||
@Assisted initialState: VerificationChooseMethodViewState,
|
||||
private val session: Session
|
||||
) : VectorViewModel<VerificationChooseMethodViewState, EmptyAction>(initialState), SasVerificationService.SasVerificationListener {
|
||||
|
||||
override fun transactionCreated(tx: SasVerificationTransaction) {}
|
||||
|
||||
override fun transactionUpdated(tx: SasVerificationTransaction) {}
|
||||
|
||||
override fun verificationRequestUpdated(pr: PendingVerificationRequest) = withState { state ->
|
||||
val pvr = session.getSasVerificationService().getExistingVerificationRequest(state.otherUserId, state.transactionId)
|
||||
val qrAvailable = pvr?.readyInfo?.methods?.contains(KeyVerificationStart.VERIF_METHOD_SCAN)
|
||||
?: false
|
||||
val emojiAvailable = pvr?.readyInfo?.methods?.contains(KeyVerificationStart.VERIF_METHOD_SAS)
|
||||
?: false
|
||||
|
||||
setState {
|
||||
copy(
|
||||
QRModeAvailable = qrAvailable,
|
||||
SASMOdeAvailable = emojiAvailable
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(initialState: VerificationChooseMethodViewState): VerificationChooseMethodViewModel
|
||||
}
|
||||
|
||||
init {
|
||||
session.getSasVerificationService().addListener(this)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
session.getSasVerificationService().removeListener(this)
|
||||
}
|
||||
|
||||
companion object : MvRxViewModelFactory<VerificationChooseMethodViewModel, VerificationChooseMethodViewState> {
|
||||
override fun create(viewModelContext: ViewModelContext, state: VerificationChooseMethodViewState): VerificationChooseMethodViewModel? {
|
||||
val fragment: VerificationChooseMethodFragment = (viewModelContext as FragmentViewModelContext).fragment()
|
||||
return fragment.verificationChooseMethodViewModelFactory.create(state)
|
||||
}
|
||||
|
||||
override fun initialState(viewModelContext: ViewModelContext): VerificationChooseMethodViewState? {
|
||||
val args: VerificationBottomSheet.VerificationArgs = viewModelContext.args()
|
||||
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
|
||||
val pvr = session.getSasVerificationService().getExistingVerificationRequest(args.otherUserId, args.verificationId)
|
||||
val qrAvailable = pvr?.readyInfo?.methods?.contains(KeyVerificationStart.VERIF_METHOD_SCAN)
|
||||
?: false
|
||||
val emojiAvailable = pvr?.readyInfo?.methods?.contains(KeyVerificationStart.VERIF_METHOD_SAS)
|
||||
?: false
|
||||
|
||||
return VerificationChooseMethodViewState(otherUserId = args.otherUserId,
|
||||
transactionId = args.verificationId ?: "",
|
||||
QRModeAvailable = qrAvailable,
|
||||
SASMOdeAvailable = emojiAvailable
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handle(action: EmptyAction) {}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotx.features.crypto.verification
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import butterknife.OnClick
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.parentFragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.extensions.setTextOrHide
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import io.noties.markwon.Markwon
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.fragment_verification_conclusion.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class VerificationConclusionFragment @Inject constructor() : VectorBaseFragment() {
|
||||
|
||||
@Parcelize
|
||||
data class Args(
|
||||
val isSuccessFull: Boolean,
|
||||
val cancelReason: String?
|
||||
) : Parcelable
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_verification_conclusion
|
||||
|
||||
private val sharedViewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class)
|
||||
|
||||
private val viewModel by fragmentViewModel(VerificationConclusionViewModel::class)
|
||||
|
||||
override fun invalidate() = withState(viewModel) {
|
||||
when (it.conclusionState) {
|
||||
ConclusionState.SUCCESS -> {
|
||||
verificationConclusionTitle.text = getString(R.string.sas_verified)
|
||||
verifyConclusionDescription.setTextOrHide(getString(R.string.sas_verified_successful_description))
|
||||
verifyConclusionBottomDescription.text = getString(R.string.verification_green_shield)
|
||||
verifyConclusionImageView.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_shield_trusted))
|
||||
}
|
||||
ConclusionState.WARNING -> {
|
||||
verificationConclusionTitle.text = getString(R.string.verification_conclusion_not_secure)
|
||||
verifyConclusionDescription.isVisible = false
|
||||
verifyConclusionImageView.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_shield_warning))
|
||||
|
||||
verifyConclusionBottomDescription.text = Markwon.builder(requireContext())
|
||||
.build()
|
||||
.toMarkdown(getString(R.string.verification_conclusion_compromised))
|
||||
}
|
||||
ConclusionState.CANCELLED -> {
|
||||
// Just dismiss in this case
|
||||
sharedViewModel.handle(VerificationAction.GotItConclusion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OnClick(R.id.verificationConclusionButton)
|
||||
fun onButtonTapped() {
|
||||
sharedViewModel.handle(VerificationAction.GotItConclusion)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotx.features.crypto.verification
|
||||
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
|
||||
import im.vector.matrix.android.api.session.crypto.sas.safeValueOf
|
||||
import im.vector.riotx.core.platform.EmptyAction
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
|
||||
data class SASVerificationConclusionViewState(
|
||||
val conclusionState: ConclusionState = ConclusionState.CANCELLED
|
||||
) : MvRxState
|
||||
|
||||
enum class ConclusionState {
|
||||
SUCCESS,
|
||||
WARNING,
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
class VerificationConclusionViewModel(initialState: SASVerificationConclusionViewState)
|
||||
: VectorViewModel<SASVerificationConclusionViewState, EmptyAction>(initialState) {
|
||||
|
||||
companion object : MvRxViewModelFactory<VerificationConclusionViewModel, SASVerificationConclusionViewState> {
|
||||
|
||||
override fun initialState(viewModelContext: ViewModelContext): SASVerificationConclusionViewState? {
|
||||
val args = viewModelContext.args<VerificationConclusionFragment.Args>()
|
||||
|
||||
return when (safeValueOf(args.cancelReason)) {
|
||||
CancelCode.MismatchedSas,
|
||||
CancelCode.MismatchedCommitment,
|
||||
CancelCode.MismatchedKeys -> {
|
||||
SASVerificationConclusionViewState(ConclusionState.WARNING)
|
||||
}
|
||||
else -> {
|
||||
SASVerificationConclusionViewState(
|
||||
if (args.isSuccessFull) ConclusionState.SUCCESS
|
||||
else ConclusionState.CANCELLED
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handle(action: EmptyAction) {}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotx.features.crypto.verification
|
||||
|
||||
import android.graphics.Typeface
|
||||
import androidx.core.text.toSpannable
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import butterknife.OnClick
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.parentFragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import im.vector.riotx.core.utils.colorizeMatchingText
|
||||
import im.vector.riotx.core.utils.styleMatchingText
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import im.vector.riotx.features.themes.ThemeUtils
|
||||
import kotlinx.android.synthetic.main.fragment_verification_request.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class VerificationRequestFragment @Inject constructor(
|
||||
val verificationRequestViewModelFactory: VerificationRequestViewModel.Factory,
|
||||
val avatarRenderer: AvatarRenderer
|
||||
) : VectorBaseFragment() {
|
||||
|
||||
private val viewModel by fragmentViewModel(VerificationRequestViewModel::class)
|
||||
|
||||
private val sharedViewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class)
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_verification_request
|
||||
|
||||
override fun invalidate() = withState(viewModel) { state ->
|
||||
state.matrixItem.let {
|
||||
val styledText = getString(R.string.verification_request_alert_description, it.id)
|
||||
.toSpannable()
|
||||
.styleMatchingText(it.id, Typeface.BOLD)
|
||||
.colorizeMatchingText(it.id, ThemeUtils.getColor(requireContext(), R.attr.vctr_notice_text_color))
|
||||
verificationRequestText.text = styledText
|
||||
}
|
||||
|
||||
when (state.started) {
|
||||
is Loading -> {
|
||||
// Hide the start button, show waiting
|
||||
verificationStartButton.isInvisible = true
|
||||
verificationWaitingText.isVisible = true
|
||||
val otherUser = state.matrixItem.displayName ?: state.matrixItem.id
|
||||
verificationWaitingText.text = getString(R.string.verification_request_waiting_for, otherUser)
|
||||
.toSpannable()
|
||||
.styleMatchingText(otherUser, Typeface.BOLD)
|
||||
.colorizeMatchingText(otherUser, ThemeUtils.getColor(requireContext(), R.attr.vctr_notice_text_color))
|
||||
}
|
||||
else -> {
|
||||
verificationStartButton.isEnabled = true
|
||||
verificationStartButton.isVisible = true
|
||||
verificationWaitingText.isInvisible = true
|
||||
}
|
||||
}
|
||||
|
||||
Unit
|
||||
}
|
||||
|
||||
@OnClick(R.id.verificationStartButton)
|
||||
fun onClickOnVerificationStart() = withState(viewModel) { state ->
|
||||
verificationStartButton.isEnabled = false
|
||||
sharedViewModel.handle(VerificationAction.RequestVerificationByDM(state.matrixItem.id, state.roomId))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotx.features.crypto.verification
|
||||
|
||||
import com.airbnb.mvrx.*
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
|
||||
import im.vector.matrix.android.api.util.MatrixItem
|
||||
import im.vector.matrix.android.api.util.toMatrixItem
|
||||
import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest
|
||||
import im.vector.riotx.core.di.HasScreenInjector
|
||||
import im.vector.riotx.core.platform.EmptyAction
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
|
||||
data class VerificationRequestViewState(
|
||||
val roomId: String? = null,
|
||||
val matrixItem: MatrixItem,
|
||||
val started: Async<Boolean> = Success(false)
|
||||
) : MvRxState
|
||||
|
||||
class VerificationRequestViewModel @AssistedInject constructor(
|
||||
@Assisted initialState: VerificationRequestViewState,
|
||||
private val session: Session
|
||||
) : VectorViewModel<VerificationRequestViewState, EmptyAction>(initialState), SasVerificationService.SasVerificationListener {
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(initialState: VerificationRequestViewState): VerificationRequestViewModel
|
||||
}
|
||||
|
||||
init {
|
||||
session.getSasVerificationService().addListener(this)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
session.getSasVerificationService().removeListener(this)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
companion object : MvRxViewModelFactory<VerificationRequestViewModel, VerificationRequestViewState> {
|
||||
override fun create(viewModelContext: ViewModelContext, state: VerificationRequestViewState): VerificationRequestViewModel? {
|
||||
val fragment: VerificationRequestFragment = (viewModelContext as FragmentViewModelContext).fragment()
|
||||
return fragment.verificationRequestViewModelFactory.create(state)
|
||||
}
|
||||
|
||||
override fun initialState(viewModelContext: ViewModelContext): VerificationRequestViewState? {
|
||||
val args = viewModelContext.args<VerificationBottomSheet.VerificationArgs>()
|
||||
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
|
||||
|
||||
val pr = session.getSasVerificationService()
|
||||
.getExistingVerificationRequest(args.otherUserId, args.verificationId)
|
||||
return session.getUser(args.otherUserId)?.let {
|
||||
VerificationRequestViewState(
|
||||
started = Success(false).takeIf { pr == null }
|
||||
?: Success(true).takeIf { pr?.isReady == true }
|
||||
?: Loading(),
|
||||
matrixItem = it.toMatrixItem()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handle(action: EmptyAction) {}
|
||||
|
||||
override fun transactionCreated(tx: SasVerificationTransaction) {}
|
||||
|
||||
override fun transactionUpdated(tx: SasVerificationTransaction) {}
|
||||
|
||||
override fun verificationRequestCreated(pr: PendingVerificationRequest) {
|
||||
verificationRequestUpdated(pr)
|
||||
}
|
||||
|
||||
override fun verificationRequestUpdated(pr: PendingVerificationRequest) = withState { state ->
|
||||
if (pr.otherUserId == state.matrixItem.id) {
|
||||
if (pr.isReady) {
|
||||
setState {
|
||||
copy(started = Success(true))
|
||||
}
|
||||
} else {
|
||||
setState {
|
||||
copy(started = Loading())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -65,5 +65,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
|||
object ResendAll : RoomDetailAction()
|
||||
|
||||
data class AcceptVerificationRequest(val transactionId: String, val otherUserId: String, val otherdDeviceId: String) : RoomDetailAction()
|
||||
data class DeclineVerificationRequest(val transactionId: String) : RoomDetailAction()
|
||||
data class DeclineVerificationRequest(val transactionId: String, val otherUserId: String, val otherdDeviceId: String) : RoomDetailAction()
|
||||
|
||||
data class RequestVerification(val userId: String) : RoomDetailAction()
|
||||
}
|
||||
|
|
|
@ -90,6 +90,7 @@ import im.vector.riotx.features.autocomplete.group.AutocompleteGroupPresenter
|
|||
import im.vector.riotx.features.autocomplete.room.AutocompleteRoomPresenter
|
||||
import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter
|
||||
import im.vector.riotx.features.command.Command
|
||||
import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import im.vector.riotx.features.home.getColorFromUserId
|
||||
import im.vector.riotx.features.home.room.detail.composer.TextComposerAction
|
||||
|
@ -431,7 +432,8 @@ class RoomDetailFragment @Inject constructor(
|
|||
composerLayout.sendButton.setContentDescription(getString(descriptionRes))
|
||||
|
||||
avatarRenderer.render(
|
||||
MatrixItem.UserItem(event.root.senderId ?: "", event.getDisambiguatedDisplayName(), event.senderAvatar),
|
||||
MatrixItem.UserItem(event.root.senderId
|
||||
?: "", event.getDisambiguatedDisplayName(), event.senderAvatar),
|
||||
composerLayout.composerRelatedMessageAvatar
|
||||
)
|
||||
composerLayout.expand {
|
||||
|
@ -923,7 +925,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
is Success -> {
|
||||
when (val data = result.invoke()) {
|
||||
is RoomDetailAction.ReportContent -> {
|
||||
is RoomDetailAction.ReportContent -> {
|
||||
when {
|
||||
data.spam -> {
|
||||
AlertDialog.Builder(requireActivity())
|
||||
|
@ -960,6 +962,21 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
is RoomDetailAction.RequestVerification -> {
|
||||
Timber.v("## SAS RequestVerification action")
|
||||
VerificationBottomSheet.withArgs(
|
||||
roomDetailArgs.roomId,
|
||||
data.userId
|
||||
).show(parentFragmentManager, "REQ")
|
||||
}
|
||||
is RoomDetailAction.AcceptVerificationRequest -> {
|
||||
Timber.v("## SAS AcceptVerificationRequest action")
|
||||
VerificationBottomSheet.withArgs(
|
||||
roomDetailArgs.roomId,
|
||||
data.otherUserId,
|
||||
data.transactionId
|
||||
).show(parentFragmentManager, "REQ")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1114,7 +1131,8 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onAvatarClicked(informationData: MessageInformationData) {
|
||||
vectorBaseActivity.notImplemented("Click on user avatar")
|
||||
// vectorBaseActivity.notImplemented("Click on user avatar")
|
||||
roomDetailViewModel.handle(RoomDetailAction.RequestVerification(informationData.senderId))
|
||||
}
|
||||
|
||||
override fun onMemberNameClicked(informationData: MessageInformationData) {
|
||||
|
|
|
@ -27,7 +27,6 @@ import com.squareup.inject.assisted.Assisted
|
|||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.MatrixPatterns
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.isImageMessage
|
||||
|
@ -49,7 +48,6 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
|||
import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent
|
||||
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
|
||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart
|
||||
import im.vector.matrix.rx.rx
|
||||
import im.vector.matrix.rx.unwrap
|
||||
import im.vector.riotx.R
|
||||
|
@ -184,8 +182,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action)
|
||||
is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages()
|
||||
is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages()
|
||||
is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action)
|
||||
is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action)
|
||||
is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action)
|
||||
is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action)
|
||||
is RoomDetailAction.RequestVerification -> handleRequestVerification(action)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -398,7 +397,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
popDraft()
|
||||
}
|
||||
is ParsedCommand.VerifyUser -> {
|
||||
session.getSasVerificationService().requestKeyVerificationInDMs(slashCommandResult.userId, room.roomId, null)
|
||||
session.getSasVerificationService().requestKeyVerificationInDMs(slashCommandResult.userId, room.roomId)
|
||||
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
|
||||
popDraft()
|
||||
}
|
||||
|
@ -796,18 +795,23 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
}
|
||||
|
||||
private fun handleAcceptVerification(action: RoomDetailAction.AcceptVerificationRequest) {
|
||||
session.getSasVerificationService().beginKeyVerificationInDMs(
|
||||
KeyVerificationStart.VERIF_METHOD_SAS,
|
||||
action.transactionId,
|
||||
room.roomId,
|
||||
action.otherUserId,
|
||||
action.otherdDeviceId,
|
||||
null
|
||||
)
|
||||
Timber.v("## SAS handleAcceptVerification ${action.otherUserId}, roomId:${room.roomId}, txId:${action.transactionId}")
|
||||
if (session.getSasVerificationService().readyPendingVerificationInDMs(action.otherUserId, room.roomId,
|
||||
action.transactionId)) {
|
||||
_requestLiveData.postValue(LiveEvent(Success(action)))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDeclineVerification(action: RoomDetailAction.DeclineVerificationRequest) {
|
||||
Timber.e("TODO implement $action")
|
||||
session.getSasVerificationService().declineVerificationRequestInDMs(
|
||||
action.otherUserId,
|
||||
action.otherdDeviceId,
|
||||
action.transactionId,
|
||||
room.roomId)
|
||||
}
|
||||
|
||||
private fun handleRequestVerification(action: RoomDetailAction.RequestVerification) {
|
||||
_requestLiveData.postValue(LiveEvent(Success(action)))
|
||||
}
|
||||
|
||||
private fun observeSyncState() {
|
||||
|
|
|
@ -46,6 +46,7 @@ class EncryptionItemFactory @Inject constructor(private val stringProvider: Stri
|
|||
eventId = event.root.eventId ?: "?",
|
||||
senderId = event.root.senderId ?: "",
|
||||
sendState = event.root.sendState,
|
||||
ageLocalTS = event.root.ageLocalTs,
|
||||
avatarUrl = event.senderAvatar,
|
||||
memberName = event.getDisambiguatedDisplayName(),
|
||||
showInformation = false,
|
||||
|
|
|
@ -151,18 +151,18 @@ class MessageItemFactory @Inject constructor(
|
|||
return VerificationRequestItem_()
|
||||
.attributes(
|
||||
VerificationRequestItem.Attributes(
|
||||
otherUserId,
|
||||
otherUserName.toString(),
|
||||
messageContent.fromDevice,
|
||||
informationData.eventId,
|
||||
informationData,
|
||||
attributes.avatarRenderer,
|
||||
attributes.colorProvider,
|
||||
attributes.itemLongClickListener,
|
||||
attributes.itemClickListener,
|
||||
attributes.reactionPillCallback,
|
||||
attributes.readReceiptsCallback,
|
||||
attributes.emojiTypeFace
|
||||
otherUserId = otherUserId,
|
||||
otherUserName = otherUserName.toString(),
|
||||
fromDevide = messageContent.fromDevice,
|
||||
referenceId = informationData.eventId,
|
||||
informationData = informationData,
|
||||
avatarRenderer = attributes.avatarRenderer,
|
||||
colorProvider = attributes.colorProvider,
|
||||
itemLongClickListener = attributes.itemLongClickListener,
|
||||
itemClickListener = attributes.itemClickListener,
|
||||
reactionPillCallback = attributes.reactionPillCallback,
|
||||
readReceiptsCallback = attributes.readReceiptsCallback,
|
||||
emojiTypeFace = attributes.emojiTypeFace
|
||||
)
|
||||
)
|
||||
.callback(callback)
|
||||
|
|
|
@ -70,6 +70,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
EventType.KEY_VERIFICATION_KEY,
|
||||
EventType.KEY_VERIFICATION_READY,
|
||||
EventType.KEY_VERIFICATION_MAC -> {
|
||||
// These events are filtered from timeline in normal case
|
||||
// Only visible in developer mode
|
||||
|
|
|
@ -52,6 +52,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
|
|||
EventType.KEY_VERIFICATION_MAC,
|
||||
EventType.KEY_VERIFICATION_DONE,
|
||||
EventType.KEY_VERIFICATION_KEY,
|
||||
EventType.KEY_VERIFICATION_READY,
|
||||
EventType.REDACTION -> formatDebug(timelineEvent.root)
|
||||
else -> {
|
||||
Timber.v("Type $type not handled by this formatter")
|
||||
|
|
|
@ -73,6 +73,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
|||
senderId = event.root.senderId ?: "",
|
||||
sendState = event.root.sendState,
|
||||
time = time,
|
||||
ageLocalTS = event.root.ageLocalTs,
|
||||
avatarUrl = avatarUrl,
|
||||
memberName = formattedMemberName,
|
||||
showInformation = showInformation,
|
||||
|
|
|
@ -50,7 +50,8 @@ object TimelineDisplayableEvents {
|
|||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
EventType.KEY_VERIFICATION_MAC,
|
||||
EventType.KEY_VERIFICATION_KEY
|
||||
EventType.KEY_VERIFICATION_KEY,
|
||||
EventType.KEY_VERIFICATION_READY
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ data class MessageInformationData(
|
|||
val senderId: String,
|
||||
val sendState: SendState,
|
||||
val time: CharSequence? = null,
|
||||
val ageLocalTS : Long?,
|
||||
val avatarUrl: String?,
|
||||
val memberName: CharSequence? = null,
|
||||
val showInformation: Boolean = true,
|
||||
|
|
|
@ -28,6 +28,7 @@ import androidx.core.view.isVisible
|
|||
import androidx.core.view.updateLayoutParams
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
|
||||
import im.vector.matrix.android.internal.session.room.VerificationState
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.resources.ColorProvider
|
||||
|
@ -108,6 +109,11 @@ abstract class VerificationRequestItem : AbsBaseMessageItem<VerificationRequestI
|
|||
}
|
||||
}
|
||||
|
||||
// Always hide buttons if request is too old
|
||||
if (!SasVerificationService.isValidRequest(attributes.informationData.ageLocalTS)) {
|
||||
holder.buttonBar.isVisible = false
|
||||
}
|
||||
|
||||
holder.callback = callback
|
||||
holder.attributes = attributes
|
||||
|
||||
|
@ -133,7 +139,7 @@ abstract class VerificationRequestItem : AbsBaseMessageItem<VerificationRequestI
|
|||
att.otherUserId,
|
||||
att.fromDevide))
|
||||
} else if (it == declineButton) {
|
||||
callback?.onTimelineItemAction(RoomDetailAction.DeclineVerificationRequest(att.referenceId))
|
||||
callback?.onTimelineItemAction(RoomDetailAction.DeclineVerificationRequest(att.referenceId, att.otherUserId, att.fromDevide))
|
||||
}
|
||||
})
|
||||
|
||||
|
|
55
vector/src/main/res/layout/bottom_sheet_verification.xml
Normal file
55
vector/src/main/res/layout/bottom_sheet_verification.xml
Normal file
|
@ -0,0 +1,55 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/bottomSheetScrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:animateLayoutChanges="true"
|
||||
android:fadeScrollbars="false"
|
||||
android:scrollbars="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingTop="16dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/verificationRequestAvatar"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:background="@drawable/circle"
|
||||
android:contentDescription="@string/avatar"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/verificationRequestName"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/verification_request_alert_title"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/bottomSheetFragmentContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
|
@ -0,0 +1,188 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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="wrap_content"
|
||||
android:background="?android:colorBackground">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sas_emoji_description"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginTop="@dimen/layout_vertical_margin"
|
||||
android:text="@string/verify_by_emoji_title"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sas_emoji_description_2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/layout_vertical_margin"
|
||||
android:text="@string/verify_user_sas_emoji_help_text"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
android:textSize="15sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/sas_emoji_description" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sas_decimal_code"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="28sp"
|
||||
android:textStyle="bold"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/sas_emoji_grid"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/sas_emoji_grid"
|
||||
tools:text="1234-4320-3905"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/sasLoadingProgress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/sas_emoji_grid"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/sas_emoji_grid" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/sas_emoji_grid"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/layout_vertical_margin"
|
||||
android:visibility="invisible"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/sas_emoji_description_2"
|
||||
tools:visibility="visible">
|
||||
|
||||
<include
|
||||
android:id="@+id/emoji0"
|
||||
layout="@layout/item_emoji_verif" />
|
||||
|
||||
<include
|
||||
android:id="@+id/emoji1"
|
||||
layout="@layout/item_emoji_verif" />
|
||||
|
||||
<include
|
||||
android:id="@+id/emoji2"
|
||||
layout="@layout/item_emoji_verif" />
|
||||
|
||||
<include
|
||||
android:id="@+id/emoji3"
|
||||
layout="@layout/item_emoji_verif" />
|
||||
|
||||
<include
|
||||
android:id="@+id/emoji4"
|
||||
layout="@layout/item_emoji_verif" />
|
||||
|
||||
<include
|
||||
android:id="@+id/emoji5"
|
||||
layout="@layout/item_emoji_verif" />
|
||||
|
||||
<include
|
||||
android:id="@+id/emoji6"
|
||||
layout="@layout/item_emoji_verif" />
|
||||
|
||||
<androidx.constraintlayout.helper.widget.Flow
|
||||
android:id="@+id/sas_emoji_grid_flow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:constraint_referenced_ids="emoji0,emoji1,emoji2,emoji3,emoji4,emoji5,emoji6"
|
||||
app:flow_horizontalBias="0.5"
|
||||
app:flow_horizontalGap="16dp"
|
||||
app:flow_horizontalStyle="packed"
|
||||
app:flow_verticalBias="0"
|
||||
app:flow_verticalGap="8dp"
|
||||
app:flow_wrapMode="chain"
|
||||
tools:ignore="MissingConstraints" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/sas_request_continue_button"
|
||||
style="@style/VectorButtonStylePositive"
|
||||
android:layout_width="0dp"
|
||||
android:layout_margin="@dimen/layout_vertical_margin"
|
||||
android:minWidth="160dp"
|
||||
android:text="@string/verification_sas_match"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/centerGuideLine"
|
||||
app:layout_constraintTop_toBottomOf="@+id/sas_emoji_grid"
|
||||
tools:text="A very long translation thats too big" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/centerGuideLine"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_percent="0.5" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/sas_request_cancel_button"
|
||||
style="@style/VectorButtonStyleDestructive"
|
||||
android:layout_width="0dp"
|
||||
android:layout_margin="@dimen/layout_vertical_margin"
|
||||
android:text="@string/verification_sas_do_not_match"
|
||||
app:layout_constraintEnd_toStartOf="@+id/centerGuideLine"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/sas_emoji_grid" />
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/bottomBarrier"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
app:barrierMargin="8dp"
|
||||
app:constraint_referenced_ids="sas_request_cancel_button,sas_request_continue_button" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sasCodeWaitingPartnerText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/sas_waiting_for_partner"
|
||||
android:textColor="?attr/vctr_notice_secondary"
|
||||
android:textSize="15sp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toTopOf="@id/bottomBarrier"
|
||||
app:layout_constraintTop_toBottomOf="@id/sas_emoji_grid"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sasEmojiSecurityTip"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/layout_vertical_margin"
|
||||
android:gravity="center"
|
||||
android:text="@string/verify_user_sas_emoji_security_tip"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/bottomBarrier" />
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/ButtonsVisibilityGroup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:constraint_referenced_ids="sas_request_continue_button,sas_request_cancel_button"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,117 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
|
||||
<!-- <ImageView-->
|
||||
<!-- android:id="@+id/verificationRequestAvatar"-->
|
||||
<!-- android:layout_width="32dp"-->
|
||||
<!-- android:layout_height="32dp"-->
|
||||
<!-- android:adjustViewBounds="true"-->
|
||||
<!-- android:background="@drawable/circle"-->
|
||||
<!-- android:contentDescription="@string/avatar"-->
|
||||
<!-- android:scaleType="centerCrop"-->
|
||||
<!-- android:transitionName="bottomSheetAvatar"-->
|
||||
<!-- app:layout_constraintStart_toStartOf="parent"-->
|
||||
<!-- app:layout_constraintTop_toTopOf="parent"-->
|
||||
<!-- app:layout_constraintVertical_bias="0"-->
|
||||
<!-- tools:src="@tools:sample/avatars" />-->
|
||||
|
||||
<!-- <TextView-->
|
||||
<!-- android:id="@+id/verificationRequestName"-->
|
||||
<!-- android:layout_width="0dp"-->
|
||||
<!-- android:layout_height="wrap_content"-->
|
||||
<!-- android:layout_marginStart="16dp"-->
|
||||
<!-- android:text="@string/verification_request_alert_title"-->
|
||||
<!-- android:textColor="?riotx_text_primary"-->
|
||||
<!-- android:textSize="20sp"-->
|
||||
<!-- android:textStyle="bold"-->
|
||||
<!-- android:transitionName="bottomSheetDisplayName"-->
|
||||
<!-- app:layout_constraintBottom_toBottomOf="@id/verificationRequestAvatar"-->
|
||||
<!-- app:layout_constraintEnd_toEndOf="parent"-->
|
||||
<!-- app:layout_constraintStart_toEndOf="@id/verificationRequestAvatar"-->
|
||||
<!-- app:layout_constraintTop_toTopOf="@id/verificationRequestAvatar" />-->
|
||||
|
||||
<TextView
|
||||
android:id="@+id/verificationQRTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/verify_by_scanning_title"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/verifyQRDescription"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/verify_by_scanning_description"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
app:layout_constraintTop_toBottomOf="@id/verificationQRTitle"
|
||||
tools:text="@string/verify_by_scanning_description" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/verifyQRImageView"
|
||||
android:layout_width="180dp"
|
||||
android:layout_height="180dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="?riotx_header_panel_background"
|
||||
android:contentDescription="@string/aria_qr_code_description"
|
||||
android:visibility="visible"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/verifyQRDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/verificationEmojiTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="40dp"
|
||||
android:text="@string/verify_by_emoji_title"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintTop_toBottomOf="@id/verifyQRImageView"
|
||||
app:layout_goneMarginTop="0dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/verifyEmojiDescription"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/verify_by_emoji_description"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
app:layout_constraintTop_toBottomOf="@id/verificationEmojiTitle" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/verificationByEmojiButton"
|
||||
style="@style/VectorButtonStylePositive"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/verify_by_emoji_title"
|
||||
app:layout_constraintTop_toBottomOf="@id/verifyEmojiDescription" />
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/verifyQRGroup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
app:constraint_referenced_ids="verifyQRDescription,verificationQRTitle,verifyQRImageView" />
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/verifyEmojiGroup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="visible"
|
||||
app:constraint_referenced_ids="verifyEmojiDescription,verificationEmojiTitle,verificationByEmojiButton" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,63 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/verificationConclusionTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/sas_verified"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="@string/sas_verified" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/verifyConclusionDescription"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/sas_verified_successful_description"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
app:layout_constraintTop_toBottomOf="@id/verificationConclusionTitle"
|
||||
tools:text="@string/sas_verified_successful_description" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/verifyConclusionImageView"
|
||||
android:layout_width="180dp"
|
||||
android:layout_height="180dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="16dp"
|
||||
android:visibility="visible"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/verifyConclusionDescription"
|
||||
tools:background="@drawable/ic_shield_trusted" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/verifyConclusionBottomDescription"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
app:layout_constraintTop_toBottomOf="@id/verifyConclusionImageView"
|
||||
tools:text="@string/verification_green_shield" />
|
||||
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/verificationConclusionButton"
|
||||
style="@style/VectorButtonStylePositive"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/sas_got_it"
|
||||
app:layout_constraintTop_toBottomOf="@id/verifyConclusionBottomDescription" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
43
vector/src/main/res/layout/fragment_verification_request.xml
Normal file
43
vector/src/main/res/layout/fragment_verification_request.xml
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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="wrap_content"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/verificationRequestText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="@string/verification_request_alert_description" />
|
||||
|
||||
<!-- app:layout_constraintTop_toBottomOf="@id/verificationRequestAvatar"-->
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/verificationStartButton"
|
||||
style="@style/VectorButtonStylePositive"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/start_verification"
|
||||
app:layout_constraintTop_toBottomOf="@id/verificationRequestText" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/verificationWaitingText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:textColor="?vctr_notice_secondary"
|
||||
android:textSize="17sp"
|
||||
android:textStyle="bold"
|
||||
android:visibility="invisible"
|
||||
tools:visibility="visible"
|
||||
app:layout_constraintBottom_toBottomOf="@id/verificationStartButton"
|
||||
app:layout_constraintTop_toTopOf="@id/verificationStartButton"
|
||||
tools:text="@string/verification_request_waiting_for" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1500,7 +1500,7 @@ Why choose Riot.im?
|
|||
|
||||
<string name="sas_verified">Verified!</string>
|
||||
<string name="sas_verified_successful">You\'ve successfully verified this device.</string>
|
||||
<string name="sas_verified_successful_description">Secure messages with this user are end-to-end encrypted and not able to be read by third parties.</string>
|
||||
<string name="sas_verified_successful_description">Messages with this user in this room are end-to-end encrypted and can‘t be read by third parties.</string>
|
||||
<string name="sas_got_it">Got it</string>
|
||||
|
||||
<string name="sas_verifying_keys">Nothing appearing? Not all clients supports interactive verification yet. Use legacy verification.</string>
|
||||
|
|
|
@ -5,15 +5,25 @@
|
|||
<string name="command_description_verify">Request to verify the given userID</string>
|
||||
<string name="command_description_shrug">Prepends ¯\\_(ツ)_/¯ to a plain-text message</string>
|
||||
|
||||
|
||||
<string name="notification_initial_sync">Initial Sync…</string>
|
||||
|
||||
<string name="sent_a_file">File</string>
|
||||
<string name="sent_an_audio_file">Audio</string>
|
||||
<string name="sent_an_image">Image.</string>
|
||||
<string name="sent_a_video">Video.</string>
|
||||
|
||||
|
||||
<string name="verification_conclusion_warning">Untrusted sign in</string>
|
||||
<string name="verification_sas_match">They match</string>
|
||||
<string name="verification_sas_do_not_match">They don\'t match</string>
|
||||
<string name="verify_user_sas_emoji_help_text">Verify this user by confirming the following unique emoji appear on their screen, in the same order."</string>
|
||||
<string name="verify_user_sas_emoji_security_tip">For ultimate security, use another trusted means of communication or do this in person.</string>
|
||||
<string name="verification_green_shield">Look for the green shield to ensure a user is trusted. Trust all users in a room to ensure the room is secure.</string>
|
||||
|
||||
<string name="verification_conclusion_not_secure">Not secure</string>
|
||||
<string name="verification_conclusion_compromised">One of the following may be compromised:\n\n - Your homeserver\n - The homeserver the user you’re verifying is connected to\n - Yours, or the other users’ internet connection\n - Yours, or the other users’ device
|
||||
</string>
|
||||
|
||||
<string name="sent_a_video">Video.</string>
|
||||
<string name="sent_an_image">Image.</string>
|
||||
<string name="sent_an_audio_file">Audio</string>
|
||||
<string name="sent_a_file">File</string>
|
||||
|
||||
<string name="verification_request_waiting">Waiting…</string>
|
||||
<string name="verification_request_other_cancelled">%s cancelled</string>
|
||||
<string name="verification_request_you_cancelled">You cancelled</string>
|
||||
|
@ -21,4 +31,22 @@
|
|||
<string name="verification_request_you_accepted">You accepted</string>
|
||||
<string name="verification_sent">Verification Sent</string>
|
||||
<string name="verification_request">Verification Request</string>
|
||||
|
||||
<!-- Sender name of a message when it is send by you, e.g. You: Hello!-->
|
||||
<string name="you">You</string>
|
||||
|
||||
<string name="verify_by_scanning_title">Verify by scanning</string>
|
||||
<!-- the %s will be replaced by verify_open_camera_link that will be clickable -->
|
||||
<string name="verify_by_scanning_description">Ask the other user to scan this code, or %s to scan theirs</string>
|
||||
<!-- This part is inserted in verify_by_scanning_description-->
|
||||
<string name="verify_open_camera_link">open your camera</string>
|
||||
|
||||
<string name="verify_by_emoji_title">Verify by Emoji</string>
|
||||
<string name="verify_by_emoji_description">If you can’t scan the code above, verify by comparing a short, unique selection of emoji.</string>
|
||||
|
||||
<string name="aria_qr_code_description">QR code image</string>
|
||||
|
||||
<string name="verification_request_alert_title">Verify %s</string>
|
||||
<string name="verification_request_waiting_for">Waiting for %s…</string>
|
||||
<string name="verification_request_alert_description">For extra security, verify %s by checking a one-time code on both your devices.\n\nFor maximum security, do this in person.</string>
|
||||
</resources>
|
||||
|
|
Loading…
Reference in a new issue