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

View file

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

View file

@ -0,0 +1,163 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.tasks
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService
import im.vector.matrix.android.internal.di.DeviceId
import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.task.Task
import io.realm.RealmConfiguration
import timber.log.Timber
import java.util.*
import javax.inject.Inject
internal interface RoomVerificationUpdateTask : Task<RoomVerificationUpdateTask.Params, Unit> {
data class Params(
val events: List<Event>,
val sasVerificationService: DefaultSasVerificationService,
val cryptoService: CryptoService
)
}
internal class DefaultRoomVerificationUpdateTask @Inject constructor(
@UserId private val userId: String,
@DeviceId private val deviceId: String?,
private val cryptoService: CryptoService) : RoomVerificationUpdateTask {
companion object {
private val transactionsHandledByOtherDevice = ArrayList<String>()
}
override suspend fun execute(params: RoomVerificationUpdateTask.Params): Unit {
// TODO ignore initial sync or back pagination?
val now = System.currentTimeMillis()
val tooInThePast = now - (10 * 60 * 1000)
val fiveMinInMs = 5 * 60 * 1000
val tooInTheFuture = System.currentTimeMillis() + fiveMinInMs
params.events.forEach { event ->
Timber.d("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}")
Timber.v("## SAS Verification live observer: received msgId: $event")
// If the request is in the future by more than 5 minutes or more than 10 minutes in the past,
// the message should be ignored by the receiver.
val ageLocalTs = event.ageLocalTs
if (ageLocalTs != null && (now - ageLocalTs) > fiveMinInMs) {
Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is too old (age: ${(now - ageLocalTs)})")
return@forEach
} else {
val eventOrigin = event.originServerTs ?: -1
if (eventOrigin < tooInThePast || eventOrigin > tooInTheFuture) {
Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is too old (ts: $eventOrigin")
return@forEach
}
}
// decrypt if needed?
if (event.isEncrypted() && event.mxDecryptionResult == null) {
// TODO use a global event decryptor? attache to session and that listen to new sessionId?
// for now decrypt sync
try {
val result = cryptoService.decryptEvent(event, event.roomId + UUID.randomUUID().toString())
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
} catch (e: MXCryptoError) {
Timber.e("## SAS Failed to decrypt event: ${event.eventId}")
}
}
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}")
if (event.senderId == userId) {
// If it's send from me, we need to keep track of Requests or Start
// done from another device of mine
if (EventType.MESSAGE == event.type) {
val msgType = event.getClearContent().toModel<MessageContent>()?.type
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) {
event.getClearContent().toModel<MessageVerificationRequestContent>()?.let {
if (it.fromDevice != deviceId) {
// The verification is requested from another device
Timber.v("## SAS Verification live observer: Transaction requested from other device tid:${event.eventId} ")
event.eventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
}
}
}
} else if (EventType.KEY_VERIFICATION_START == event.type) {
event.getClearContent().toModel<MessageVerificationStartContent>()?.let {
if (it.fromDevice != deviceId) {
// The verification is started from another device
Timber.v("## SAS Verification live observer: Transaction started by other device tid:${it.transactionID} ")
it.transactionID?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
}
}
} else if (EventType.KEY_VERIFICATION_READY == event.type) {
event.getClearContent().toModel<MessageVerificationReadyContent>()?.let {
if (it.fromDevice != deviceId) {
// The verification is started from another device
Timber.v("## SAS Verification live observer: Transaction started by other device tid:${it.transactionID} ")
it.transactionID?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
}
}
} else if (EventType.KEY_VERIFICATION_CANCEL == event.type || EventType.KEY_VERIFICATION_DONE == event.type) {
event.getClearContent().toModel<MessageRelationContent>()?.relatesTo?.eventId?.let {
transactionsHandledByOtherDevice.remove(it)
}
}
return@forEach
}
val relatesTo = event.getClearContent().toModel<MessageRelationContent>()?.relatesTo?.eventId
if (relatesTo != null && transactionsHandledByOtherDevice.contains(relatesTo)) {
// Ignore this event, it is directed to another of my devices
Timber.v("## SAS Verification live observer: Ignore Transaction handled by other device tid:$relatesTo ")
return@forEach
}
when (event.getClearType()) {
EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_MAC,
EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_READY,
EventType.KEY_VERIFICATION_DONE -> {
params.sasVerificationService.onRoomEvent(event)
}
EventType.MESSAGE -> {
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.type) {
params.sasVerificationService.onRoomRequestReceived(event)
}
}
}
}
}
}

