This commit is contained in:
Valere 2019-12-30 13:44:13 +01:00
parent 4edd5e3530
commit a73cd61b96
32 changed files with 1622 additions and 322 deletions

View file

@ -42,6 +42,8 @@ interface SasVerificationService {
fun getExistingVerificationRequest(otherUser: String): List<PendingVerificationRequest>? fun getExistingVerificationRequest(otherUser: String): List<PendingVerificationRequest>?
fun getExistingVerificationRequest(otherUser: String, tid: String?): PendingVerificationRequest?
/** /**
* Shortcut for KeyVerificationStart.VERIF_METHOD_SAS * Shortcut for KeyVerificationStart.VERIF_METHOD_SAS
* @see beginKeyVerification * @see beginKeyVerification

View file

@ -0,0 +1,163 @@
/*
* 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.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.SessionDatabase
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.task.Task
import io.realm.RealmConfiguration
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 {
private val transactionsHandledByOtherDevice = ArrayList<String>()
}
override suspend fun execute(params: RoomVerificationUpdateTask.Params): Unit {
// 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
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.
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_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)
}
}
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)
}
}
}
}
}
}

View file

@ -223,7 +223,7 @@ internal class DefaultSasVerificationService @Inject constructor(
} }
} }
fun onRoomRequestReceived(event: Event) { suspend fun onRoomRequestReceived(event: Event) {
Timber.v("## SAS Verification request from ${event.senderId} in room ${event.roomId}") Timber.v("## SAS Verification request from ${event.senderId} in room ${event.roomId}")
val requestInfo = event.getClearContent().toModel<MessageVerificationRequestContent>() val requestInfo = event.getClearContent().toModel<MessageVerificationRequestContent>()
?: return ?: return
@ -234,6 +234,14 @@ internal class DefaultSasVerificationService @Inject constructor(
Timber.w("## SAS Verification ignoring request from ${event.senderId}, not sent to me") Timber.w("## SAS Verification ignoring request from ${event.senderId}, not sent to me")
return return
} }
if(checkKeysAreDownloaded(senderId, requestInfo.fromDevice) == null) {
//I should ignore this, it's not for me
Timber.e("## SAS Verification device ${requestInfo.fromDevice} is not knwon")
// TODO cancel?
return
}
// Remember this request // Remember this request
val requestsForUser = pendingRequests[senderId] val requestsForUser = pendingRequests[senderId]
?: ArrayList<PendingVerificationRequest>().also { ?: ArrayList<PendingVerificationRequest>().also {
@ -329,7 +337,7 @@ internal class DefaultSasVerificationService @Inject constructor(
private suspend fun handleStart(otherUserId: String?, startReq: VerificationInfoStart, txConfigure: (SASVerificationTransaction) -> Unit): CancelCode? { private suspend fun handleStart(otherUserId: String?, startReq: VerificationInfoStart, txConfigure: (SASVerificationTransaction) -> Unit): CancelCode? {
Timber.d("## SAS onStartRequestReceived ${startReq.transactionID!!}") Timber.d("## SAS onStartRequestReceived ${startReq.transactionID!!}")
if (checkKeysAreDownloaded(otherUserId!!, startReq) != null) { if (checkKeysAreDownloaded(otherUserId!!, startReq.fromDevice ?: "") != null) {
Timber.v("## SAS onStartRequestReceived $startReq") Timber.v("## SAS onStartRequestReceived $startReq")
val tid = startReq.transactionID!! val tid = startReq.transactionID!!
val existing = getExistingTransaction(otherUserId, tid) val existing = getExistingTransaction(otherUserId, tid)
@ -382,11 +390,11 @@ internal class DefaultSasVerificationService @Inject constructor(
} }
private suspend fun checkKeysAreDownloaded(otherUserId: String, private suspend fun checkKeysAreDownloaded(otherUserId: String,
startReq: VerificationInfoStart): MXUsersDevicesMap<MXDeviceInfo>? { fromDevice: String): MXUsersDevicesMap<MXDeviceInfo>? {
return try { return try {
val keys = deviceListManager.downloadKeys(listOf(otherUserId), true) val keys = deviceListManager.downloadKeys(listOf(otherUserId), true)
val deviceIds = keys.getUserDeviceIds(otherUserId) ?: return null val deviceIds = keys.getUserDeviceIds(otherUserId) ?: return null
keys.takeIf { deviceIds.contains(startReq.fromDevice) } keys.takeIf { deviceIds.contains(fromDevice) }
} catch (e: Exception) { } catch (e: Exception) {
null null
} }
@ -404,6 +412,10 @@ internal class DefaultSasVerificationService @Inject constructor(
// TODO should we cancel? // TODO should we cancel?
return return
} }
getExistingVerificationRequest(event.senderId ?: "", cancelReq.transactionID)?.let {
updateOutgoingPendingRequest(it.copy(cancelConclusion = safeValueOf(cancelReq.code)))
// Should we remove it from the list?
}
handleOnCancel(event.senderId!!, cancelReq) handleOnCancel(event.senderId!!, cancelReq)
} }
@ -527,7 +539,7 @@ internal class DefaultSasVerificationService @Inject constructor(
handleMacReceived(event.senderId, macReq) handleMacReceived(event.senderId, macReq)
} }
private fun onRoomReadyReceived(event: Event) { private suspend fun onRoomReadyReceived(event: Event) {
val readyReq = event.getClearContent().toModel<MessageVerificationReadyContent>() val readyReq = event.getClearContent().toModel<MessageVerificationReadyContent>()
?.copy( ?.copy(
// relates_to is in clear in encrypted payload // relates_to is in clear in encrypted payload
@ -539,6 +551,13 @@ internal class DefaultSasVerificationService @Inject constructor(
// TODO should we cancel? // TODO should we cancel?
return 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) handleReadyReceived(event.senderId, readyReq)
} }
@ -588,6 +607,12 @@ internal class DefaultSasVerificationService @Inject constructor(
} }
} }
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>? { private fun getExistingTransactionsForUser(otherUser: String): Collection<VerificationTransaction>? {
synchronized(txMap) { synchronized(txMap) {
return txMap[otherUser]?.values return txMap[otherUser]?.values
@ -637,7 +662,8 @@ internal class DefaultSasVerificationService @Inject constructor(
override fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback<String>?) override fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback<String>?)
: PendingVerificationRequest { : PendingVerificationRequest {
Timber.i("Requesting verification to user: $userId in room ${roomId}")
Timber.i("## SAS Requesting verification to user: $userId in room ${roomId}")
val requestsForUser = pendingRequests[userId] val requestsForUser = pendingRequests[userId]
?: ArrayList<PendingVerificationRequest>().also { ?: ArrayList<PendingVerificationRequest>().also {
pendingRequests[userId] = it pendingRequests[userId] = it

View file

@ -15,6 +15,7 @@
*/ */
package im.vector.matrix.android.internal.crypto.verification 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 im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
import java.util.* import java.util.*
@ -27,7 +28,10 @@ data class PendingVerificationRequest(
val otherUserId: String, val otherUserId: String,
val transactionId: String? = null, val transactionId: String? = null,
val requestInfo: MessageVerificationRequestContent? = null, val requestInfo: MessageVerificationRequestContent? = null,
val readyInfo: VerificationInfoReady? = null val readyInfo: VerificationInfoReady? = null,
val cancelConclusion: CancelCode? = null,
val isSuccessful : Boolean = false
) { ) {
val isReady: Boolean = readyInfo != null val isReady: Boolean = readyInfo != null

View file

@ -227,13 +227,14 @@ internal abstract class SASVerificationTransaction(
val keyIDNoPrefix = if (it.startsWith("ed25519:")) it.substring("ed25519:".length) else it val keyIDNoPrefix = if (it.startsWith("ed25519:")) it.substring("ed25519:".length) else it
val otherDeviceKey = otherUserKnownDevices?.get(keyIDNoPrefix)?.fingerprint() val otherDeviceKey = otherUserKnownDevices?.get(keyIDNoPrefix)?.fingerprint()
if (otherDeviceKey == null) { 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 // just ignore and continue
return@forEach return@forEach
} }
val mac = macUsingAgreedMethod(otherDeviceKey, baseInfo + it) val mac = macUsingAgreedMethod(otherDeviceKey, baseInfo + it)
if (mac != theirMac?.mac?.get(it)) { if (mac != theirMac?.mac?.get(it)) {
// WRONG! // WRONG!
Timber.e("## SAS Verification: mac mismatch for $otherDeviceKey with id $keyIDNoPrefix")
cancel(CancelCode.MismatchedKeys) cancel(CancelCode.MismatchedKeys)
return return
} }

View file

@ -17,38 +17,31 @@ package im.vector.matrix.android.internal.crypto.verification
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.crypto.CryptoService 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.EventType
import im.vector.matrix.android.api.session.events.model.LocalEcho 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.internal.crypto.tasks.DefaultRoomVerificationUpdateTask
import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.internal.crypto.tasks.RoomVerificationUpdateTask
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
import im.vector.matrix.android.internal.database.mapper.asDomain 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.model.EventEntity
import im.vector.matrix.android.internal.database.query.types 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.SessionDatabase
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import io.realm.OrderedCollectionChangeSet import io.realm.OrderedCollectionChangeSet
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import io.realm.RealmResults import io.realm.RealmResults
import timber.log.Timber
import java.util.*
import javax.inject.Inject import javax.inject.Inject
import kotlin.collections.ArrayList
internal class VerificationMessageLiveObserver @Inject constructor( internal class VerificationMessageLiveObserver @Inject constructor(
@SessionDatabase realmConfiguration: RealmConfiguration, @SessionDatabase realmConfiguration: RealmConfiguration,
@UserId private val userId: String, private val roomVerificationUpdateTask: DefaultRoomVerificationUpdateTask,
@DeviceId private val deviceId: String?,
private val cryptoService: CryptoService, private val cryptoService: CryptoService,
private val sasVerificationService: DefaultSasVerificationService, private val sasVerificationService: DefaultSasVerificationService,
private val taskExecutor: TaskExecutor private val taskExecutor: TaskExecutor
) : RealmLiveEntityObserver<EventEntity>(realmConfiguration) { ) : RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
override val query = Monarchy.Query<EventEntity> { override val query = Monarchy.Query {
EventEntity.types(it, listOf( EventEntity.types(it, listOf(
EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_ACCEPT,
@ -62,11 +55,8 @@ internal class VerificationMessageLiveObserver @Inject constructor(
) )
} }
val transactionsHandledByOtherDevice = ArrayList<String>()
override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) { override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
// TODO do that in a task // Should we ignore when it's an initial sync?
// TODO how to ignore when it's an initial sync?
val events = changeSet.insertions val events = changeSet.insertions
.asSequence() .asSequence()
.mapNotNull { results[it]?.asDomain() } .mapNotNull { results[it]?.asDomain() }
@ -76,111 +66,9 @@ internal class VerificationMessageLiveObserver @Inject constructor(
} }
.toList() .toList()
// TODO ignore initial sync or back pagination? roomVerificationUpdateTask.configureWith(
RoomVerificationUpdateTask.Params(events, sasVerificationService, cryptoService)
).executeBy(taskExecutor)
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_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)
}
}
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 -> {
sasVerificationService.onRoomEvent(event)
}
EventType.MESSAGE -> {
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.type) {
sasVerificationService.onRoomRequestReceived(event)
}
}
}
}
} }
} }

