diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt index a24963ff66..418b7ac508 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt @@ -42,6 +42,8 @@ interface SasVerificationService { fun getExistingVerificationRequest(otherUser: String): List? + fun getExistingVerificationRequest(otherUser: String, tid: String?): PendingVerificationRequest? + /** * Shortcut for KeyVerificationStart.VERIF_METHOD_SAS * @see beginKeyVerification diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt new file mode 100644 index 0000000000..c01c121890 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt @@ -0,0 +1,163 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.tasks + +import im.vector.matrix.android.api.session.crypto.CryptoService +import im.vector.matrix.android.api.session.crypto.MXCryptoError +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.message.* +import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult +import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService +import im.vector.matrix.android.internal.di.DeviceId +import im.vector.matrix.android.internal.di.SessionDatabase +import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.task.Task +import io.realm.RealmConfiguration +import timber.log.Timber +import java.util.* +import javax.inject.Inject + +internal interface RoomVerificationUpdateTask : Task { + data class Params( + val events: List, + val sasVerificationService: DefaultSasVerificationService, + val cryptoService: CryptoService + ) +} + +internal class DefaultRoomVerificationUpdateTask @Inject constructor( + @UserId private val userId: String, + @DeviceId private val deviceId: String?, + private val cryptoService: CryptoService) : RoomVerificationUpdateTask { + + companion object { + private val transactionsHandledByOtherDevice = ArrayList() + } + + 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()?.type + if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) { + event.getClearContent().toModel()?.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()?.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()?.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()?.relatesTo?.eventId?.let { + transactionsHandledByOtherDevice.remove(it) + } + } + + return@forEach + } + + val relatesTo = event.getClearContent().toModel()?.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()?.type) { + params.sasVerificationService.onRoomRequestReceived(event) + } + } + } + } + } + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt index 9a466da192..d214769e97 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt @@ -223,7 +223,7 @@ internal class DefaultSasVerificationService @Inject constructor( } } - fun onRoomRequestReceived(event: Event) { + suspend fun onRoomRequestReceived(event: Event) { Timber.v("## SAS Verification request from ${event.senderId} in room ${event.roomId}") val requestInfo = event.getClearContent().toModel() ?: return @@ -234,6 +234,14 @@ internal class DefaultSasVerificationService @Inject constructor( 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().also { @@ -329,7 +337,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) @@ -382,11 +390,11 @@ internal class DefaultSasVerificationService @Inject constructor( } private suspend fun checkKeysAreDownloaded(otherUserId: String, - startReq: VerificationInfoStart): MXUsersDevicesMap? { + fromDevice: String): MXUsersDevicesMap? { 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 } @@ -404,6 +412,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) } @@ -527,7 +539,7 @@ internal class DefaultSasVerificationService @Inject constructor( handleMacReceived(event.senderId, macReq) } - private fun onRoomReadyReceived(event: Event) { + private suspend fun onRoomReadyReceived(event: Event) { val readyReq = event.getClearContent().toModel() ?.copy( // relates_to is in clear in encrypted payload @@ -539,6 +551,13 @@ internal class DefaultSasVerificationService @Inject constructor( // 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) } @@ -588,6 +607,12 @@ internal class DefaultSasVerificationService @Inject constructor( } } + override fun getExistingVerificationRequest(otherUser: String, tid: String?): PendingVerificationRequest? { + synchronized(lock = pendingRequests) { + return tid?.let { tid -> pendingRequests[otherUser]?.firstOrNull { it.transactionId == tid } } + } + } + private fun getExistingTransactionsForUser(otherUser: String): Collection? { synchronized(txMap) { return txMap[otherUser]?.values @@ -637,7 +662,8 @@ internal class DefaultSasVerificationService @Inject constructor( override fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback?) : PendingVerificationRequest { - Timber.i("Requesting verification to user: $userId in room ${roomId}") + + Timber.i("## SAS Requesting verification to user: $userId in room ${roomId}") val requestsForUser = pendingRequests[userId] ?: ArrayList().also { pendingRequests[userId] = it diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt index 6394f26178..6447b8668b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt @@ -15,6 +15,7 @@ */ package im.vector.matrix.android.internal.crypto.verification +import im.vector.matrix.android.api.session.crypto.sas.CancelCode import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent import java.util.* @@ -27,7 +28,10 @@ data class PendingVerificationRequest( val otherUserId: String, val transactionId: String? = null, val requestInfo: MessageVerificationRequestContent? = null, - val readyInfo: VerificationInfoReady? = null + val readyInfo: VerificationInfoReady? = null, + val cancelConclusion: CancelCode? = null, + val isSuccessful : Boolean = false + ) { val isReady: Boolean = readyInfo != null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASVerificationTransaction.kt index 4e0accd8e0..9df9248993 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASVerificationTransaction.kt @@ -227,13 +227,14 @@ internal abstract class SASVerificationTransaction( val keyIDNoPrefix = if (it.startsWith("ed25519:")) it.substring("ed25519:".length) else it val otherDeviceKey = otherUserKnownDevices?.get(keyIDNoPrefix)?.fingerprint() if (otherDeviceKey == null) { - Timber.e("Verification: Could not find device $keyIDNoPrefix to verify") + Timber.e("## SAS Verification: Could not find device $keyIDNoPrefix to verify") // just ignore and continue return@forEach } val mac = macUsingAgreedMethod(otherDeviceKey, baseInfo + it) if (mac != theirMac?.mac?.get(it)) { // WRONG! + Timber.e("## SAS Verification: mac mismatch for $otherDeviceKey with id $keyIDNoPrefix") cancel(CancelCode.MismatchedKeys) return } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt index 76511c9650..5b871b31b0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt @@ -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(realmConfiguration) { - override val query = Monarchy.Query { + override val query = Monarchy.Query { EventEntity.types(it, listOf( EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_ACCEPT, @@ -62,11 +55,8 @@ internal class VerificationMessageLiveObserver @Inject constructor( ) } - val transactionsHandledByOtherDevice = ArrayList() - override fun onChange(results: RealmResults, 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() } @@ -76,111 +66,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()?.type - if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) { - event.getClearContent().toModel()?.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()?.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()?.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()?.relatesTo?.eventId?.let { - transactionsHandledByOtherDevice.remove(it) - } - } - - return@forEach - } - - val relatesTo = event.getClearContent().toModel()?.relatesTo?.eventId - if (relatesTo != null && transactionsHandledByOtherDevice.contains(relatesTo)) { - // Ignore this event, it is directed to another of my devices - Timber.v("## SAS Verification live observer: Ignore Transaction handled by other device tid:$relatesTo ") - return@forEach - } - when (event.getClearType()) { - EventType.KEY_VERIFICATION_START, - EventType.KEY_VERIFICATION_ACCEPT, - EventType.KEY_VERIFICATION_KEY, - EventType.KEY_VERIFICATION_MAC, - EventType.KEY_VERIFICATION_CANCEL, - EventType.KEY_VERIFICATION_READY, - EventType.KEY_VERIFICATION_DONE -> { - sasVerificationService.onRoomEvent(event) - } - EventType.MESSAGE -> { - if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel()?.type) { - sasVerificationService.onRoomRequestReceived(event) - } - } - } - } } } diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index 2c3061ee55..5048c81a7f 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -273,11 +273,23 @@ interface FragmentModule { @Binds @IntoMap - @FragmentKey(OutgoingVerificationRequestFragment::class) - fun bindVerificationRequestFragment(fragment: OutgoingVerificationRequestFragment): Fragment + @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 } diff --git a/vector/src/main/java/im/vector/riotx/core/utils/SpannableUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/SpannableUtils.kt index 91b5ae2ffd..1b56fc2a57 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/SpannableUtils.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/SpannableUtils.kt @@ -16,9 +16,12 @@ 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 @@ -35,3 +38,21 @@ fun Spannable.colorizeMatchingText(match: String, @ColorInt color: Int): Spannab } 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() + }) +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/OutgoingVerificationRequestViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/OutgoingVerificationRequestViewModel.kt deleted file mode 100644 index 0f16b0786b..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/OutgoingVerificationRequestViewModel.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package im.vector.riotx.features.crypto.verification - -import com.airbnb.mvrx.* -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject -import im.vector.matrix.android.api.session.Session -import im.vector.matrix.android.api.util.MatrixItem -import im.vector.matrix.android.api.util.toMatrixItem -import im.vector.riotx.core.platform.VectorViewModel -import im.vector.riotx.core.platform.VectorViewModelAction - - -data class VerificationRequestViewState( - val otherUserId: String = "", - val matrixItem: MatrixItem? = null, - val started: Async = Success(false) -) : MvRxState - -sealed class VerificationAction : VectorViewModelAction { - data class RequestVerificationByDM(val userID: String) : VerificationAction() -} - -class OutgoingVerificationRequestViewModel @AssistedInject constructor( - @Assisted initialState: VerificationRequestViewState, - private val session: Session -) : VectorViewModel(initialState) { - - @AssistedInject.Factory - interface Factory { - fun create(initialState: VerificationRequestViewState): OutgoingVerificationRequestViewModel - } - - init { - withState { - val user = session.getUser(it.otherUserId) - setState { - copy(matrixItem = user?.toMatrixItem()) - } - } - } - - companion object : MvRxViewModelFactory { - override fun create(viewModelContext: ViewModelContext, state: VerificationRequestViewState): OutgoingVerificationRequestViewModel? { - val fragment: OutgoingVerificationRequestFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.outgoingVerificationRequestViewModelFactory.create(state) - } - - override fun initialState(viewModelContext: ViewModelContext): VerificationRequestViewState? { - val userID: String = viewModelContext.args() - return VerificationRequestViewState(otherUserId = userID) - } - } - - - override fun handle(action: VerificationAction) { - } - -} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationCodeFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationCodeFragment.kt new file mode 100644 index 0000000000..ebd7f351a4 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationCodeFragment.kt @@ -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(R.id.item_emoji_tv).text = emojiRepresentation.emoji + emoji0View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId) + } + 1 -> { + emoji1View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji + emoji1View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId) + } + 2 -> { + emoji2View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji + emoji2View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId) + } + 3 -> { + emoji3View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji + emoji3View.findViewById(R.id.item_emoji_name_tv)?.setText(emojiRepresentation.nameResId) + } + 4 -> { + emoji4View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji + emoji4View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId) + } + 5 -> { + emoji5View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji + emoji5View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId) + } + 6 -> { + emoji6View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji + emoji6View.findViewById(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)) + } + +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationCodeViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationCodeViewModel.kt new file mode 100644 index 0000000000..5fa9cc6cff --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationCodeViewModel.kt @@ -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> = Uninitialized, + val decimalDescription: Async = Uninitialized, + val isWaitingFromOther: Boolean = false +) : MvRxState + +class SASVerificationCodeViewModel @AssistedInject constructor( + @Assisted initialState: SASVerificationCodeViewState, + private val session: Session +) : VectorViewModel(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>() + .takeIf { sasTx.supportsEmoji() } + ?: Uninitialized, + decimalDescription = Loading() + .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 { + + override fun create(viewModelContext: ViewModelContext, state: SASVerificationCodeViewState): SASVerificationCodeViewModel? { + val factory = (viewModelContext as FragmentViewModelContext).fragment().viewModelFactory + return factory.create(state) + } + + override fun initialState(viewModelContext: ViewModelContext): SASVerificationCodeViewState? { + val args = viewModelContext.args() + return SASVerificationCodeViewState( + transactionId = args.verificationId ?: "", + otherUserId = args.otherUserId + ) + } + } + + override fun handle(action: EmptyAction) { + + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationStartFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationStartFragment.kt index d9c3b1d155..d33167518f 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationStartFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationStartFragment.kt @@ -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() { override fun onSuccess(info: MXDeviceInfo?) { info?.let { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SasVerificationViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SasVerificationViewModel.kt index f14a85c516..637df8818e 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SasVerificationViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SasVerificationViewModel.kt @@ -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 diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt index abac6c6b72..66d0a918cd 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt @@ -1,6 +1,22 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package im.vector.riotx.features.crypto.verification import android.os.Bundle +import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -9,28 +25,43 @@ 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.Uninitialized +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.commitTransaction +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() { - @Inject lateinit var outgoingVerificationRequestViewModelFactory: OutgoingVerificationRequestViewModel.Factory + @Parcelize + data class VerificationArgs( + val otherUserId: String, + val verificationId: String? = null, + val roomId: String? = null + ) : Parcelable + + + @Inject + lateinit var verificationRequestViewModelFactory: VerificationRequestViewModel.Factory @Inject lateinit var verificationViewModelFactory: VerificationBottomSheetViewModel.Factory @Inject lateinit var avatarRenderer: AvatarRenderer - private val viewModel by fragmentViewModel(VerificationBottomSheetViewModel::class) override fun injectWith(injector: ScreenComponent) { @@ -49,24 +80,25 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { return view } - override fun invalidate() = withState(viewModel) { - when (it.verificationRequestEvent) { - is Uninitialized -> { - if (childFragmentManager.findFragmentByTag("REQUEST") == null) { - //Verification not yet started, put outgoing verification - childFragmentManager.commitTransaction { - setCustomAnimations(R.anim.fade_in, R.anim.fade_out) - replace(R.id.bottomSheetFragmentContainer, - OutgoingVerificationRequestFragment::class.java, - Bundle().apply { putString(MvRx.KEY_ARG, it.userId) }, - "REQUEST" - ) + 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() + } } } } - } + }) + } - it.otherUserId?.let { matrixItem -> + override fun invalidate() = withState(viewModel) { + + it.otherUserMxItem?.let { matrixItem -> val displayName = matrixItem.displayName ?: "" otherUserNameText.text = getString(R.string.verification_request_alert_title, displayName) .toSpannable() @@ -75,13 +107,121 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { avatarRenderer.render(matrixItem, otherUserAvatarImageView) } + //Did the request result in a SAS transaction? + if (it.sasTransactionState != null) { + + when (it.sasTransactionState) { + SasVerificationTxState.None, + SasVerificationTxState.SendingStart, + SasVerificationTxState.Started, + SasVerificationTxState.OnStarted, + SasVerificationTxState.SendingAccept, + SasVerificationTxState.Accepted, + SasVerificationTxState.OnAccepted, + SasVerificationTxState.SendingKey, + SasVerificationTxState.KeySent, + SasVerificationTxState.OnKeyReceived, + SasVerificationTxState.ShortCodeReady, + SasVerificationTxState.ShortCodeAccepted, + SasVerificationTxState.SendingMac, + SasVerificationTxState.MacSent, + SasVerificationTxState.Verifying -> { + val fragmentTag = SASVerificationCodeFragment::class.simpleName + if (childFragmentManager.findFragmentByTag(fragmentTag) == null) { + + // Commit now, to ensure changes occurs before next rendering frame (or bottomsheet want animate) + bottomSheetFragmentContainer.getParentCoordinatorLayout()?.let { coordinatorLayout -> + TransitionManager.beginDelayedTransition(coordinatorLayout, AutoTransition().apply { duration = 150 }) + } + childFragmentManager.commitTransactionNow { + replace(R.id.bottomSheetFragmentContainer, + SASVerificationCodeFragment::class.java, + Bundle().apply { + putParcelable(MvRx.KEY_ARG, VerificationArgs( + it.otherUserMxItem?.id ?: "", + it.pendingRequest?.transactionId)) + }, + SASVerificationCodeFragment::class.simpleName + ) + } + } + } + SasVerificationTxState.Verified, + SasVerificationTxState.Cancelled, + SasVerificationTxState.OnCancelled -> { + val fragmentTag = VerificationConclusionFragment::class.simpleName + if (childFragmentManager.findFragmentByTag(fragmentTag) == null) { + bottomSheetFragmentContainer.getParentCoordinatorLayout()?.let { coordinatorLayout -> + TransitionManager.beginDelayedTransition(coordinatorLayout, AutoTransition().apply { duration = 150 }) + } + childFragmentManager.commitTransactionNow { + replace(R.id.bottomSheetFragmentContainer, + VerificationConclusionFragment::class.java, + Bundle().apply { + putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args( + it.sasTransactionState == SasVerificationTxState.Verified, + it.cancelCode?.value)) + }, + fragmentTag + ) + } + } + } + } + + return@withState + } + + + // Transaction has not yet started + if (it.pendingRequest == null || !it.pendingRequest.isReady) { + if (childFragmentManager.findFragmentByTag("REQUEST") == null) { + bottomSheetFragmentContainer.getParentCoordinatorLayout()?.let { coordinatorLayout -> + TransitionManager.beginDelayedTransition(coordinatorLayout, AutoTransition().apply { duration = 150 }) + } + //Verification not yet started, put outgoing verification + childFragmentManager.commitTransactionNow { + replace(R.id.bottomSheetFragmentContainer, + VerificationRequestFragment::class.java, + Bundle().apply { + putParcelable(MvRx.KEY_ARG, VerificationArgs(it.otherUserMxItem?.id ?: "")) + }, + "REQUEST" + ) + } + } + } else if (it.pendingRequest.isReady) { + showFragment(VerificationChooseMethodFragment::class, Bundle().apply { + putParcelable(MvRx.KEY_ARG, VerificationArgs(it.otherUserMxItem?.id ?: "", it.pendingRequest.transactionId)) + }) + + } + super.invalidate() } + + private fun showFragment(fragmentClass: KClass, bundle: Bundle) { + if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) { + // choose method + bottomSheetFragmentContainer.getParentCoordinatorLayout()?.let { coordinatorLayout -> + TransitionManager.beginDelayedTransition(coordinatorLayout, AutoTransition().apply { duration = 150 }) + } + // Commit now, to ensure changes occurs before next rendering frame (or bottomsheet want animate) + childFragmentManager.commitTransactionNow { + + replace(R.id.bottomSheetFragmentContainer, + fragmentClass.java, + bundle, + fragmentClass.simpleName + ) + } + } + } } -fun Fragment.getParentCoordinatorLayout(): CoordinatorLayout? { - var current = view?.parent as? View +fun View.getParentCoordinatorLayout(): CoordinatorLayout? { + var current = this as? View while (current != null) { if (current is CoordinatorLayout) return current current = current.parent as? View diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt index 578cedc07e..a9265293fa 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt @@ -1,50 +1,178 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package im.vector.riotx.features.crypto.verification +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.room.timeline.TimelineEvent +import im.vector.matrix.android.api.session.crypto.sas.CancelCode +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.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 userId: String = "", - val otherUserId: MatrixItem? = null, - val verificationRequestEvent: Async = Uninitialized + 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(initialState) { + : VectorViewModel(initialState), + SasVerificationService.SasVerificationListener { + + + // Can be used for several actions, for a one shot result + private val _requestLiveData = MutableLiveData>>() + val requestLiveData: LiveData>> + get() = _requestLiveData init { - withState { - session.getUser(it.userId).let { user -> - setState { - copy(otherUserId = user?.toMatrixItem()) - } - } - } + 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 { override fun create(viewModelContext: ViewModelContext, state: VerificationBottomSheetViewState): VerificationBottomSheetViewModel? { val fragment: VerificationBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() - val userId: String = viewModelContext.args() - return fragment.verificationViewModelFactory.create(VerificationBottomSheetViewState(userId)) + 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) { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + 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) + } + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodFragment.kt index ca441b8a24..69c599d335 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodFragment.kt @@ -1,37 +1,69 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package im.vector.riotx.features.crypto.verification -import android.os.Bundle -import androidx.transition.AutoTransition -import androidx.transition.ChangeBounds -import androidx.transition.TransitionManager +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.MvRx +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState import im.vector.riotx.R -import im.vector.riotx.core.extensions.commitTransaction import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.platform.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() : VectorBaseFragment() { +class VerificationChooseMethodFragment @Inject constructor( + val verificationChooseMethodViewModelFactory: VerificationChooseMethodViewModel.Factory +) : VectorBaseFragment() { override fun getLayoutResId() = R.layout.fragment_verification_choose_method -// init { -// sharedElementEnterTransition = ChangeBounds() -// sharedElementReturnTransition = ChangeBounds() -// } + 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 test() { //withState(viewModel) { state -> - getParentCoordinatorLayout()?.let { - TransitionManager.beginDelayedTransition(it, AutoTransition().apply { duration = 150 }) - } - parentFragmentManager.commitTransaction { - // setCustomAnimations(R.anim.fade_in, R.anim.fade_out) - replace(R.id.bottomSheetFragmentContainer, - OutgoingVerificationRequestFragment::class.java, - Bundle().apply { putString(MvRx.KEY_ARG, "@valere35:matrix.org") }, - "REQUEST" - ) - } + fun doVerifyBySas() = withState(sharedViewModel) { + sharedViewModel.handle(VerificationAction.StartSASVerification(it.otherUserMxItem?.id ?: "", it.pendingRequest?.transactionId + ?: "")) } + } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodViewModel.kt new file mode 100644 index 0000000000..38482e9b09 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodViewModel.kt @@ -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(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 { + 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) {} + + +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationConclusionFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationConclusionFragment.kt new file mode 100644 index 0000000000..da3b0dd187 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationConclusionFragment.kt @@ -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) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationConclusionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationConclusionViewModel.kt new file mode 100644 index 0000000000..ca069bf853 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationConclusionViewModel.kt @@ -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(initialState) { + + companion object : MvRxViewModelFactory { + + override fun initialState(viewModelContext: ViewModelContext): SASVerificationConclusionViewState? { + val args = viewModelContext.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) {} +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/OutgoingVerificationRequestFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationRequestFragment.kt similarity index 61% rename from vector/src/main/java/im/vector/riotx/features/crypto/verification/OutgoingVerificationRequestFragment.kt rename to vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationRequestFragment.kt index f652c7bfdd..60b89c19d6 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/OutgoingVerificationRequestFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationRequestFragment.kt @@ -16,16 +16,14 @@ package im.vector.riotx.features.crypto.verification import android.graphics.Typeface -import android.os.Bundle import androidx.core.text.toSpannable -import androidx.transition.AutoTransition -import androidx.transition.TransitionManager +import androidx.core.view.isInvisible +import androidx.core.view.isVisible import butterknife.OnClick -import com.airbnb.mvrx.MvRx +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.extensions.commitTransaction import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.parentFragmentViewModel import im.vector.riotx.core.utils.colorizeMatchingText @@ -35,43 +33,51 @@ import im.vector.riotx.features.themes.ThemeUtils import kotlinx.android.synthetic.main.fragment_verification_request.* import javax.inject.Inject -class OutgoingVerificationRequestFragment @Inject constructor( - val outgoingVerificationRequestViewModelFactory: OutgoingVerificationRequestViewModel.Factory, +class VerificationRequestFragment @Inject constructor( + val verificationRequestViewModelFactory: VerificationRequestViewModel.Factory, val avatarRenderer: AvatarRenderer ) : VectorBaseFragment() { - private val viewModel by fragmentViewModel(OutgoingVerificationRequestViewModel::class) + 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 { + 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 -> - - sharedViewModel.handle(VerificationAction.RequestVerificationByDM(state.otherUserId)) - - getParentCoordinatorLayout()?.let { - TransitionManager.beginDelayedTransition(it, AutoTransition().apply { duration = 150 }) - } - parentFragmentManager.commitTransaction { - replace(R.id.bottomSheetFragmentContainer, - VerificationChooseMethodFragment::class.java, - Bundle().apply { putString(MvRx.KEY_ARG, state.otherUserId) }, - "REQUEST" - ) - } + verificationStartButton.isEnabled = false + sharedViewModel.handle(VerificationAction.RequestVerificationByDM(state.matrixItem.id, state.roomId)) } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationRequestViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationRequestViewModel.kt new file mode 100644 index 0000000000..752d6a6a8a --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationRequestViewModel.kt @@ -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 = Success(false) +) : MvRxState + + +class VerificationRequestViewModel @AssistedInject constructor( + @Assisted initialState: VerificationRequestViewState, + private val session: Session +) : VectorViewModel(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() + ) + } + } + session.getSasVerificationService().addListener(this) + } + + override fun onCleared() { + session.getSasVerificationService().removeListener(this) + super.onCleared() + } + + companion object : MvRxViewModelFactory { + 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().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()) + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 1db153e594..f0c4ce5971 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -925,7 +925,7 @@ class RoomDetailFragment @Inject constructor( } is Success -> { when (val data = result.invoke()) { - is RoomDetailAction.ReportContent -> { + is RoomDetailAction.ReportContent -> { when { data.spam -> { AlertDialog.Builder(requireActivity()) @@ -962,9 +962,20 @@ class RoomDetailFragment @Inject constructor( } } } - is RoomDetailAction.RequestVerification -> { + is RoomDetailAction.RequestVerification -> { VerificationBottomSheet().apply { - arguments = Bundle().apply { putString(MvRx.KEY_ARG, data.userId) } + arguments = Bundle().apply { + putParcelable(MvRx.KEY_ARG, VerificationBottomSheet.VerificationArgs(data.userId, roomId = roomDetailArgs.roomId)) + } +// setArguments() + }.show(parentFragmentManager, "REQ") + } + is RoomDetailAction.AcceptVerificationRequest -> { + VerificationBottomSheet().apply { + arguments = Bundle().apply { + putParcelable(MvRx.KEY_ARG, VerificationBottomSheet.VerificationArgs( + data.otherUserId, data.transactionId, roomId = roomDetailArgs.roomId)) + } }.show(parentFragmentManager, "REQ") } } @@ -1121,7 +1132,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) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index a8623c4cb2..36c0442e2a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -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 @@ -186,6 +185,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages() is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action) is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action) + is RoomDetailAction.RequestVerification -> handleRequestVerification(action) } } @@ -798,20 +798,27 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } private fun handleAcceptVerification(action: RoomDetailAction.AcceptVerificationRequest) { - 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() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index dadf267dd7..36856eebe4 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -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) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 13679cecaf..3ff4af27c2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -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 diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index 39afebf5af..c8058d0fa8 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -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") diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index 71272fe815..2864fe6802 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -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 ) } diff --git a/vector/src/main/res/layout/fragment_bottom_sas_verification_code.xml b/vector/src/main/res/layout/fragment_bottom_sas_verification_code.xml new file mode 100644 index 0000000000..bf51aab3df --- /dev/null +++ b/vector/src/main/res/layout/fragment_bottom_sas_verification_code.xml @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_verification_choose_method.xml b/vector/src/main/res/layout/fragment_verification_choose_method.xml index 70411c69b6..37b3c6e53a 100644 --- a/vector/src/main/res/layout/fragment_verification_choose_method.xml +++ b/vector/src/main/res/layout/fragment_verification_choose_method.xml @@ -97,14 +97,15 @@ style="@style/VectorButtonStylePositive" android:layout_width="match_parent" android:layout_marginTop="16dp" - android:text="@string/accept" + android:text="@string/verify_by_emoji_title" app:layout_constraintTop_toBottomOf="@id/verifyEmojiDescription" /> + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 8c1b2b819d..a320caeef7 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1500,7 +1500,7 @@ Why choose Riot.im? Verified! You\'ve successfully verified this device. - Secure messages with this user are end-to-end encrypted and not able to be read by third parties. + Messages with this user in this room are end-to-end encrypted and can‘t be read by third parties. Got it Nothing appearing? Not all clients supports interactive verification yet. Use legacy verification. diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 944a5336b3..e9db6a4f39 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -2,16 +2,28 @@ + Request to verify the given userID + Prepends ¯\\_(ツ)_/¯ to a plain-text message + Initial Sync… - File - Audio - Image. - Video. - - Untrusted sign in + They match + They don‘t match + Verify this user by confirming the following unique emoji appear on their screen, in the same order." + For ultimate security, use another trusted means of communication or do this in person. + Look for the green shield to ensure a user is trusted. Trust all users in a room to ensure the room is secure. + + Not secure + 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 + + + Video. + Image. + Audio + File + Waiting… %s cancelled You cancelled