View file

@ -223,7 +223,7 @@ internal class DefaultSasVerificationService @Inject constructor(
}
}
fun onRoomRequestReceived(event: Event) {
suspend fun onRoomRequestReceived(event: Event) {
Timber.v("## SAS Verification request from ${event.senderId} in room ${event.roomId}")
val requestInfo = event.getClearContent().toModel<MessageVerificationRequestContent>()
?: 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<PendingVerificationRequest>().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<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
}
@ -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<MessageVerificationReadyContent>()
?.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<VerificationTransaction>? {
synchronized(txMap) {
return txMap[otherUser]?.values
@ -637,7 +662,8 @@ internal class DefaultSasVerificationService @Inject constructor(
override fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback<String>?)
: 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<PendingVerificationRequest>().also {
pendingRequests[userId] = it

View file

@ -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

View file

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

View file

@ -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,
@ -62,11 +55,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() }
@ -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<MessageContent>()?.type
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) {
event.getClearContent().toModel<MessageVerificationRequestContent>()?.let {
if (it.fromDevice != deviceId) {
// The verification is requested from another device
Timber.v("## SAS Verification live observer: Transaction requested from other device tid:${event.eventId} ")
event.eventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
}
}
}
} else if (EventType.KEY_VERIFICATION_START == event.type) {
event.getClearContent().toModel<MessageVerificationStartContent>()?.let {
if (it.fromDevice != deviceId) {
// The verification is started from another device
Timber.v("## SAS Verification live observer: Transaction started by other device tid:${it.transactionID} ")
it.transactionID?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
}
}
} else if (EventType.KEY_VERIFICATION_READY == event.type) {
event.getClearContent().toModel<MessageVerificationReadyContent>()?.let {
if (it.fromDevice != deviceId) {
// The verification is started from another device
Timber.v("## SAS Verification live observer: Transaction started by other device tid:${it.transactionID} ")
it.transactionID?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
}
}
} else if (EventType.KEY_VERIFICATION_CANCEL == event.type || EventType.KEY_VERIFICATION_DONE == event.type) {
event.getClearContent().toModel<MessageRelationContent>()?.relatesTo?.eventId?.let {
transactionsHandledByOtherDevice.remove(it)
}
}
return@forEach
}
val relatesTo = event.getClearContent().toModel<MessageRelationContent>()?.relatesTo?.eventId
if (relatesTo != null && transactionsHandledByOtherDevice.contains(relatesTo)) {
// Ignore this event, it is directed to another of my devices
Timber.v("## SAS Verification live observer: Ignore Transaction handled by other device tid:$relatesTo ")
return@forEach
}
when (event.getClearType()) {
EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_MAC,
EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_READY,
EventType.KEY_VERIFICATION_DONE -> {
sasVerificationService.onRoomEvent(event)
}
EventType.MESSAGE -> {
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.type) {
sasVerificationService.onRoomRequestReceived(event)
}
}
}
}
}
}

View file

@ -273,11 +273,23 @@ interface FragmentModule {
@Binds
@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
}

View file

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

View file

@ -1,73 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.crypto.verification
import com.airbnb.mvrx.*
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.platform.VectorViewModelAction
data class VerificationRequestViewState(
val otherUserId: String = "",
val matrixItem: MatrixItem? = null,
val started: Async<Boolean> = Success(false)
) : MvRxState
sealed class VerificationAction : VectorViewModelAction {
data class RequestVerificationByDM(val userID: String) : VerificationAction()
}
class OutgoingVerificationRequestViewModel @AssistedInject constructor(
@Assisted initialState: VerificationRequestViewState,
private val session: Session
) : VectorViewModel<VerificationRequestViewState, VerificationAction>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: VerificationRequestViewState): OutgoingVerificationRequestViewModel
}
init {
withState {
val user = session.getUser(it.otherUserId)
setState {
copy(matrixItem = user?.toMatrixItem())
}
}
}
companion object : MvRxViewModelFactory<OutgoingVerificationRequestViewModel, VerificationRequestViewState> {
override fun create(viewModelContext: ViewModelContext, state: VerificationRequestViewState): OutgoingVerificationRequestViewModel? {
val fragment: OutgoingVerificationRequestFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.outgoingVerificationRequestViewModelFactory.create(state)
}
override fun initialState(viewModelContext: ViewModelContext): VerificationRequestViewState? {
val userID: String = viewModelContext.args<String>()
return VerificationRequestViewState(otherUserId = userID)
}
}
override fun handle(action: VerificationAction) {
}
}

