mirror of
https://github.com/element-hq/element-android
synced 2024-11-25 02:45:37 +03:00
WIP
This commit is contained in:
parent
4edd5e3530
commit
a73cd61b96
32 changed files with 1622 additions and 322 deletions
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -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) {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {}
|
||||||
|
}
|
|
@ -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"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
|
@ -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 can‘t 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>
|
||||||
|
|
|
@ -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 don‘t match</string>
|
||||||
|
<string name="verify_user_sas_emoji_help_text">Verify this user by confirming the following unique emoji appear on their screen, in the same order."</string>
|
||||||
|
<string name="verify_user_sas_emoji_security_tip">For ultimate security, use another trusted means of communication or do this in person.</string>
|
||||||
|
<string name="verification_green_shield">Look for the green shield to ensure a user is trusted. Trust all users in a room to ensure the room is secure.</string>
|
||||||
|
|
||||||
|
<string name="verification_conclusion_not_secure">Not secure</string>
|
||||||
|
<string name="verification_conclusion_compromised">One of the following may be compromised:\n\n - Your homeserver\n - The homeserver the user you’re verifying is connected to\n - Yours, or the other users’ internet connection\n - Yours, or the other users’ device
|
||||||
|
</string>
|
||||||
|
|
||||||
|
<string name="sent_a_video">Video.</string>
|
||||||
|
<string name="sent_an_image">Image.</string>
|
||||||
|
<string name="sent_an_audio_file">Audio</string>
|
||||||
|
<string name="sent_a_file">File</string>
|
||||||
|
|
||||||
<string name="verification_request_waiting">Waiting…</string>
|
<string name="verification_request_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>
|
||||||
|
|
Loading…
Reference in a new issue