mirror of
https://github.com/element-hq/element-android
synced 2024-11-28 13:38:49 +03:00
BottomSheet UX
This commit is contained in:
parent
2152af8851
commit
6bf3a703df
31 changed files with 2034 additions and 153 deletions
|
@ -39,6 +39,10 @@ interface SasVerificationService {
|
|||
|
||||
fun getExistingTransaction(otherUser: String, tid: String): SasVerificationTransaction?
|
||||
|
||||
fun getExistingVerificationRequest(otherUser: String): List<PendingVerificationRequest>?
|
||||
|
||||
fun getExistingVerificationRequest(otherUser: String, tid: String?): PendingVerificationRequest?
|
||||
|
||||
/**
|
||||
* Shortcut for KeyVerificationStart.VERIF_METHOD_SAS
|
||||
* @see beginKeyVerification
|
||||
|
|
|
@ -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.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 {
|
||||
// XXX what about multi-account?
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -189,9 +189,49 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun onRoomRequestReceived(event: Event) {
|
||||
// TODO
|
||||
suspend fun onRoomRequestReceived(event: Event) {
|
||||
Timber.v("## SAS Verification request from ${event.senderId} in room ${event.roomId}")
|
||||
val requestInfo = event.getClearContent().toModel<MessageVerificationRequestContent>()
|
||||
?: return
|
||||
val senderId = event.senderId ?: return
|
||||
|
||||
if (requestInfo.toUserId != credentials.userId) {
|
||||
//I should ignore this, it's not for me
|
||||
Timber.w("## SAS Verification ignoring request from ${event.senderId}, not sent to me")
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
val requestsForUser = pendingRequests[senderId]
|
||||
?: ArrayList<PendingVerificationRequest>().also {
|
||||
pendingRequests[event.senderId] = it
|
||||
}
|
||||
|
||||
val pendingVerificationRequest = PendingVerificationRequest(
|
||||
isIncoming = true,
|
||||
otherUserId = senderId,//requestInfo.toUserId,
|
||||
transactionId = event.eventId,
|
||||
requestInfo = requestInfo
|
||||
)
|
||||
requestsForUser.add(pendingVerificationRequest)
|
||||
|
||||
/*
|
||||
* After the m.key.verification.ready event is sent, either party can send an m.key.verification.start event
|
||||
* to begin the verification.
|
||||
* If both parties send an m.key.verification.start event, and they both specify the same verification method,
|
||||
* then the event sent by the user whose user ID is the smallest is used, and the other m.key.verification.start
|
||||
* event is ignored.
|
||||
* In the case of a single user verifying two of their devices, the device ID is compared instead.
|
||||
* If both parties send an m.key.verification.start event, but they specify different verification methods,
|
||||
* the verification should be cancelled with a code of m.unexpected_message.
|
||||
*/
|
||||
}
|
||||
|
||||
private suspend fun onRoomStartRequestReceived(event: Event) {
|
||||
|
@ -263,7 +303,7 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
|
||||
private suspend fun handleStart(otherUserId: String?, startReq: VerificationInfoStart, txConfigure: (SASVerificationTransaction) -> Unit): CancelCode? {
|
||||
Timber.d("## SAS onStartRequestReceived ${startReq.transactionID!!}")
|
||||
if (checkKeysAreDownloaded(otherUserId!!, startReq) != null) {
|
||||
if (checkKeysAreDownloaded(otherUserId!!, startReq.fromDevice ?: "") != null) {
|
||||
Timber.v("## SAS onStartRequestReceived $startReq")
|
||||
val tid = startReq.transactionID!!
|
||||
val existing = getExistingTransaction(otherUserId, tid)
|
||||
|
@ -311,11 +351,11 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun checkKeysAreDownloaded(otherUserId: String,
|
||||
startReq: VerificationInfoStart): MXUsersDevicesMap<MXDeviceInfo>? {
|
||||
fromDevice: String): MXUsersDevicesMap<MXDeviceInfo>? {
|
||||
return try {
|
||||
val keys = deviceListManager.downloadKeys(listOf(otherUserId), true)
|
||||
val deviceIds = keys.getUserDeviceIds(otherUserId) ?: return null
|
||||
keys.takeIf { deviceIds.contains(startReq.fromDevice) }
|
||||
keys.takeIf { deviceIds.contains(fromDevice) }
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
@ -333,6 +373,10 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
// TODO should we cancel?
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -456,6 +500,28 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
handleMacReceived(event.senderId, macReq)
|
||||
}
|
||||
|
||||
private suspend fun onRoomReadyReceived(event: Event) {
|
||||
val readyReq = event.getClearContent().toModel<MessageVerificationReadyContent>()
|
||||
?.copy(
|
||||
// relates_to is in clear in encrypted payload
|
||||
relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo
|
||||
)
|
||||
if (readyReq == null || readyReq.isValid().not() || event.senderId == null) {
|
||||
// ignore
|
||||
Timber.e("## SAS Received invalid ready request")
|
||||
// TODO should we cancel?
|
||||
return
|
||||
}
|
||||
if(checkKeysAreDownloaded(event.senderId, readyReq.fromDevice ?: "") == null) {
|
||||
Timber.e("## SAS Verification device ${readyReq.fromDevice} is not knwown")
|
||||
// TODO cancel?
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
handleReadyReceived(event.senderId, readyReq)
|
||||
}
|
||||
|
||||
private fun onMacReceived(event: Event) {
|
||||
val macReq = event.getClearContent().toModel<KeyVerificationMac>()!!
|
||||
|
||||
|
@ -487,6 +553,18 @@ internal class DefaultSasVerificationService @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun getExistingVerificationRequest(otherUser: String): List<PendingVerificationRequest>? {
|
||||
synchronized(lock = pendingRequests) {
|
||||
return pendingRequests[otherUser]
|
||||
}
|
||||
}
|
||||
|
||||
override fun getExistingVerificationRequest(otherUser: String, tid: String?): PendingVerificationRequest? {
|
||||
synchronized(lock = pendingRequests) {
|
||||
return tid?.let { tid -> pendingRequests[otherUser]?.firstOrNull { it.transactionId == tid } }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getExistingTransactionsForUser(otherUser: String): Collection<VerificationTransaction>? {
|
||||
synchronized(txMap) {
|
||||
return txMap[otherUser]?.values
|
||||
|
@ -536,7 +614,30 @@ 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 {
|
||||
|
||||
Timber.i("## SAS Requesting verification to user: $userId in room ${roomId}")
|
||||
val requestsForUser = pendingRequests[userId]
|
||||
?: ArrayList<PendingVerificationRequest>().also {
|
||||
pendingRequests[userId] = it
|
||||
}
|
||||
|
||||
val params = requestVerificationDMTask.createParamsAndLocalEcho(
|
||||
roomId = roomId,
|
||||
from = credentials.deviceId ?: "",
|
||||
methods = listOf(KeyVerificationStart.VERIF_METHOD_SAS),
|
||||
to = userId,
|
||||
cryptoService = cryptoService
|
||||
)
|
||||
val verificationRequest = PendingVerificationRequest(
|
||||
isIncoming = false,
|
||||
localID = params.event.eventId ?: "",
|
||||
otherUserId = userId
|
||||
)
|
||||
requestsForUser.add(verificationRequest)
|
||||
dispatchRequestAdded(verificationRequest)
|
||||
|
||||
requestVerificationDMTask.configureWith(
|
||||
requestVerificationDMTask.createParamsAndLocalEcho(
|
||||
roomId = roomId,
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.matrix.android.internal.crypto.verification
|
||||
|
||||
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Stores current pending verification requests
|
||||
*/
|
||||
data class PendingVerificationRequest(
|
||||
val isIncoming: Boolean = false,
|
||||
val localID: String = UUID.randomUUID().toString(),
|
||||
val otherUserId: String,
|
||||
val transactionId: String? = null,
|
||||
val requestInfo: MessageVerificationRequestContent? = null,
|
||||
val readyInfo: VerificationInfoReady? = null,
|
||||
val cancelConclusion: CancelCode? = null,
|
||||
val isSuccessful : Boolean = false
|
||||
|
||||
) {
|
||||
|
||||
val isReady: Boolean = readyInfo != null
|
||||
val isSent: Boolean = transactionId != null
|
||||
}
|
|
@ -222,13 +222,14 @@ internal abstract class SASVerificationTransaction(
|
|||
val keyIDNoPrefix = if (it.startsWith("ed25519:")) it.substring("ed25519:".length) else it
|
||||
val otherDeviceKey = otherUserKnownDevices?.get(keyIDNoPrefix)?.fingerprint()
|
||||
if (otherDeviceKey == null) {
|
||||
Timber.e("Verification: Could not find device $keyIDNoPrefix to verify")
|
||||
Timber.e("## SAS Verification: Could not find device $keyIDNoPrefix to verify")
|
||||
// just ignore and continue
|
||||
return@forEach
|
||||
}
|
||||
val mac = macUsingAgreedMethod(otherDeviceKey, baseInfo + it)
|
||||
if (mac != theirMac?.mac?.get(it)) {
|
||||
// WRONG!
|
||||
Timber.e("## SAS Verification: mac mismatch for $otherDeviceKey with id $keyIDNoPrefix")
|
||||
cancel(CancelCode.MismatchedKeys)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -17,38 +17,31 @@ package im.vector.matrix.android.internal.crypto.verification
|
|||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.message.*
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultRoomVerificationUpdateTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.RoomVerificationUpdateTask
|
||||
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
|
||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.query.types
|
||||
import im.vector.matrix.android.internal.di.DeviceId
|
||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import io.realm.OrderedCollectionChangeSet
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.RealmResults
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
internal class VerificationMessageLiveObserver @Inject constructor(
|
||||
@SessionDatabase realmConfiguration: RealmConfiguration,
|
||||
@UserId private val userId: String,
|
||||
@DeviceId private val deviceId: String?,
|
||||
private val roomVerificationUpdateTask: DefaultRoomVerificationUpdateTask,
|
||||
private val cryptoService: CryptoService,
|
||||
private val sasVerificationService: DefaultSasVerificationService,
|
||||
private val taskExecutor: TaskExecutor
|
||||
) : RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
|
||||
|
||||
override val query = Monarchy.Query<EventEntity> {
|
||||
override val query = Monarchy.Query {
|
||||
EventEntity.types(it, listOf(
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
|
@ -61,11 +54,8 @@ internal class VerificationMessageLiveObserver @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
val transactionsHandledByOtherDevice = ArrayList<String>()
|
||||
|
||||
override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
|
||||
// TODO do that in a task
|
||||
// TODO how to ignore when it's an initial sync?
|
||||
// Should we ignore when it's an initial sync?
|
||||
val events = changeSet.insertions
|
||||
.asSequence()
|
||||
.mapNotNull { results[it]?.asDomain() }
|
||||
|
@ -75,102 +65,9 @@ internal class VerificationMessageLiveObserver @Inject constructor(
|
|||
}
|
||||
.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_CANCEL == event.type || EventType.KEY_VERIFICATION_DONE == event.type) {
|
||||
event.getClearContent().toModel<MessageRelationContent>()?.relatesTo?.eventId?.let {
|
||||
transactionsHandledByOtherDevice.remove(it)
|
||||
}
|
||||
}
|
||||
|
||||
return@forEach
|
||||
}
|
||||
|
||||
val relatesTo = event.getClearContent().toModel<MessageRelationContent>()?.relatesTo?.eventId
|
||||
if (relatesTo != null && transactionsHandledByOtherDevice.contains(relatesTo)) {
|
||||
// Ignore this event, it is directed to another of my devices
|
||||
Timber.v("## SAS Verification live observer: Ignore Transaction handled by other device tid:$relatesTo ")
|
||||
return@forEach
|
||||
}
|
||||
when (event.getClearType()) {
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
EventType.KEY_VERIFICATION_KEY,
|
||||
EventType.KEY_VERIFICATION_MAC,
|
||||
EventType.KEY_VERIFICATION_CANCEL,
|
||||
EventType.KEY_VERIFICATION_DONE -> {
|
||||
sasVerificationService.onRoomEvent(event)
|
||||
}
|
||||
EventType.MESSAGE -> {
|
||||
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.type) {
|
||||
sasVerificationService.onRoomRequestReceived(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -272,4 +272,27 @@ interface FragmentModule {
|
|||
@IntoMap
|
||||
@FragmentKey(SoftLogoutFragment::class)
|
||||
fun bindSoftLogoutFragment(fragment: SoftLogoutFragment): Fragment
|
||||
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(VerificationRequestFragment::class)
|
||||
fun bindVerificationRequestFragment(fragment: VerificationRequestFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(VerificationChooseMethodFragment::class)
|
||||
fun bindVerificationMethodChooserFragment(fragment: VerificationChooseMethodFragment): Fragment
|
||||
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(SASVerificationCodeFragment::class)
|
||||
fun bindVerificationSasCodeFragment(fragment: SASVerificationCodeFragment): Fragment
|
||||
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(VerificationConclusionFragment::class)
|
||||
fun bindVerificationSasConclusionFragment(fragment: VerificationConclusionFragment): Fragment
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotx.core.utils
|
||||
|
||||
import android.text.Spannable
|
||||
import android.text.style.BulletSpan
|
||||
import android.text.style.ClickableSpan
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.StyleSpan
|
||||
import androidx.annotation.ColorInt
|
||||
import me.gujun.android.span.Span
|
||||
|
||||
fun Spannable.styleMatchingText(match: String, typeFace: Int): Spannable {
|
||||
if (match.isEmpty()) return this
|
||||
indexOf(match).takeIf { it != -1 }?.let { start ->
|
||||
this.setSpan(StyleSpan(typeFace), start, start + match.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun Spannable.colorizeMatchingText(match: String, @ColorInt color: Int): Spannable {
|
||||
if (match.isEmpty()) return this
|
||||
indexOf(match).takeIf { it != -1 }?.let { start ->
|
||||
this.setSpan(ForegroundColorSpan(color), start, start + match.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun Spannable.tappableMatchingText(match: String, clickSpan: ClickableSpan): Spannable {
|
||||
if (match.isEmpty()) return this
|
||||
indexOf(match).takeIf { it != -1 }?.let { start ->
|
||||
this.setSpan(clickSpan, start, start + match.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun Span.bullet(text: CharSequence = "",
|
||||
init: Span.() -> Unit = {}): Span = apply {
|
||||
append(Span(parent = this).apply {
|
||||
this.text = text
|
||||
this.spans.add(BulletSpan())
|
||||
init()
|
||||
build()
|
||||
})
|
||||
}
|
|
@ -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()
|
||||
|
||||
/*
|
||||
viewModel.session.crypto?.getDeviceInfo(viewModel.otherUserId ?: "", viewModel.otherDeviceId
|
||||
viewModel.session.crypto?.getDeviceInfo(viewModel.otherUserMxItem ?: "", viewModel.otherDeviceId
|
||||
?: "", object : SimpleApiCallback<MXDeviceInfo>() {
|
||||
override fun onSuccess(info: MXDeviceInfo?) {
|
||||
info?.let {
|
||||
|
|
|
@ -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.SasVerificationTxState
|
||||
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 javax.inject.Inject
|
||||
|
||||
|
|
|
@ -0,0 +1,195 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotx.features.crypto.verification
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.text.toSpannable
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.transition.AutoTransition
|
||||
import androidx.transition.TransitionManager
|
||||
import butterknife.BindView
|
||||
import butterknife.ButterKnife
|
||||
import com.airbnb.mvrx.MvRx
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
import im.vector.riotx.core.extensions.commitTransactionNow
|
||||
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
|
||||
import im.vector.riotx.core.utils.colorizeMatchingText
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import im.vector.riotx.features.themes.ThemeUtils
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.bottom_sheet_verification.*
|
||||
import javax.inject.Inject
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
|
||||
@Parcelize
|
||||
data class VerificationArgs(
|
||||
val otherUserId: String,
|
||||
val verificationId: String? = null,
|
||||
val roomId: String? = null
|
||||
) : Parcelable
|
||||
|
||||
|
||||
@Inject
|
||||
lateinit var verificationRequestViewModelFactory: VerificationRequestViewModel.Factory
|
||||
@Inject
|
||||
lateinit var verificationViewModelFactory: VerificationBottomSheetViewModel.Factory
|
||||
@Inject
|
||||
lateinit var avatarRenderer: AvatarRenderer
|
||||
|
||||
private val viewModel by fragmentViewModel(VerificationBottomSheetViewModel::class)
|
||||
|
||||
override fun injectWith(injector: ScreenComponent) {
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
@BindView(R.id.verificationRequestName)
|
||||
lateinit var otherUserNameText: TextView
|
||||
|
||||
@BindView(R.id.verificationRequestAvatar)
|
||||
lateinit var otherUserAvatarImageView: ImageView
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val view = inflater.inflate(R.layout.bottom_sheet_verification, container, false)
|
||||
ButterKnife.bind(this, view)
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
viewModel.requestLiveData.observe(this, Observer {
|
||||
it.peekContent().let { va ->
|
||||
when (va) {
|
||||
is Success -> {
|
||||
if (va.invoke() is VerificationAction.GotItConclusion) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(viewModel) {
|
||||
|
||||
it.otherUserMxItem?.let { matrixItem ->
|
||||
val displayName = matrixItem.displayName ?: ""
|
||||
otherUserNameText.text = getString(R.string.verification_request_alert_title, displayName)
|
||||
.toSpannable()
|
||||
.colorizeMatchingText(displayName, ThemeUtils.getColor(requireContext(), R.attr.vctr_notice_text_color))
|
||||
|
||||
avatarRenderer.render(matrixItem, otherUserAvatarImageView)
|
||||
}
|
||||
|
||||
// Did the request result in a SAS transaction?
|
||||
if (it.sasTransactionState != null) {
|
||||
|
||||
when (it.sasTransactionState) {
|
||||
SasVerificationTxState.None,
|
||||
SasVerificationTxState.SendingStart,
|
||||
SasVerificationTxState.Started,
|
||||
SasVerificationTxState.OnStarted,
|
||||
SasVerificationTxState.SendingAccept,
|
||||
SasVerificationTxState.Accepted,
|
||||
SasVerificationTxState.OnAccepted,
|
||||
SasVerificationTxState.SendingKey,
|
||||
SasVerificationTxState.KeySent,
|
||||
SasVerificationTxState.OnKeyReceived,
|
||||
SasVerificationTxState.ShortCodeReady,
|
||||
SasVerificationTxState.ShortCodeAccepted,
|
||||
SasVerificationTxState.SendingMac,
|
||||
SasVerificationTxState.MacSent,
|
||||
SasVerificationTxState.Verifying -> {
|
||||
showFragment(SASVerificationCodeFragment::class, Bundle().apply {
|
||||
putParcelable(MvRx.KEY_ARG, VerificationArgs(
|
||||
it.otherUserMxItem?.id ?: "",
|
||||
it.pendingRequest?.transactionId))
|
||||
})
|
||||
}
|
||||
SasVerificationTxState.Verified,
|
||||
SasVerificationTxState.Cancelled,
|
||||
SasVerificationTxState.OnCancelled -> {
|
||||
showFragment(VerificationConclusionFragment::class, Bundle().apply {
|
||||
putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(
|
||||
it.sasTransactionState == SasVerificationTxState.Verified,
|
||||
it.cancelCode?.value))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return@withState
|
||||
}
|
||||
|
||||
|
||||
// Transaction has not yet started
|
||||
if (it.pendingRequest == null || !it.pendingRequest.isReady) {
|
||||
showFragment(VerificationRequestFragment::class, Bundle().apply {
|
||||
putParcelable(MvRx.KEY_ARG, VerificationArgs(it.otherUserMxItem?.id ?: ""))
|
||||
})
|
||||
} else if (it.pendingRequest.isReady) {
|
||||
showFragment(VerificationChooseMethodFragment::class, Bundle().apply {
|
||||
putParcelable(MvRx.KEY_ARG, VerificationArgs(it.otherUserMxItem?.id
|
||||
?: "", it.pendingRequest.transactionId))
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
super.invalidate()
|
||||
}
|
||||
|
||||
private fun showFragment(fragmentClass: KClass<out Fragment>, bundle: Bundle) {
|
||||
if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) {
|
||||
// We want to animate the bottomsheet bound changes
|
||||
bottomSheetFragmentContainer.getParentCoordinatorLayout()?.let { coordinatorLayout ->
|
||||
TransitionManager.beginDelayedTransition(coordinatorLayout, AutoTransition().apply { duration = 150 })
|
||||
}
|
||||
// Commit now, to ensure changes occurs before next rendering frame (or bottomsheet want animate)
|
||||
childFragmentManager.commitTransactionNow {
|
||||
|
||||
replace(R.id.bottomSheetFragmentContainer,
|
||||
fragmentClass.java,
|
||||
bundle,
|
||||
fragmentClass.simpleName
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun View.getParentCoordinatorLayout(): CoordinatorLayout? {
|
||||
var current = this as? View
|
||||
while (current != null) {
|
||||
if (current is CoordinatorLayout) return current
|
||||
current = current.parent as? View
|
||||
}
|
||||
return null
|
||||
}
|
|
@ -0,0 +1,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
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.airbnb.mvrx.*
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
|
||||
import im.vector.matrix.android.api.util.MatrixItem
|
||||
import im.vector.matrix.android.api.util.toMatrixItem
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart
|
||||
import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest
|
||||
import im.vector.riotx.core.di.HasScreenInjector
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
import im.vector.riotx.core.platform.VectorViewModelAction
|
||||
import im.vector.riotx.core.utils.LiveEvent
|
||||
|
||||
|
||||
data class VerificationBottomSheetViewState(
|
||||
val otherUserMxItem: MatrixItem? = null,
|
||||
val roomId: String? = null,
|
||||
val pendingRequest: PendingVerificationRequest? = null,
|
||||
val sasTransactionState: SasVerificationTxState? = null,
|
||||
val cancelCode: CancelCode? = null
|
||||
) : MvRxState
|
||||
|
||||
|
||||
sealed class VerificationAction : VectorViewModelAction {
|
||||
data class RequestVerificationByDM(val userID: String, val roomId: String?) : VerificationAction()
|
||||
data class StartSASVerification(val userID: String, val pendingRequestTransactionId: String) : VerificationAction()
|
||||
data class SASMatchAction(val userID: String, val sasTransactionId: String) : VerificationAction()
|
||||
data class SASDoNotMatchAction(val userID: String, val sasTransactionId: String) : VerificationAction()
|
||||
object GotItConclusion : VerificationAction()
|
||||
}
|
||||
|
||||
class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: VerificationBottomSheetViewState,
|
||||
private val session: Session)
|
||||
: VectorViewModel<VerificationBottomSheetViewState, VerificationAction>(initialState),
|
||||
SasVerificationService.SasVerificationListener {
|
||||
|
||||
|
||||
// Can be used for several actions, for a one shot result
|
||||
private val _requestLiveData = MutableLiveData<LiveEvent<Async<VerificationAction>>>()
|
||||
val requestLiveData: LiveData<LiveEvent<Async<VerificationAction>>>
|
||||
get() = _requestLiveData
|
||||
|
||||
init {
|
||||
session.getSasVerificationService().addListener(this)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
session.getSasVerificationService().removeListener(this)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(initialState: VerificationBottomSheetViewState): VerificationBottomSheetViewModel
|
||||
}
|
||||
|
||||
companion object : MvRxViewModelFactory<VerificationBottomSheetViewModel, VerificationBottomSheetViewState> {
|
||||
|
||||
override fun create(viewModelContext: ViewModelContext, state: VerificationBottomSheetViewState): VerificationBottomSheetViewModel? {
|
||||
val fragment: VerificationBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
|
||||
val args: VerificationBottomSheet.VerificationArgs = viewModelContext.args()
|
||||
|
||||
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
|
||||
|
||||
val userItem = session.getUser(args.otherUserId)
|
||||
|
||||
val 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) = withState { state ->
|
||||
val otherUserId = state.otherUserMxItem?.id ?: return@withState
|
||||
val roomId = state.roomId
|
||||
?: session.getExistingDirectRoomWithUser(otherUserId)?.roomId
|
||||
?: return@withState
|
||||
when (action) {
|
||||
is VerificationAction.RequestVerificationByDM -> {
|
||||
// session
|
||||
setState {
|
||||
copy(pendingRequest = session.getSasVerificationService().requestKeyVerificationInDMs(otherUserId, roomId, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +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
|
||||
|
||||
import android.text.style.ClickableSpan
|
||||
import android.view.View
|
||||
import androidx.core.text.toSpannable
|
||||
import androidx.core.view.isVisible
|
||||
import butterknife.OnClick
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.riotx.R
|
||||
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
|
||||
|
||||
class VerificationChooseMethodFragment @Inject constructor(
|
||||
val verificationChooseMethodViewModelFactory: VerificationChooseMethodViewModel.Factory
|
||||
) : VectorBaseFragment() {
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_verification_choose_method
|
||||
|
||||
private val sharedViewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class)
|
||||
|
||||
private val viewModel by fragmentViewModel(VerificationChooseMethodViewModel::class)
|
||||
|
||||
override fun invalidate() = withState(viewModel) { state ->
|
||||
if (state.QRModeAvailable) {
|
||||
val cSpan = object : ClickableSpan() {
|
||||
override fun onClick(widget: View) {
|
||||
|
||||
}
|
||||
}
|
||||
val openLink = getString(R.string.verify_open_camera_link)
|
||||
val descCharSequence =
|
||||
getString(R.string.verify_by_scanning_description, openLink)
|
||||
.toSpannable()
|
||||
.tappableMatchingText(openLink, cSpan)
|
||||
verifyQRDescription.text = descCharSequence
|
||||
verifyQRGroup.isVisible = true
|
||||
} else {
|
||||
verifyQRGroup.isVisible = false
|
||||
}
|
||||
|
||||
verifyEmojiGroup.isVisible = state.SASMOdeAvailable
|
||||
}
|
||||
|
||||
@OnClick(R.id.verificationByEmojiButton)
|
||||
fun doVerifyBySas() = withState(sharedViewModel) {
|
||||
sharedViewModel.handle(VerificationAction.StartSASVerification(it.otherUserMxItem?.id ?: "", it.pendingRequest?.transactionId
|
||||
?: ""))
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,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) {}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotx.features.crypto.verification
|
||||
|
||||
import android.graphics.Typeface
|
||||
import androidx.core.text.toSpannable
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import butterknife.OnClick
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import im.vector.riotx.core.platform.parentFragmentViewModel
|
||||
import im.vector.riotx.core.utils.colorizeMatchingText
|
||||
import im.vector.riotx.core.utils.styleMatchingText
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import im.vector.riotx.features.themes.ThemeUtils
|
||||
import kotlinx.android.synthetic.main.fragment_verification_request.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class VerificationRequestFragment @Inject constructor(
|
||||
val verificationRequestViewModelFactory: VerificationRequestViewModel.Factory,
|
||||
val avatarRenderer: AvatarRenderer
|
||||
) : VectorBaseFragment() {
|
||||
|
||||
private val viewModel by fragmentViewModel(VerificationRequestViewModel::class)
|
||||
|
||||
private val sharedViewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class)
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_verification_request
|
||||
|
||||
override fun invalidate() = withState(viewModel) { state ->
|
||||
state.matrixItem.let {
|
||||
val styledText = getString(R.string.verification_request_alert_description, it.id)
|
||||
.toSpannable()
|
||||
.styleMatchingText(it.id, Typeface.BOLD)
|
||||
.colorizeMatchingText(it.id, ThemeUtils.getColor(requireContext(), R.attr.vctr_notice_text_color))
|
||||
verificationRequestText.text = styledText
|
||||
}
|
||||
|
||||
when (state.started) {
|
||||
is Loading -> {
|
||||
//Hide the start button, show waiting
|
||||
verificationStartButton.isInvisible = true
|
||||
verificationWaitingText.isVisible = true
|
||||
val otherUser = state.matrixItem.displayName ?: state.matrixItem.id
|
||||
verificationWaitingText.text = getString(R.string.verification_request_waiting_for, otherUser)
|
||||
.toSpannable()
|
||||
.styleMatchingText(otherUser, Typeface.BOLD)
|
||||
.colorizeMatchingText(otherUser, ThemeUtils.getColor(requireContext(), R.attr.vctr_notice_text_color))
|
||||
}
|
||||
else -> {
|
||||
verificationStartButton.isEnabled = true
|
||||
verificationStartButton.isVisible = true
|
||||
verificationWaitingText.isInvisible = true
|
||||
}
|
||||
}
|
||||
|
||||
Unit
|
||||
}
|
||||
|
||||
@OnClick(R.id.verificationStartButton)
|
||||
fun onClickOnVerificationStart() = withState(viewModel) { state ->
|
||||
verificationStartButton.isEnabled = false
|
||||
sharedViewModel.handle(VerificationAction.RequestVerificationByDM(state.matrixItem.id, state.roomId))
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -923,7 +923,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
is Success -> {
|
||||
when (val data = result.invoke()) {
|
||||
is RoomDetailAction.ReportContent -> {
|
||||
is RoomDetailAction.ReportContent -> {
|
||||
when {
|
||||
data.spam -> {
|
||||
AlertDialog.Builder(requireActivity())
|
||||
|
@ -960,6 +960,22 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
is RoomDetailAction.RequestVerification -> {
|
||||
VerificationBottomSheet().apply {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1114,7 +1130,8 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onAvatarClicked(informationData: MessageInformationData) {
|
||||
vectorBaseActivity.notImplemented("Click on user avatar")
|
||||
//vectorBaseActivity.notImplemented("Click on user avatar")
|
||||
roomDetailViewModel.handle(RoomDetailAction.RequestVerification(informationData.senderId))
|
||||
}
|
||||
|
||||
override fun onMemberNameClicked(informationData: MessageInformationData) {
|
||||
|
|
|
@ -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.internal.crypto.attachments.toElementToDecrypt
|
||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart
|
||||
import im.vector.matrix.rx.rx
|
||||
import im.vector.matrix.rx.unwrap
|
||||
import im.vector.riotx.R
|
||||
|
@ -184,8 +183,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action)
|
||||
is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages()
|
||||
is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages()
|
||||
is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action)
|
||||
is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action)
|
||||
is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action)
|
||||
is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action)
|
||||
is RoomDetailAction.RequestVerification -> handleRequestVerification(action)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -796,20 +796,27 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
}
|
||||
|
||||
private fun handleAcceptVerification(action: RoomDetailAction.AcceptVerificationRequest) {
|
||||
session.getSasVerificationService().beginKeyVerificationInDMs(
|
||||
KeyVerificationStart.VERIF_METHOD_SAS,
|
||||
action.transactionId,
|
||||
room.roomId,
|
||||
action.otherUserId,
|
||||
action.otherdDeviceId,
|
||||
null
|
||||
)
|
||||
session.getSasVerificationService().readyPendingVerificationInDMs(action.otherUserId,room.roomId,
|
||||
action.transactionId)
|
||||
_requestLiveData.postValue(LiveEvent(Success(action)))
|
||||
// session.getSasVerificationService().beginKeyVerificationInDMs(
|
||||
// KeyVerificationStart.VERIF_METHOD_SAS,
|
||||
// action.transactionId,
|
||||
// room.roomId,
|
||||
// action.otherUserMxItem,
|
||||
// action.otherdDeviceId,
|
||||
// null
|
||||
// )
|
||||
}
|
||||
|
||||
private fun handleDeclineVerification(action: RoomDetailAction.DeclineVerificationRequest) {
|
||||
Timber.e("TODO implement $action")
|
||||
}
|
||||
|
||||
private fun handleRequestVerification(action: RoomDetailAction.RequestVerification) {
|
||||
_requestLiveData.postValue(LiveEvent(Success(action)))
|
||||
}
|
||||
|
||||
private fun observeSyncState() {
|
||||
session.rx()
|
||||
.liveSyncState()
|
||||
|
|
|
@ -151,18 +151,18 @@ class MessageItemFactory @Inject constructor(
|
|||
return VerificationRequestItem_()
|
||||
.attributes(
|
||||
VerificationRequestItem.Attributes(
|
||||
otherUserId,
|
||||
otherUserName.toString(),
|
||||
messageContent.fromDevice,
|
||||
informationData.eventId,
|
||||
informationData,
|
||||
attributes.avatarRenderer,
|
||||
attributes.colorProvider,
|
||||
attributes.itemLongClickListener,
|
||||
attributes.itemClickListener,
|
||||
attributes.reactionPillCallback,
|
||||
attributes.readReceiptsCallback,
|
||||
attributes.emojiTypeFace
|
||||
otherUserId = otherUserId,
|
||||
otherUserName = otherUserName.toString(),
|
||||
fromDevide = messageContent.fromDevice,
|
||||
referenceId = informationData.eventId,
|
||||
informationData = informationData,
|
||||
avatarRenderer = attributes.avatarRenderer,
|
||||
colorProvider = attributes.colorProvider,
|
||||
itemLongClickListener = attributes.itemLongClickListener,
|
||||
itemClickListener = attributes.itemClickListener,
|
||||
reactionPillCallback = attributes.reactionPillCallback,
|
||||
readReceiptsCallback = attributes.readReceiptsCallback,
|
||||
emojiTypeFace = attributes.emojiTypeFace
|
||||
)
|
||||
)
|
||||
.callback(callback)
|
||||
|
|
|
@ -70,6 +70,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
EventType.KEY_VERIFICATION_KEY,
|
||||
EventType.KEY_VERIFICATION_READY,
|
||||
EventType.KEY_VERIFICATION_MAC -> {
|
||||
// These events are filtered from timeline in normal case
|
||||
// Only visible in developer mode
|
||||
|
|
|
@ -52,6 +52,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
|
|||
EventType.KEY_VERIFICATION_MAC,
|
||||
EventType.KEY_VERIFICATION_DONE,
|
||||
EventType.KEY_VERIFICATION_KEY,
|
||||
EventType.KEY_VERIFICATION_READY,
|
||||
EventType.REDACTION -> formatDebug(timelineEvent.root)
|
||||
else -> {
|
||||
Timber.v("Type $type not handled by this formatter")
|
||||
|
|
|
@ -50,7 +50,8 @@ object TimelineDisplayableEvents {
|
|||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
EventType.KEY_VERIFICATION_MAC,
|
||||
EventType.KEY_VERIFICATION_KEY
|
||||
EventType.KEY_VERIFICATION_KEY,
|
||||
EventType.KEY_VERIFICATION_READY
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -0,0 +1,117 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
|
||||
<!-- <ImageView-->
|
||||
<!-- android:id="@+id/verificationRequestAvatar"-->
|
||||
<!-- android:layout_width="32dp"-->
|
||||
<!-- android:layout_height="32dp"-->
|
||||
<!-- android:adjustViewBounds="true"-->
|
||||
<!-- android:background="@drawable/circle"-->
|
||||
<!-- android:contentDescription="@string/avatar"-->
|
||||
<!-- android:scaleType="centerCrop"-->
|
||||
<!-- android:transitionName="bottomSheetAvatar"-->
|
||||
<!-- app:layout_constraintStart_toStartOf="parent"-->
|
||||
<!-- app:layout_constraintTop_toTopOf="parent"-->
|
||||
<!-- app:layout_constraintVertical_bias="0"-->
|
||||
<!-- tools:src="@tools:sample/avatars" />-->
|
||||
|
||||
<!-- <TextView-->
|
||||
<!-- android:id="@+id/verificationRequestName"-->
|
||||
<!-- android:layout_width="0dp"-->
|
||||
<!-- android:layout_height="wrap_content"-->
|
||||
<!-- android:layout_marginStart="16dp"-->
|
||||
<!-- android:text="@string/verification_request_alert_title"-->
|
||||
<!-- android:textColor="?riotx_text_primary"-->
|
||||
<!-- android:textSize="20sp"-->
|
||||
<!-- android:textStyle="bold"-->
|
||||
<!-- android:transitionName="bottomSheetDisplayName"-->
|
||||
<!-- app:layout_constraintBottom_toBottomOf="@id/verificationRequestAvatar"-->
|
||||
<!-- app:layout_constraintEnd_toEndOf="parent"-->
|
||||
<!-- app:layout_constraintStart_toEndOf="@id/verificationRequestAvatar"-->
|
||||
<!-- app:layout_constraintTop_toTopOf="@id/verificationRequestAvatar" />-->
|
||||
|
||||
<TextView
|
||||
android:id="@+id/verificationQRTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/verify_by_scanning_title"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/verifyQRDescription"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/verify_by_scanning_description"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
app:layout_constraintTop_toBottomOf="@id/verificationQRTitle"
|
||||
tools:text="@string/verify_by_scanning_description" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/verifyQRImageView"
|
||||
android:layout_width="180dp"
|
||||
android:layout_height="180dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="?riotx_header_panel_background"
|
||||
android:contentDescription="@string/aria_qr_code_description"
|
||||
android:visibility="visible"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/verifyQRDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/verificationEmojiTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="40dp"
|
||||
android:text="@string/verify_by_emoji_title"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintTop_toBottomOf="@id/verifyQRImageView"
|
||||
app:layout_goneMarginTop="0dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/verifyEmojiDescription"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/verify_by_emoji_description"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
app:layout_constraintTop_toBottomOf="@id/verificationEmojiTitle" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/verificationByEmojiButton"
|
||||
style="@style/VectorButtonStylePositive"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/verify_by_emoji_title"
|
||||
app:layout_constraintTop_toBottomOf="@id/verifyEmojiDescription" />
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/verifyQRGroup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
app:constraint_referenced_ids="verifyQRDescription,verificationQRTitle,verifyQRImageView" />
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/verifyEmojiGroup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="visible"
|
||||
app:constraint_referenced_ids="verifyEmojiDescription,verificationEmojiTitle,verificationByEmojiButton" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,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_successful">You\'ve successfully verified this device.</string>
|
||||
<string name="sas_verified_successful_description">Secure messages with this user are end-to-end encrypted and not able to be read by third parties.</string>
|
||||
<string name="sas_verified_successful_description">Messages with this user in this room are end-to-end encrypted and can‘t be read by third parties.</string>
|
||||
<string name="sas_got_it">Got it</string>
|
||||
|
||||
<string name="sas_verifying_keys">Nothing appearing? Not all clients supports interactive verification yet. Use legacy verification.</string>
|
||||
|
|
|
@ -5,15 +5,25 @@
|
|||
<string name="command_description_verify">Request to verify the given userID</string>
|
||||
<string name="command_description_shrug">Prepends ¯\\_(ツ)_/¯ to a plain-text message</string>
|
||||
|
||||
|
||||
<string name="notification_initial_sync">Initial Sync…</string>
|
||||
|
||||
<string name="sent_a_file">File</string>
|
||||
<string name="sent_an_audio_file">Audio</string>
|
||||
<string name="sent_an_image">Image.</string>
|
||||
<string name="sent_a_video">Video.</string>
|
||||
|
||||
|
||||
<string name="verification_conclusion_warning">Untrusted sign in</string>
|
||||
<string name="verification_sas_match">They match</string>
|
||||
<string name="verification_sas_do_not_match">They don‘t match</string>
|
||||
<string name="verify_user_sas_emoji_help_text">Verify this user by confirming the following unique emoji appear on their screen, in the same order."</string>
|
||||
<string name="verify_user_sas_emoji_security_tip">For ultimate security, use another trusted means of communication or do this in person.</string>
|
||||
<string name="verification_green_shield">Look for the green shield to ensure a user is trusted. Trust all users in a room to ensure the room is secure.</string>
|
||||
|
||||
<string name="verification_conclusion_not_secure">Not secure</string>
|
||||
<string name="verification_conclusion_compromised">One of the following may be compromised:\n\n - Your homeserver\n - The homeserver the user you’re verifying is connected to\n - Yours, or the other users’ internet connection\n - Yours, or the other users’ device
|
||||
</string>
|
||||
|
||||
<string name="sent_a_video">Video.</string>
|
||||
<string name="sent_an_image">Image.</string>
|
||||
<string name="sent_an_audio_file">Audio</string>
|
||||
<string name="sent_a_file">File</string>
|
||||
|
||||
<string name="verification_request_waiting">Waiting…</string>
|
||||
<string name="verification_request_other_cancelled">%s cancelled</string>
|
||||
<string name="verification_request_you_cancelled">You cancelled</string>
|
||||
|
|
Loading…
Reference in a new issue