View file

@ -273,11 +273,23 @@ interface FragmentModule {
@Binds @Binds
@IntoMap @IntoMap
@FragmentKey(OutgoingVerificationRequestFragment::class) @FragmentKey(VerificationRequestFragment::class)
fun bindVerificationRequestFragment(fragment: OutgoingVerificationRequestFragment): Fragment fun bindVerificationRequestFragment(fragment: VerificationRequestFragment): Fragment
@Binds @Binds
@IntoMap @IntoMap
@FragmentKey(VerificationChooseMethodFragment::class) @FragmentKey(VerificationChooseMethodFragment::class)
fun bindVerificationMethodChooserFragment(fragment: VerificationChooseMethodFragment): Fragment 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
} }

View file

@ -16,9 +16,12 @@
package im.vector.riotx.core.utils package im.vector.riotx.core.utils
import android.text.Spannable import android.text.Spannable
import android.text.style.BulletSpan
import android.text.style.ClickableSpan
import android.text.style.ForegroundColorSpan import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan import android.text.style.StyleSpan
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import me.gujun.android.span.Span
fun Spannable.styleMatchingText(match: String, typeFace: Int): Spannable { fun Spannable.styleMatchingText(match: String, typeFace: Int): Spannable {
if (match.isEmpty()) return this if (match.isEmpty()) return this
@ -35,3 +38,21 @@ fun Spannable.colorizeMatchingText(match: String, @ColorInt color: Int): Spannab
} }
return this 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()
})
}