View file

@ -0,0 +1,164 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.crypto.verification
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import butterknife.BindView
import butterknife.OnClick
import com.airbnb.mvrx.*
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.platform.parentFragmentViewModel
import kotlinx.android.synthetic.main.fragment_bottom_sas_verification_code.*
import javax.inject.Inject
class SASVerificationCodeFragment @Inject constructor(
val viewModelFactory: SASVerificationCodeViewModel.Factory
) : VectorBaseFragment() {
override fun getLayoutResId() = R.layout.fragment_bottom_sas_verification_code
@BindView(R.id.sas_emoji_grid)
lateinit var emojiGrid: ViewGroup
@BindView(R.id.sas_decimal_code)
lateinit var decimalTextView: TextView
@BindView(R.id.emoji0)
lateinit var emoji0View: ViewGroup
@BindView(R.id.emoji1)
lateinit var emoji1View: ViewGroup
@BindView(R.id.emoji2)
lateinit var emoji2View: ViewGroup
@BindView(R.id.emoji3)
lateinit var emoji3View: ViewGroup
@BindView(R.id.emoji4)
lateinit var emoji4View: ViewGroup
@BindView(R.id.emoji5)
lateinit var emoji5View: ViewGroup
@BindView(R.id.emoji6)
lateinit var emoji6View: ViewGroup
private val viewModel by fragmentViewModel(SASVerificationCodeViewModel::class)
private val sharedViewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class)
override fun invalidate() = withState(viewModel) { state ->
if (state.supportsEmoji) {
decimalTextView.isVisible = false
when(val emojiDescription = state.emojiDescription) {
is Success -> {
sasLoadingProgress.isVisible = false
emojiGrid.isVisible = true
ButtonsVisibilityGroup.isVisible = true
emojiDescription.invoke().forEachIndexed { index, emojiRepresentation ->
when (index) {
0 -> {
emoji0View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
emoji0View.findViewById<TextView>(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
}
1 -> {
emoji1View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
emoji1View.findViewById<TextView>(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
}
2 -> {
emoji2View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
emoji2View.findViewById<TextView>(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
}
3 -> {
emoji3View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
emoji3View.findViewById<TextView>(R.id.item_emoji_name_tv)?.setText(emojiRepresentation.nameResId)
}
4 -> {
emoji4View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
emoji4View.findViewById<TextView>(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
}
5 -> {
emoji5View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
emoji5View.findViewById<TextView>(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
}
6 -> {
emoji6View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
emoji6View.findViewById<TextView>(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
}
}
}
if (state.isWaitingFromOther) {
//hide buttons
ButtonsVisibilityGroup.isInvisible = true
sasCodeWaitingPartnerText.isVisible = true
} else {
ButtonsVisibilityGroup.isVisible = true
sasCodeWaitingPartnerText.isVisible = false
}
}
is Fail -> {
sasLoadingProgress.isVisible = false
emojiGrid.isInvisible = true
ButtonsVisibilityGroup.isInvisible = true
//TODO?
}
else -> {
sasLoadingProgress.isVisible = true
emojiGrid.isInvisible = true
ButtonsVisibilityGroup.isInvisible = true
}
}
} else {
//Decimal
emojiGrid.isInvisible = true
decimalTextView.isVisible = true
val decimalCode = state.decimalDescription.invoke()
decimalTextView.text = decimalCode
//TODO
if (state.isWaitingFromOther) {
//hide buttons
ButtonsVisibilityGroup.isInvisible = true
sasCodeWaitingPartnerText.isVisible = true
} else {
ButtonsVisibilityGroup.isVisible = decimalCode != null
sasCodeWaitingPartnerText.isVisible = false
}
}
}
@OnClick(R.id.sas_request_continue_button)
fun onMatchButtonTapped() = withState(viewModel) { state ->
//UX echo
ButtonsVisibilityGroup.isInvisible = true
sasCodeWaitingPartnerText.isVisible = true
sharedViewModel.handle(VerificationAction.SASMatchAction(state.otherUserId, state.transactionId))
}
@OnClick(R.id.sas_request_cancel_button)
fun onDoNotMatchButtonTapped() = withState(viewModel) { state ->
//UX echo
ButtonsVisibilityGroup.isInvisible = true
sasCodeWaitingPartnerText.isVisible = true
sharedViewModel.handle(VerificationAction.SASDoNotMatchAction(state.otherUserId, state.transactionId))
}
}

View file

@ -0,0 +1,170 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.crypto.verification
import com.airbnb.mvrx.*
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.sas.EmojiRepresentation
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.core.platform.EmptyAction
import im.vector.riotx.core.platform.VectorViewModel
data class SASVerificationCodeViewState(
val transactionId: String,
val otherUserId: String,
val otherUser: MatrixItem? = null,
val supportsEmoji: Boolean = true,
val emojiDescription: Async<List<EmojiRepresentation>> = Uninitialized,
val decimalDescription: Async<String> = Uninitialized,
val isWaitingFromOther: Boolean = false
) : MvRxState
class SASVerificationCodeViewModel @AssistedInject constructor(
@Assisted initialState: SASVerificationCodeViewState,
private val session: Session
) : VectorViewModel<SASVerificationCodeViewState, EmptyAction>(initialState)
, SasVerificationService.SasVerificationListener {
init {
withState { state ->
val matrixItem = session.getUser(state.otherUserId)?.toMatrixItem()
setState {
copy(otherUser = matrixItem)
}
val sasTx = session.getSasVerificationService()
.getExistingTransaction(state.otherUserId, state.transactionId)
if (sasTx == null) {
setState {
copy(
isWaitingFromOther = false,
emojiDescription = Fail(Throwable("Unknown Transaction")),
decimalDescription = Fail(Throwable("Unknown Transaction"))
)
}
} else {
refreshStateFromTx(sasTx)
}
}
session.getSasVerificationService().addListener(this)
}
override fun onCleared() {
session.getSasVerificationService().removeListener(this)
super.onCleared()
}
private fun refreshStateFromTx(sasTx: SasVerificationTransaction) {
when (sasTx.state) {
SasVerificationTxState.None,
SasVerificationTxState.SendingStart,
SasVerificationTxState.Started,
SasVerificationTxState.OnStarted,
SasVerificationTxState.SendingAccept,
SasVerificationTxState.Accepted,
SasVerificationTxState.OnAccepted,
SasVerificationTxState.SendingKey,
SasVerificationTxState.KeySent,
SasVerificationTxState.OnKeyReceived -> {
setState {
copy(
isWaitingFromOther = false,
supportsEmoji = sasTx.supportsEmoji(),
emojiDescription = Loading<List<EmojiRepresentation>>()
.takeIf { sasTx.supportsEmoji() }
?: Uninitialized,
decimalDescription = Loading<String>()
.takeIf { sasTx.supportsEmoji().not() }
?: Uninitialized
)
}
}
SasVerificationTxState.ShortCodeReady -> {
setState {
copy(
isWaitingFromOther = false,
supportsEmoji = sasTx.supportsEmoji(),
emojiDescription = if (sasTx.supportsEmoji()) Success(sasTx.getEmojiCodeRepresentation())
else Uninitialized,
decimalDescription = if (!sasTx.supportsEmoji()) Success(sasTx.getDecimalCodeRepresentation())
else Uninitialized
)
}
}
SasVerificationTxState.ShortCodeAccepted,
SasVerificationTxState.SendingMac,
SasVerificationTxState.MacSent,
SasVerificationTxState.Verifying,
SasVerificationTxState.Verified -> {
setState {
copy(isWaitingFromOther = true)
}
}
SasVerificationTxState.Cancelled,
SasVerificationTxState.OnCancelled -> {
// The fragment should not be rendered in this state,
// it should have been replaced by a conclusion fragment
setState {
copy(
isWaitingFromOther = false,
supportsEmoji = sasTx.supportsEmoji(),
emojiDescription = Fail(Throwable("Transaction Cancelled")),
decimalDescription = Fail(Throwable("Transaction Cancelled"))
)
}
}
}
}
override fun transactionCreated(tx: SasVerificationTransaction) {
transactionUpdated(tx)
}
override fun transactionUpdated(tx: SasVerificationTransaction) {
refreshStateFromTx(tx)
}
@AssistedInject.Factory
interface Factory {
fun create(initialState: SASVerificationCodeViewState): SASVerificationCodeViewModel
}
companion object : MvRxViewModelFactory<SASVerificationCodeViewModel, SASVerificationCodeViewState> {
override fun create(viewModelContext: ViewModelContext, state: SASVerificationCodeViewState): SASVerificationCodeViewModel? {
val factory = (viewModelContext as FragmentViewModelContext).fragment<SASVerificationCodeFragment>().viewModelFactory
return factory.create(state)
}
override fun initialState(viewModelContext: ViewModelContext): SASVerificationCodeViewState? {
val args = viewModelContext.args<VerificationBottomSheet.VerificationArgs>()
return SASVerificationCodeViewState(
transactionId = args.verificationId ?: "",
otherUserId = args.otherUserId
)
}
}
override fun handle(action: EmptyAction) {
}
}

View file

@ -91,7 +91,7 @@ class SASVerificationStartFragment @Inject constructor(): VectorBaseFragment() {
(requireActivity() as VectorBaseActivity).notImplemented()
/*
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 {

View file

@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.sas.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

View file

@ -1,6 +1,22 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.crypto.verification
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<out Fragment>, bundle: Bundle) {
if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) {
// choose method
bottomSheetFragmentContainer.getParentCoordinatorLayout()?.let { coordinatorLayout ->
TransitionManager.beginDelayedTransition(coordinatorLayout, AutoTransition().apply { duration = 150 })
}
// Commit now, to ensure changes occurs before next rendering frame (or bottomsheet want animate)
childFragmentManager.commitTransactionNow {
replace(R.id.bottomSheetFragmentContainer,
fragmentClass.java,
bundle,
fragmentClass.simpleName
)
}
}
}
}
fun Fragment.getParentCoordinatorLayout(): CoordinatorLayout? {
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

View file

@ -1,50 +1,178 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.crypto.verification
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<TimelineEvent> = 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<VerificationBottomSheetViewState, VerificationAction>(initialState) {
: VectorViewModel<VerificationBottomSheetViewState, VerificationAction>(initialState),
SasVerificationService.SasVerificationListener {
// Can be used for several actions, for a one shot result
private val _requestLiveData = MutableLiveData<LiveEvent<Async<VerificationAction>>>()
val requestLiveData: LiveData<LiveEvent<Async<VerificationAction>>>
get() = _requestLiveData
init {
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<VerificationBottomSheetViewModel, VerificationBottomSheetViewState> {
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)
}
}
}
}

View file

@ -1,37 +1,69 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.crypto.verification
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
?: ""))
}
}

View file

@ -0,0 +1,77 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.crypto.verification
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart
import im.vector.riotx.core.platform.EmptyAction
import im.vector.riotx.core.platform.VectorViewModel
data class VerificationChooseMethodViewState(
val otherUserId: String = "",
val transactionId: String = "",
val QRModeAvailable: Boolean = false,
val SASMOdeAvailable: Boolean = false
) : MvRxState
class VerificationChooseMethodViewModel @AssistedInject constructor(
@Assisted initialState: VerificationChooseMethodViewState,
private val session: Session
) : VectorViewModel<VerificationChooseMethodViewState, EmptyAction>(initialState) {
init {
withState { state ->
val pvr = session.getSasVerificationService().getExistingVerificationRequest(state.otherUserId)?.first {
it.transactionId == state.transactionId
}
val qrAvailable = pvr?.readyInfo?.methods?.contains(KeyVerificationStart.VERIF_METHOD_SCAN) ?: false
val emojiAvailable = pvr?.readyInfo?.methods?.contains(KeyVerificationStart.VERIF_METHOD_SAS) ?: false
setState {
copy(QRModeAvailable = qrAvailable, SASMOdeAvailable = emojiAvailable)
}
}
}
@AssistedInject.Factory
interface Factory {
fun create(initialState: VerificationChooseMethodViewState): VerificationChooseMethodViewModel
}
companion object : MvRxViewModelFactory<VerificationChooseMethodViewModel, VerificationChooseMethodViewState> {
override fun create(viewModelContext: ViewModelContext, state: VerificationChooseMethodViewState): VerificationChooseMethodViewModel? {
val fragment: VerificationChooseMethodFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.verificationChooseMethodViewModelFactory.create(state)
}
override fun initialState(viewModelContext: ViewModelContext): VerificationChooseMethodViewState? {
val args: VerificationBottomSheet.VerificationArgs = viewModelContext.args()
return VerificationChooseMethodViewState(otherUserId = args.otherUserId, transactionId = args.verificationId ?: "")
}
}
override fun handle(action: EmptyAction) {}
}

View file

@ -0,0 +1,73 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.crypto.verification
import android.os.Parcelable
import androidx.core.content.ContextCompat
import butterknife.OnClick
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotx.R
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.platform.parentFragmentViewModel
import io.noties.markwon.Markwon
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_verification_conclusion.*
import javax.inject.Inject
class VerificationConclusionFragment @Inject constructor() : VectorBaseFragment() {
@Parcelize
data class Args(
val isSuccessFull: Boolean,
val cancelReason: String?
) : Parcelable
override fun getLayoutResId() = R.layout.fragment_verification_conclusion
private val sharedViewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class)
private val viewModel by fragmentViewModel(VerificationConclusionViewModel::class)
override fun invalidate() = withState(viewModel) {
when (it.conclusionState) {
ConclusionState.SUCCESS -> {
verificationConclusionTitle.text = getString(R.string.sas_verified)
verifyConclusionDescription.setTextOrHide(getString(R.string.sas_verified_successful_description))
verifyConclusionBottomDescription.text = getString(R.string.verification_green_shield)
verifyConclusionImageView.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_shield_trusted))
}
ConclusionState.WARNING -> {
verificationConclusionTitle.text = getString(R.string.verification_conclusion_not_secure)
verifyConclusionDescription.setTextOrHide(null)
verifyConclusionImageView.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_shield_warning))
verifyConclusionBottomDescription.text = Markwon.builder(requireContext()).build().toMarkdown(getString(R.string.verification_conclusion_compromised))
}
ConclusionState.CANCELLED -> {
// Just dismiss in this case
sharedViewModel.handle(VerificationAction.GotItConclusion)
}
}
}
@OnClick(R.id.verificationConclusionButton)
fun onButtonTapped() {
sharedViewModel.handle(VerificationAction.GotItConclusion)
}
}

View file

@ -0,0 +1,61 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.crypto.verification
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
import im.vector.matrix.android.api.session.crypto.sas.safeValueOf
import im.vector.riotx.core.platform.EmptyAction
import im.vector.riotx.core.platform.VectorViewModel
data class SASVerificationConclusionViewState(
val conclusionState: ConclusionState = ConclusionState.CANCELLED
) : MvRxState
enum class ConclusionState {
SUCCESS,
WARNING,
CANCELLED
}
class VerificationConclusionViewModel(initialState: SASVerificationConclusionViewState)
: VectorViewModel<SASVerificationConclusionViewState, EmptyAction>(initialState) {
companion object : MvRxViewModelFactory<VerificationConclusionViewModel, SASVerificationConclusionViewState> {
override fun initialState(viewModelContext: ViewModelContext): SASVerificationConclusionViewState? {
val args = viewModelContext.args<VerificationConclusionFragment.Args>()
return when (safeValueOf(args.cancelReason)) {
CancelCode.MismatchedSas,
CancelCode.MismatchedCommitment,
CancelCode.MismatchedKeys -> {
SASVerificationConclusionViewState(ConclusionState.WARNING)
}
else -> {
SASVerificationConclusionViewState(
if (args.isSuccessFull) ConclusionState.SUCCESS
else ConclusionState.CANCELLED
)
}
}
}
}
override fun handle(action: EmptyAction) {}
}

View file

@ -16,16 +16,14 @@
package im.vector.riotx.features.crypto.verification
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))
}
}

View file

@ -0,0 +1,109 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.crypto.verification
import com.airbnb.mvrx.*
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest
import im.vector.riotx.core.di.HasScreenInjector
import im.vector.riotx.core.platform.VectorViewModel
data class VerificationRequestViewState(
val roomId: String? = null,
val matrixItem: MatrixItem,
val started: Async<Boolean> = Success(false)
) : MvRxState
class VerificationRequestViewModel @AssistedInject constructor(
@Assisted initialState: VerificationRequestViewState,
private val session: Session
) : VectorViewModel<VerificationRequestViewState, VerificationAction>(initialState), SasVerificationService.SasVerificationListener {
@AssistedInject.Factory
interface Factory {
fun create(initialState: VerificationRequestViewState): VerificationRequestViewModel
}
init {
withState {
val pr = session.getSasVerificationService()
.getExistingVerificationRequest(it.matrixItem.id)
?.firstOrNull()
setState {
copy(
started = Success(false).takeIf { pr == null }
?: Success(true).takeIf { pr?.isReady == true }
?: Loading<Boolean>()
)
}
}
session.getSasVerificationService().addListener(this)
}
override fun onCleared() {
session.getSasVerificationService().removeListener(this)
super.onCleared()
}
companion object : MvRxViewModelFactory<VerificationRequestViewModel, VerificationRequestViewState> {
override fun create(viewModelContext: ViewModelContext, state: VerificationRequestViewState): VerificationRequestViewModel? {
val fragment: VerificationRequestFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.verificationRequestViewModelFactory.create(state)
}
override fun initialState(viewModelContext: ViewModelContext): VerificationRequestViewState? {
val otherUserId = viewModelContext.args<VerificationBottomSheet.VerificationArgs>().otherUserId
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
return session.getUser(otherUserId)?.let {
VerificationRequestViewState(matrixItem = it.toMatrixItem())
}
}
}
override fun handle(action: VerificationAction) {
}
override fun transactionCreated(tx: SasVerificationTransaction) {}
override fun transactionUpdated(tx: SasVerificationTransaction) {}
override fun verificationRequestCreated(pr: PendingVerificationRequest) {
verificationRequestUpdated(pr)
}
override fun verificationRequestUpdated(pr: PendingVerificationRequest) = withState { state ->
if (pr.otherUserId == state.matrixItem.id) {
if (pr.isReady) {
setState {
copy(started = Success(true))
}
} else {
setState {
copy(started = Loading())
}
}
}
}
}

View file

@ -964,7 +964,18 @@ class RoomDetailFragment @Inject constructor(
}
is RoomDetailAction.RequestVerification -> {
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) {

View file

@ -49,7 +49,6 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent
import im.vector.matrix.android.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()

View file

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

View file

@ -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

View file

@ -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")

View file

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

View file

@ -0,0 +1,195 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:colorBackground">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/sas_emoji_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/layout_horizontal_margin"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:text="@string/verify_by_emoji_title"
android:textColor="?riotx_text_primary"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/sas_emoji_description_2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/layout_vertical_margin"
android:text="@string/verify_user_sas_emoji_help_text"
android:textColor="?riotx_text_secondary"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/sas_emoji_description" />
<TextView
android:id="@+id/sas_decimal_code"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textColor="?riotx_text_primary"
android:textSize="28sp"
android:textStyle="bold"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@+id/sas_emoji_grid"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/sas_emoji_grid"
tools:text="1234-4320-3905"
tools:visibility="visible" />
<ProgressBar
android:id="@+id/sasLoadingProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
app:layout_constraintBottom_toBottomOf="@+id/sas_emoji_grid"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/sas_emoji_grid"
/>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/sas_emoji_grid"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/layout_vertical_margin"
android:visibility="invisible"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/sas_emoji_description_2"
tools:visibility="visible">
<include
android:id="@+id/emoji0"
layout="@layout/item_emoji_verif" />
<include
android:id="@+id/emoji1"
layout="@layout/item_emoji_verif" />
<include
android:id="@+id/emoji2"
layout="@layout/item_emoji_verif" />
<include
android:id="@+id/emoji3"
layout="@layout/item_emoji_verif" />
<include
android:id="@+id/emoji4"
layout="@layout/item_emoji_verif" />
<include
android:id="@+id/emoji5"
layout="@layout/item_emoji_verif" />
<include
android:id="@+id/emoji6"
layout="@layout/item_emoji_verif" />
<androidx.constraintlayout.helper.widget.Flow
android:id="@+id/sas_emoji_grid_flow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:constraint_referenced_ids="emoji0,emoji1,emoji2,emoji3,emoji4,emoji5,emoji6"
app:flow_horizontalBias="0.5"
app:flow_horizontalGap="16dp"
app:flow_horizontalStyle="packed"
app:flow_verticalBias="0"
app:flow_verticalGap="8dp"
app:flow_wrapMode="chain"
tools:ignore="MissingConstraints" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/sas_request_continue_button"
style="@style/VectorButtonStylePositive"
android:layout_width="0dp"
android:layout_margin="@dimen/layout_vertical_margin"
android:minWidth="160dp"
android:text="@string/verification_sas_match"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/centerGuideLine"
app:layout_constraintTop_toBottomOf="@+id/sas_emoji_grid"
tools:text="A very long translation thats too big" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/centerGuideLine"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
<com.google.android.material.button.MaterialButton
android:id="@+id/sas_request_cancel_button"
style="@style/VectorButtonStyleDestructive"
android:layout_width="0dp"
android:layout_margin="@dimen/layout_vertical_margin"
android:text="@string/verification_sas_do_not_match"
app:layout_constraintEnd_toStartOf="@+id/centerGuideLine"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/sas_emoji_grid" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/bottomBarrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:barrierMargin="8dp"
app:constraint_referenced_ids="sas_request_cancel_button,sas_request_continue_button" />
<TextView
android:id="@+id/sasCodeWaitingPartnerText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="?attr/vctr_notice_secondary"
android:textSize="15sp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/bottomBarrier"
app:layout_constraintTop_toBottomOf="@id/sas_emoji_grid"
android:text="@string/sas_waiting_for_partner"
tools:visibility="visible" />
<TextView
android:id="@+id/sasEmojiSecurityTip"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/layout_vertical_margin"
android:gravity="center"
android:text="@string/verify_user_sas_emoji_security_tip"
android:textColor="?riotx_text_secondary"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/bottomBarrier" />
<androidx.constraintlayout.widget.Group
android:id="@+id/ButtonsVisibilityGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="sas_request_continue_button,sas_request_cancel_button"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View file

@ -97,14 +97,15 @@
style="@style/VectorButtonStylePositive"
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" />
<androidx.constraintlayout.widget.Group
android:id="@+id/verifyQRGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
android:visibility="gone"
tools:visibility="visible"
app:constraint_referenced_ids="verifyQRDescription,verificationQRTitle,verifyQRImageView" />
<androidx.constraintlayout.widget.Group

View file

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/verificationConclusionTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/sas_verified"
tools:text="@string/sas_verified"
android:textColor="?riotx_text_primary"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/verifyConclusionDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
tools:text="@string/sas_verified_successful_description"
android:text="@string/sas_verified_successful_description"
android:textColor="?riotx_text_secondary"
app:layout_constraintTop_toBottomOf="@id/verificationConclusionTitle"/>
<ImageView
android:id="@+id/verifyConclusionImageView"
android:layout_width="180dp"
android:layout_height="180dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
tools:background="@drawable/ic_shield_trusted"
android:background="@drawable/ic_shield_trusted"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/verifyConclusionDescription" />
<TextView
android:id="@+id/verifyConclusionBottomDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
tools:text="@string/verification_green_shield"
android:text="@string/verification_green_shield"
android:textColor="?riotx_text_secondary"
app:layout_constraintTop_toBottomOf="@id/verifyConclusionImageView"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/verificationConclusionButton"
style="@style/VectorButtonStylePositive"
android:layout_width="match_parent"
android:layout_marginTop="16dp"
android:text="@string/sas_got_it"
app:layout_constraintTop_toBottomOf="@id/verifyConclusionBottomDescription" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1500,7 +1500,7 @@ Why choose Riot.im?
<string name="sas_verified">Verified!</string>
<string name="sas_verified_successful">You\'ve successfully verified this device.</string>
<string name="sas_verified_successful_description">Secure messages with this user are end-to-end encrypted and not able to be read by third parties.</string>
<string name="sas_verified_successful_description">Messages with this user in this room are end-to-end encrypted and cant be read by third parties.</string>
<string name="sas_got_it">Got it</string>
<string name="sas_verifying_keys">Nothing appearing? Not all clients supports interactive verification yet. Use legacy verification.</string>

View file

@ -2,16 +2,28 @@
<resources>
<!-- Strings not defined in Riot -->
<string name="command_description_verify">Request to verify the given userID</string>
<string name="command_description_shrug">Prepends ¯\\_(ツ)_/¯ to a plain-text message</string>
<string name="notification_initial_sync">Initial Sync…</string>
<string name="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 dont match</string>
<string name="verify_user_sas_emoji_help_text">Verify this user by confirming the following unique emoji appear on their screen, in the same order."</string>
<string name="verify_user_sas_emoji_security_tip">For ultimate security, use another trusted means of communication or do this in person.</string>
<string name="verification_green_shield">Look for the green shield to ensure a user is trusted. Trust all users in a room to ensure the room is secure.</string>
<string name="verification_conclusion_not_secure">Not secure</string>
<string name="verification_conclusion_compromised">One of the following may be compromised:\n\n - Your homeserver\n - The homeserver the user youre verifying is connected to\n - Yours, or the other users internet connection\n - Yours, or the other users device
</string>
<string name="sent_a_video">Video.</string>
<string name="sent_an_image">Image.</string>
<string name="sent_an_audio_file">Audio</string>
<string name="sent_a_file">File</string>
<string name="verification_request_waiting">Waiting…</string>
<string name="verification_request_other_cancelled">%s cancelled</string>
<string name="verification_request_you_cancelled">You cancelled</string>