View file

@ -1,73 +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.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.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.platform.VectorViewModelAction
data class VerificationRequestViewState(
val otherUserId: String = "",
val matrixItem: MatrixItem? = null,
val started: Async<Boolean> = Success(false)
) : MvRxState
sealed class VerificationAction : VectorViewModelAction {
data class RequestVerificationByDM(val userID: String) : VerificationAction()
}
class OutgoingVerificationRequestViewModel @AssistedInject constructor(
@Assisted initialState: VerificationRequestViewState,
private val session: Session
) : VectorViewModel<VerificationRequestViewState, VerificationAction>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: VerificationRequestViewState): OutgoingVerificationRequestViewModel
}
init {
withState {
val user = session.getUser(it.otherUserId)
setState {
copy(matrixItem = user?.toMatrixItem())
}
}
}
companion object : MvRxViewModelFactory<OutgoingVerificationRequestViewModel, VerificationRequestViewState> {
override fun create(viewModelContext: ViewModelContext, state: VerificationRequestViewState): OutgoingVerificationRequestViewModel? {
val fragment: OutgoingVerificationRequestFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.outgoingVerificationRequestViewModelFactory.create(state)
}
override fun initialState(viewModelContext: ViewModelContext): VerificationRequestViewState? {
val userID: String = viewModelContext.args<String>()
return VerificationRequestViewState(otherUserId = userID)
}
}
override fun handle(action: VerificationAction) {
}
}

View file

@ -0,0 +1,164 @@
/*
* 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 im.vector.riotx.core.platform.parentFragmentViewModel
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 ->
//UX echo
ButtonsVisibilityGroup.isInvisible = true
sasCodeWaitingPartnerText.isVisible = true
sharedViewModel.handle(VerificationAction.SASMatchAction(state.otherUserId, state.transactionId))
}
@OnClick(R.id.sas_request_cancel_button)
fun onDoNotMatchButtonTapped() = withState(viewModel) { state ->
//UX echo
ButtonsVisibilityGroup.isInvisible = true
sasCodeWaitingPartnerText.isVisible = true
sharedViewModel.handle(VerificationAction.SASDoNotMatchAction(state.otherUserId, state.transactionId))
}
}

View file

@ -0,0 +1,170 @@
/*
* 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.platform.EmptyAction
import im.vector.riotx.core.platform.VectorViewModel
data class SASVerificationCodeViewState(
val transactionId: String,
val otherUserId: 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 ->
val matrixItem = session.getUser(state.otherUserId)?.toMatrixItem()
setState {
copy(otherUser = matrixItem)
}
val sasTx = session.getSasVerificationService()
.getExistingTransaction(state.otherUserId, state.transactionId)
if (sasTx == null) {
setState {
copy(
isWaitingFromOther = false,
emojiDescription = Fail(Throwable("Unknown Transaction")),
decimalDescription = Fail(Throwable("Unknown Transaction"))
)
}
} else {
refreshStateFromTx(sasTx)
}
}
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"))
)
}
}
}
}
override fun transactionCreated(tx: SasVerificationTransaction) {
transactionUpdated(tx)
}
override fun transactionUpdated(tx: SasVerificationTransaction) {
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>()
return SASVerificationCodeViewState(
transactionId = args.verificationId ?: "",
otherUserId = args.otherUserId
)
}
}
override fun handle(action: EmptyAction) {
}
}

View file

@ -91,7 +91,7 @@ class SASVerificationStartFragment @Inject constructor(): VectorBaseFragment() {
(requireActivity() as VectorBaseActivity).notImplemented() (requireActivity() as VectorBaseActivity).notImplemented()
/* /*
viewModel.session.crypto?.getDeviceInfo(viewModel.otherUserId ?: "", viewModel.otherDeviceId viewModel.session.crypto?.getDeviceInfo(viewModel.otherUserMxItem ?: "", viewModel.otherDeviceId
?: "", object : SimpleApiCallback<MXDeviceInfo>() { ?: "", object : SimpleApiCallback<MXDeviceInfo>() {
override fun onSuccess(info: MXDeviceInfo?) { override fun onSuccess(info: MXDeviceInfo?) {
info?.let { info?.let {

View file

@ -24,6 +24,7 @@ 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.SasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest
import im.vector.riotx.core.utils.LiveEvent import im.vector.riotx.core.utils.LiveEvent
import javax.inject.Inject import javax.inject.Inject

View file

@ -1,6 +1,22 @@
/*
* 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 package im.vector.riotx.features.crypto.verification
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -9,28 +25,43 @@ import android.widget.TextView
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.text.toSpannable import androidx.core.text.toSpannable
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.transition.AutoTransition
import androidx.transition.TransitionManager
import butterknife.BindView import butterknife.BindView
import butterknife.ButterKnife import butterknife.ButterKnife
import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.commitTransaction import im.vector.riotx.core.extensions.commitTransactionNow
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.riotx.core.utils.colorizeMatchingText import im.vector.riotx.core.utils.colorizeMatchingText
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.themes.ThemeUtils import im.vector.riotx.features.themes.ThemeUtils
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.bottom_sheet_verification.*
import javax.inject.Inject import javax.inject.Inject
import kotlin.reflect.KClass
class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
@Inject lateinit var outgoingVerificationRequestViewModelFactory: OutgoingVerificationRequestViewModel.Factory @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 verificationViewModelFactory: VerificationBottomSheetViewModel.Factory
@Inject lateinit var avatarRenderer: AvatarRenderer @Inject lateinit var avatarRenderer: AvatarRenderer
private val viewModel by fragmentViewModel(VerificationBottomSheetViewModel::class) private val viewModel by fragmentViewModel(VerificationBottomSheetViewModel::class)
override fun injectWith(injector: ScreenComponent) { override fun injectWith(injector: ScreenComponent) {
@ -49,24 +80,25 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
return view return view
} }
override fun invalidate() = withState(viewModel) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
when (it.verificationRequestEvent) { super.onViewCreated(view, savedInstanceState)
is Uninitialized -> {
if (childFragmentManager.findFragmentByTag("REQUEST") == null) { viewModel.requestLiveData.observe(this, Observer {
//Verification not yet started, put outgoing verification it.peekContent().let { va ->
childFragmentManager.commitTransaction { when (va) {
setCustomAnimations(R.anim.fade_in, R.anim.fade_out) is Success -> {
replace(R.id.bottomSheetFragmentContainer, if (va.invoke() is VerificationAction.GotItConclusion) {
OutgoingVerificationRequestFragment::class.java, dismiss()
Bundle().apply { putString(MvRx.KEY_ARG, it.userId) },
"REQUEST"
)
} }
} }
} }
} }
})
}
it.otherUserId?.let { matrixItem -> override fun invalidate() = withState(viewModel) {
it.otherUserMxItem?.let { matrixItem ->
val displayName = matrixItem.displayName ?: "" val displayName = matrixItem.displayName ?: ""
otherUserNameText.text = getString(R.string.verification_request_alert_title, displayName) otherUserNameText.text = getString(R.string.verification_request_alert_title, displayName)
.toSpannable() .toSpannable()
@ -75,13 +107,121 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
avatarRenderer.render(matrixItem, otherUserAvatarImageView) 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 -> {
val fragmentTag = SASVerificationCodeFragment::class.simpleName
if (childFragmentManager.findFragmentByTag(fragmentTag) == null) {
// Commit now, to ensure changes occurs before next rendering frame (or bottomsheet want animate)
bottomSheetFragmentContainer.getParentCoordinatorLayout()?.let { coordinatorLayout ->
TransitionManager.beginDelayedTransition(coordinatorLayout, AutoTransition().apply { duration = 150 })
}
childFragmentManager.commitTransactionNow {
replace(R.id.bottomSheetFragmentContainer,
SASVerificationCodeFragment::class.java,
Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationArgs(
it.otherUserMxItem?.id ?: "",
it.pendingRequest?.transactionId))
},
SASVerificationCodeFragment::class.simpleName
)
}
}
}
SasVerificationTxState.Verified,
SasVerificationTxState.Cancelled,
SasVerificationTxState.OnCancelled -> {
val fragmentTag = VerificationConclusionFragment::class.simpleName
if (childFragmentManager.findFragmentByTag(fragmentTag) == null) {
bottomSheetFragmentContainer.getParentCoordinatorLayout()?.let { coordinatorLayout ->
TransitionManager.beginDelayedTransition(coordinatorLayout, AutoTransition().apply { duration = 150 })
}
childFragmentManager.commitTransactionNow {
replace(R.id.bottomSheetFragmentContainer,
VerificationConclusionFragment::class.java,
Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(
it.sasTransactionState == SasVerificationTxState.Verified,
it.cancelCode?.value))
},
fragmentTag
)
}
}
}
}
return@withState
}
// Transaction has not yet started
if (it.pendingRequest == null || !it.pendingRequest.isReady) {
if (childFragmentManager.findFragmentByTag("REQUEST") == null) {
bottomSheetFragmentContainer.getParentCoordinatorLayout()?.let { coordinatorLayout ->
TransitionManager.beginDelayedTransition(coordinatorLayout, AutoTransition().apply { duration = 150 })
}
//Verification not yet started, put outgoing verification
childFragmentManager.commitTransactionNow {
replace(R.id.bottomSheetFragmentContainer,
VerificationRequestFragment::class.java,
Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationArgs(it.otherUserMxItem?.id ?: ""))
},
"REQUEST"
)
}
}
} else if (it.pendingRequest.isReady) {
showFragment(VerificationChooseMethodFragment::class, Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationArgs(it.otherUserMxItem?.id ?: "", it.pendingRequest.transactionId))
})
}
super.invalidate() super.invalidate()
} }
private fun showFragment(fragmentClass: KClass<out Fragment>, bundle: Bundle) {
if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) {
// choose method
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
)
}
}
}
} }
fun Fragment.getParentCoordinatorLayout(): CoordinatorLayout? { fun View.getParentCoordinatorLayout(): CoordinatorLayout? {
var current = view?.parent as? View var current = this as? View
while (current != null) { while (current != null) {
if (current is CoordinatorLayout) return current if (current is CoordinatorLayout) return current
current = current.parent as? View current = current.parent as? View

View file

@ -1,50 +1,178 @@
/*
* 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 package im.vector.riotx.features.crypto.verification
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.* import com.airbnb.mvrx.*
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent 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.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem 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.VectorViewModel
import im.vector.riotx.core.platform.VectorViewModelAction
import im.vector.riotx.core.utils.LiveEvent
data class VerificationBottomSheetViewState( data class VerificationBottomSheetViewState(
val userId: String = "", val otherUserMxItem: MatrixItem? = null,
val otherUserId: MatrixItem? = null, val roomId: String? = null,
val verificationRequestEvent: Async<TimelineEvent> = Uninitialized val pendingRequest: PendingVerificationRequest? = null,
val sasTransactionState: SasVerificationTxState? = null,
val cancelCode: CancelCode? = null
) : MvRxState ) : 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, class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: VerificationBottomSheetViewState,
private val session: Session) private val session: Session)
: VectorViewModel<VerificationBottomSheetViewState, VerificationAction>(initialState) { : 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 { init {
withState { session.getSasVerificationService().addListener(this)
session.getUser(it.userId).let { user ->
setState {
copy(otherUserId = user?.toMatrixItem())
}
}
} }
override fun onCleared() {
session.getSasVerificationService().removeListener(this)
super.onCleared()
} }
@AssistedInject.Factory @AssistedInject.Factory
interface Factory { interface Factory {
fun create(initialState: VerificationBottomSheetViewState): VerificationBottomSheetViewModel fun create(initialState: VerificationBottomSheetViewState): VerificationBottomSheetViewModel
} }
companion object : MvRxViewModelFactory<VerificationBottomSheetViewModel, VerificationBottomSheetViewState> { companion object : MvRxViewModelFactory<VerificationBottomSheetViewModel, VerificationBottomSheetViewState> {
override fun create(viewModelContext: ViewModelContext, state: VerificationBottomSheetViewState): VerificationBottomSheetViewModel? { override fun create(viewModelContext: ViewModelContext, state: VerificationBottomSheetViewState): VerificationBottomSheetViewModel? {
val fragment: VerificationBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() val fragment: VerificationBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
val userId: String = viewModelContext.args() val args: VerificationBottomSheet.VerificationArgs = viewModelContext.args()
return fragment.verificationViewModelFactory.create(VerificationBottomSheetViewState(userId))
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
val userItem = session.getUser(args.otherUserId)
val sasTx = state.pendingRequest?.transactionId?.let {
session.getSasVerificationService().getExistingTransaction(args.otherUserId, it)
}
val pr = session.getSasVerificationService().getExistingVerificationRequest(args.otherUserId)
?.firstOrNull { it.transactionId == args.verificationId }
return fragment.verificationViewModelFactory.create(VerificationBottomSheetViewState(
otherUserMxItem = userItem?.toMatrixItem(),
sasTransactionState = sasTx?.state,
pendingRequest = pr,
roomId = args.roomId)
)
} }
} }
override fun handle(action: VerificationAction) { override fun handle(action: VerificationAction) = withState { state ->
TODO("not implemented") //To change body of created functions use File | Settings | File Templates. 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, null))
}
}
is VerificationAction.StartSASVerification -> {
val request = session.getSasVerificationService().getExistingVerificationRequest(otherUserId)
?.firstOrNull { it.transactionId == 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)
?.shortCodeDoNotMatch()
}
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)
}
}
} }
} }

View file

@ -1,37 +1,69 @@
/*
* 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 package im.vector.riotx.features.crypto.verification
import android.os.Bundle import android.text.style.ClickableSpan
import androidx.transition.AutoTransition import android.view.View
import androidx.transition.ChangeBounds import androidx.core.text.toSpannable
import androidx.transition.TransitionManager import androidx.core.view.isVisible
import butterknife.OnClick import butterknife.OnClick
import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.commitTransaction
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.platform.parentFragmentViewModel
import im.vector.riotx.core.utils.tappableMatchingText
import kotlinx.android.synthetic.main.fragment_verification_choose_method.*
import javax.inject.Inject import javax.inject.Inject
class VerificationChooseMethodFragment @Inject constructor() : VectorBaseFragment() { class VerificationChooseMethodFragment @Inject constructor(
val verificationChooseMethodViewModelFactory: VerificationChooseMethodViewModel.Factory
) : VectorBaseFragment() {
override fun getLayoutResId() = R.layout.fragment_verification_choose_method override fun getLayoutResId() = R.layout.fragment_verification_choose_method
// init { private val sharedViewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class)
// sharedElementEnterTransition = ChangeBounds()
// sharedElementReturnTransition = ChangeBounds() 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) @OnClick(R.id.verificationByEmojiButton)
fun test() { //withState(viewModel) { state -> fun doVerifyBySas() = withState(sharedViewModel) {
getParentCoordinatorLayout()?.let { sharedViewModel.handle(VerificationAction.StartSASVerification(it.otherUserMxItem?.id ?: "", it.pendingRequest?.transactionId
TransitionManager.beginDelayedTransition(it, AutoTransition().apply { duration = 150 }) ?: ""))
}
parentFragmentManager.commitTransaction {
// setCustomAnimations(R.anim.fade_in, R.anim.fade_out)
replace(R.id.bottomSheetFragmentContainer,
OutgoingVerificationRequestFragment::class.java,
Bundle().apply { putString(MvRx.KEY_ARG, "@valere35:matrix.org") },
"REQUEST"
)
}
} }
} }

View file

@ -0,0 +1,77 @@
/*
* 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.internal.crypto.model.rest.KeyVerificationStart
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) {
init {
withState { state ->
val pvr = session.getSasVerificationService().getExistingVerificationRequest(state.otherUserId)?.first {
it.transactionId == 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
}
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()
return VerificationChooseMethodViewState(otherUserId = args.otherUserId, transactionId = args.verificationId ?: "")
}
}
override fun handle(action: EmptyAction) {}
}

View file

@ -0,0 +1,73 @@
/*
* 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 butterknife.OnClick
import com.airbnb.mvrx.fragmentViewModel
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 im.vector.riotx.core.platform.parentFragmentViewModel
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.setTextOrHide(null)
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)
}
}

View file

@ -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) {}
}

View file

@ -16,16 +16,14 @@
package im.vector.riotx.features.crypto.verification package im.vector.riotx.features.crypto.verification
import android.graphics.Typeface import android.graphics.Typeface
import android.os.Bundle
import androidx.core.text.toSpannable import androidx.core.text.toSpannable
import androidx.transition.AutoTransition import androidx.core.view.isInvisible
import androidx.transition.TransitionManager import androidx.core.view.isVisible
import butterknife.OnClick import butterknife.OnClick
import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.commitTransaction
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.platform.parentFragmentViewModel import im.vector.riotx.core.platform.parentFragmentViewModel
import im.vector.riotx.core.utils.colorizeMatchingText import im.vector.riotx.core.utils.colorizeMatchingText
@ -35,43 +33,51 @@ import im.vector.riotx.features.themes.ThemeUtils
import kotlinx.android.synthetic.main.fragment_verification_request.* import kotlinx.android.synthetic.main.fragment_verification_request.*
import javax.inject.Inject import javax.inject.Inject
class OutgoingVerificationRequestFragment @Inject constructor( class VerificationRequestFragment @Inject constructor(
val outgoingVerificationRequestViewModelFactory: OutgoingVerificationRequestViewModel.Factory, val verificationRequestViewModelFactory: VerificationRequestViewModel.Factory,
val avatarRenderer: AvatarRenderer val avatarRenderer: AvatarRenderer
) : VectorBaseFragment() { ) : VectorBaseFragment() {
private val viewModel by fragmentViewModel(OutgoingVerificationRequestViewModel::class) private val viewModel by fragmentViewModel(VerificationRequestViewModel::class)
private val sharedViewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class) private val sharedViewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class)
override fun getLayoutResId() = R.layout.fragment_verification_request override fun getLayoutResId() = R.layout.fragment_verification_request
override fun invalidate() = withState(viewModel) { state -> override fun invalidate() = withState(viewModel) { state ->
state.matrixItem?.let { state.matrixItem.let {
val styledText = getString(R.string.verification_request_alert_description, it.id) val styledText = getString(R.string.verification_request_alert_description, it.id)
.toSpannable() .toSpannable()
.styleMatchingText(it.id, Typeface.BOLD) .styleMatchingText(it.id, Typeface.BOLD)
.colorizeMatchingText(it.id, ThemeUtils.getColor(requireContext(), R.attr.vctr_notice_text_color)) .colorizeMatchingText(it.id, ThemeUtils.getColor(requireContext(), R.attr.vctr_notice_text_color))
verificationRequestText.text = styledText 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 Unit
} }
@OnClick(R.id.verificationStartButton) @OnClick(R.id.verificationStartButton)
fun onClickOnVerificationStart() = withState(viewModel) { state -> fun onClickOnVerificationStart() = withState(viewModel) { state ->
verificationStartButton.isEnabled = false
sharedViewModel.handle(VerificationAction.RequestVerificationByDM(state.otherUserId)) sharedViewModel.handle(VerificationAction.RequestVerificationByDM(state.matrixItem.id, state.roomId))
getParentCoordinatorLayout()?.let {
TransitionManager.beginDelayedTransition(it, AutoTransition().apply { duration = 150 })
}
parentFragmentManager.commitTransaction {
replace(R.id.bottomSheetFragmentContainer,
VerificationChooseMethodFragment::class.java,
Bundle().apply { putString(MvRx.KEY_ARG, state.otherUserId) },
"REQUEST"
)
}
} }
} }

View file

@ -0,0 +1,109 @@
/*
* 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.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, VerificationAction>(initialState), SasVerificationService.SasVerificationListener {
@AssistedInject.Factory
interface Factory {
fun create(initialState: VerificationRequestViewState): VerificationRequestViewModel
}
init {
withState {
val pr = session.getSasVerificationService()
.getExistingVerificationRequest(it.matrixItem.id)
?.firstOrNull()
setState {
copy(
started = Success(false).takeIf { pr == null }
?: Success(true).takeIf { pr?.isReady == true }
?: Loading<Boolean>()
)
}
}
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 otherUserId = viewModelContext.args<VerificationBottomSheet.VerificationArgs>().otherUserId
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
return session.getUser(otherUserId)?.let {
VerificationRequestViewState(matrixItem = it.toMatrixItem())
}
}
}
override fun handle(action: VerificationAction) {
}
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())
}
}
}
}
}

View file

@ -964,7 +964,18 @@ class RoomDetailFragment @Inject constructor(
} }
is RoomDetailAction.RequestVerification -> { is RoomDetailAction.RequestVerification -> {
VerificationBottomSheet().apply { VerificationBottomSheet().apply {
arguments = Bundle().apply { putString(MvRx.KEY_ARG, data.userId) } arguments = Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationBottomSheet.VerificationArgs(data.userId, roomId = roomDetailArgs.roomId))
}
// setArguments()
}.show(parentFragmentManager, "REQ")
}
is RoomDetailAction.AcceptVerificationRequest -> {
VerificationBottomSheet().apply {
arguments = Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationBottomSheet.VerificationArgs(
data.otherUserId, data.transactionId, roomId = roomDetailArgs.roomId))
}
}.show(parentFragmentManager, "REQ") }.show(parentFragmentManager, "REQ")
} }
} }
@ -1121,7 +1132,8 @@ class RoomDetailFragment @Inject constructor(
} }
override fun onAvatarClicked(informationData: MessageInformationData) { 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) { override fun onMemberNameClicked(informationData: MessageInformationData) {

View file

@ -49,7 +49,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.api.session.room.timeline.getTextEditableContent
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt 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.event.EncryptedEventContent
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.matrix.rx.unwrap import im.vector.matrix.rx.unwrap
import im.vector.riotx.R import im.vector.riotx.R
@ -186,6 +185,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages() is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages()
is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action) is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action)
is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action) is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action)
is RoomDetailAction.RequestVerification -> handleRequestVerification(action)
} }
} }
@ -798,20 +798,27 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }
private fun handleAcceptVerification(action: RoomDetailAction.AcceptVerificationRequest) { private fun handleAcceptVerification(action: RoomDetailAction.AcceptVerificationRequest) {
session.getSasVerificationService().beginKeyVerificationInDMs( session.getSasVerificationService().readyPendingVerificationInDMs(action.otherUserId,room.roomId,
KeyVerificationStart.VERIF_METHOD_SAS, action.transactionId)
action.transactionId, _requestLiveData.postValue(LiveEvent(Success(action)))
room.roomId, // session.getSasVerificationService().beginKeyVerificationInDMs(
action.otherUserId, // KeyVerificationStart.VERIF_METHOD_SAS,
action.otherdDeviceId, // action.transactionId,
null // room.roomId,
) // action.otherUserMxItem,
// action.otherdDeviceId,
// null
// )
} }
private fun handleDeclineVerification(action: RoomDetailAction.DeclineVerificationRequest) { private fun handleDeclineVerification(action: RoomDetailAction.DeclineVerificationRequest) {
Timber.e("TODO implement $action") Timber.e("TODO implement $action")
} }
private fun handleRequestVerification(action: RoomDetailAction.RequestVerification) {
_requestLiveData.postValue(LiveEvent(Success(action)))
}
private fun observeSyncState() { private fun observeSyncState() {
session.rx() session.rx()
.liveSyncState() .liveSyncState()

View file

@ -151,18 +151,18 @@ class MessageItemFactory @Inject constructor(
return VerificationRequestItem_() return VerificationRequestItem_()
.attributes( .attributes(
VerificationRequestItem.Attributes( VerificationRequestItem.Attributes(
otherUserId, otherUserId = otherUserId,
otherUserName.toString(), otherUserName = otherUserName.toString(),
messageContent.fromDevice, fromDevide = messageContent.fromDevice,
informationData.eventId, referenceId = informationData.eventId,
informationData, informationData = informationData,
attributes.avatarRenderer, avatarRenderer = attributes.avatarRenderer,
attributes.colorProvider, colorProvider = attributes.colorProvider,
attributes.itemLongClickListener, itemLongClickListener = attributes.itemLongClickListener,
attributes.itemClickListener, itemClickListener = attributes.itemClickListener,
attributes.reactionPillCallback, reactionPillCallback = attributes.reactionPillCallback,
attributes.readReceiptsCallback, readReceiptsCallback = attributes.readReceiptsCallback,
attributes.emojiTypeFace emojiTypeFace = attributes.emojiTypeFace
) )
) )
.callback(callback) .callback(callback)

View file

@ -70,6 +70,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_KEY, EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_READY,
EventType.KEY_VERIFICATION_MAC -> { EventType.KEY_VERIFICATION_MAC -> {
// These events are filtered from timeline in normal case // These events are filtered from timeline in normal case
// Only visible in developer mode // Only visible in developer mode

View file

@ -52,6 +52,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
EventType.KEY_VERIFICATION_MAC, EventType.KEY_VERIFICATION_MAC,
EventType.KEY_VERIFICATION_DONE, EventType.KEY_VERIFICATION_DONE,
EventType.KEY_VERIFICATION_KEY, EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_READY,
EventType.REDACTION -> formatDebug(timelineEvent.root) EventType.REDACTION -> formatDebug(timelineEvent.root)
else -> { else -> {
Timber.v("Type $type not handled by this formatter") Timber.v("Type $type not handled by this formatter")

View file

@ -50,7 +50,8 @@ object TimelineDisplayableEvents {
EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_MAC, EventType.KEY_VERIFICATION_MAC,
EventType.KEY_VERIFICATION_KEY EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_READY
) )
} }

View file

@ -0,0 +1,195 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:colorBackground">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<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: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"
android:text="@string/sas_waiting_for_partner"
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>
</ScrollView>

View file

@ -97,14 +97,15 @@
style="@style/VectorButtonStylePositive" style="@style/VectorButtonStylePositive"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:text="@string/accept" android:text="@string/verify_by_emoji_title"
app:layout_constraintTop_toBottomOf="@id/verifyEmojiDescription" /> app:layout_constraintTop_toBottomOf="@id/verifyEmojiDescription" />
<androidx.constraintlayout.widget.Group <androidx.constraintlayout.widget.Group
android:id="@+id/verifyQRGroup" android:id="@+id/verifyQRGroup"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="visible" android:visibility="gone"
tools:visibility="visible"
app:constraint_referenced_ids="verifyQRDescription,verificationQRTitle,verifyQRImageView" /> app:constraint_referenced_ids="verifyQRDescription,verificationQRTitle,verifyQRImageView" />
<androidx.constraintlayout.widget.Group <androidx.constraintlayout.widget.Group

View file

@ -0,0 +1,65 @@
<?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"
tools:text="@string/sas_verified"
android:textColor="?riotx_text_primary"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/verifyConclusionDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
tools:text="@string/sas_verified_successful_description"
android:text="@string/sas_verified_successful_description"
android:textColor="?riotx_text_secondary"
app:layout_constraintTop_toBottomOf="@id/verificationConclusionTitle"/>
<ImageView
android:id="@+id/verifyConclusionImageView"
android:layout_width="180dp"
android:layout_height="180dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
tools:background="@drawable/ic_shield_trusted"
android:background="@drawable/ic_shield_trusted"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/verifyConclusionDescription" />
<TextView
android:id="@+id/verifyConclusionBottomDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
tools:text="@string/verification_green_shield"
android:text="@string/verification_green_shield"
android:textColor="?riotx_text_secondary"
app:layout_constraintTop_toBottomOf="@id/verifyConclusionImageView"/>
<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>

View file

@ -1500,7 +1500,7 @@ Why choose Riot.im?
<string name="sas_verified">Verified!</string> <string name="sas_verified">Verified!</string>
<string name="sas_verified_successful">You\'ve successfully verified this device.</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 cant be read by third parties.</string>
<string name="sas_got_it">Got it</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> <string name="sas_verifying_keys">Nothing appearing? Not all clients supports interactive verification yet. Use legacy verification.</string>

View file

@ -2,16 +2,28 @@
<resources> <resources>
<!-- Strings not defined in Riot --> <!-- Strings not defined in Riot -->
<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="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_conclusion_warning">Untrusted sign in</string>
<string name="verification_sas_match">They match</string>
<string name="verification_sas_do_not_match">They dont 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 youre 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_waiting">Waiting…</string>
<string name="verification_request_other_cancelled">%s cancelled</string> <string name="verification_request_other_cancelled">%s cancelled</string>
<string name="verification_request_you_cancelled">You cancelled</string> <string name="verification_request_you_cancelled">You cancelled</string>