Merge branch 'develop' into feature/fix_big_image_rotation

This commit is contained in:
Onuray Sahin 2020-02-21 13:13:02 +03:00 committed by GitHub
commit 1eefda18e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 803 additions and 668 deletions

View File

@ -9,13 +9,15 @@ Features ✨:
- Sending image: image are sent to rooms with a reduced size. It's still possible to send original image file (#1010)
Improvements 🙌:
-
- Migrate to binary QR code verification (#994)
Bugfix 🐛:
- Account creation: wrongly hints that an email can be used to create an account (#941)
- Fix crash in the room directory, when public room has no name (#1023)
- Fix restoring keys backup with passphrase (#526)
- Fix rotation of full-size image (#647)
- Fix joining rooms from directory via federation isn't working. (#808)
- Leaving a room creates a stuck "leaving room" loading screen. (#1041)
Translations 🗣:
-

View File

@ -95,10 +95,10 @@ class RxSession(private val session: Session) {
session.searchUsersDirectory(search, limit, excludedUserIds, it)
}
fun joinRoom(roomId: String,
fun joinRoom(roomIdOrAlias: String,
reason: String? = null,
viaServers: List<String> = emptyList()): Single<Unit> = singleBuilder {
session.joinRoom(roomId, reason, viaServers, it)
session.joinRoom(roomIdOrAlias, reason, viaServers, it)
}
fun getRoomIdByAlias(roomAlias: String,

View File

@ -0,0 +1,35 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.verification.qrcode
fun hexToByteArray(hex: String): ByteArray {
// Remove all spaces
return hex.replace(" ", "")
.let {
if (it.length % 2 != 0) "0$it" else it
}
.let {
ByteArray(it.length / 2)
.apply {
for (i in this.indices) {
val index = i * 2
val v = it.substring(index, index + 2).toInt(16)
this[i] = v.toByte()
}
}
}
}

View File

@ -0,0 +1,249 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.verification.qrcode
import androidx.test.ext.junit.runners.AndroidJUnit4
import im.vector.matrix.android.InstrumentedTest
import org.amshove.kluent.shouldBeNull
import org.amshove.kluent.shouldEqual
import org.amshove.kluent.shouldEqualTo
import org.amshove.kluent.shouldNotBeNull
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
class QrCodeTest : InstrumentedTest {
private val qrCode1 = QrCodeData.VerifyingAnotherUser(
transactionId = "MaTransaction",
userMasterCrossSigningPublicKey = "ktEwcUP6su1xh+GuE+CYkQ3H6W/DIl+ybHFdaEOrolU",
otherUserMasterCrossSigningPublicKey = "TXluZKTZLvSRWOTPlOqLq534bA+/K4zLFKSu9cGLQaU",
sharedSecret = "MTIzNDU2Nzg"
)
private val value1 = "MATRIX\u0002\u0000\u0000\u000DMaTransaction\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢UMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008BA¥12345678"
private val qrCode2 = QrCodeData.SelfVerifyingMasterKeyTrusted(
transactionId = "MaTransaction",
userMasterCrossSigningPublicKey = "ktEwcUP6su1xh+GuE+CYkQ3H6W/DIl+ybHFdaEOrolU",
otherDeviceKey = "TXluZKTZLvSRWOTPlOqLq534bA+/K4zLFKSu9cGLQaU",
sharedSecret = "MTIzNDU2Nzg"
)
private val value2 = "MATRIX\u0002\u0001\u0000\u000DMaTransaction\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢UMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008BA¥12345678"
private val qrCode3 = QrCodeData.SelfVerifyingMasterKeyNotTrusted(
transactionId = "MaTransaction",
deviceKey = "TXluZKTZLvSRWOTPlOqLq534bA+/K4zLFKSu9cGLQaU",
userMasterCrossSigningPublicKey = "ktEwcUP6su1xh+GuE+CYkQ3H6W/DIl+ybHFdaEOrolU",
sharedSecret = "MTIzNDU2Nzg"
)
private val value3 = "MATRIX\u0002\u0002\u0000\u000DMaTransactionMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008B\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢U12345678"
private val sharedSecretByteArray = "12345678".toByteArray(Charsets.ISO_8859_1)
private val tlx_byteArray = hexToByteArray("4d 79 6e 64 a4 d9 2e f4 91 58 e4 cf 94 ea 8b ab 9d f8 6c 0f bf 2b 8c cb 14 a4 ae f5 c1 8b 41 a5")
private val kte_byteArray = hexToByteArray("92 d1 30 71 43 fa b2 ed 71 87 e1 ae 13 e0 98 91 0d c7 e9 6f c3 22 5f b2 6c 71 5d 68 43 ab a2 55")
@Test
fun testEncoding1() {
qrCode1.toEncodedString() shouldEqual value1
}
@Test
fun testEncoding2() {
qrCode2.toEncodedString() shouldEqual value2
}
@Test
fun testEncoding3() {
qrCode3.toEncodedString() shouldEqual value3
}
@Test
fun testSymmetry1() {
qrCode1.toEncodedString().toQrCodeData() shouldEqual qrCode1
}
@Test
fun testSymmetry2() {
qrCode2.toEncodedString().toQrCodeData() shouldEqual qrCode2
}
@Test
fun testSymmetry3() {
qrCode3.toEncodedString().toQrCodeData() shouldEqual qrCode3
}
@Test
fun testCase1() {
val url = qrCode1.toEncodedString()
val byteArray = url.toByteArray(Charsets.ISO_8859_1)
checkHeader(byteArray)
// Mode
byteArray[7] shouldEqualTo 0
checkSizeAndTransaction(byteArray)
compareArray(byteArray.copyOfRange(23, 23 + 32), kte_byteArray)
compareArray(byteArray.copyOfRange(23 + 32, 23 + 64), tlx_byteArray)
compareArray(byteArray.copyOfRange(23 + 64, byteArray.size), sharedSecretByteArray)
}
@Test
fun testCase2() {
val url = qrCode2.toEncodedString()
val byteArray = url.toByteArray(Charsets.ISO_8859_1)
checkHeader(byteArray)
// Mode
byteArray[7] shouldEqualTo 1
checkSizeAndTransaction(byteArray)
compareArray(byteArray.copyOfRange(23, 23 + 32), kte_byteArray)
compareArray(byteArray.copyOfRange(23 + 32, 23 + 64), tlx_byteArray)
compareArray(byteArray.copyOfRange(23 + 64, byteArray.size), sharedSecretByteArray)
}
@Test
fun testCase3() {
val url = qrCode3.toEncodedString()
val byteArray = url.toByteArray(Charsets.ISO_8859_1)
checkHeader(byteArray)
// Mode
byteArray[7] shouldEqualTo 2
checkSizeAndTransaction(byteArray)
compareArray(byteArray.copyOfRange(23, 23 + 32), tlx_byteArray)
compareArray(byteArray.copyOfRange(23 + 32, 23 + 64), kte_byteArray)
compareArray(byteArray.copyOfRange(23 + 64, byteArray.size), sharedSecretByteArray)
}
@Test
fun testLongTransactionId() {
// Size on two bytes (2_000 = 0x07D0)
val longTransactionId = "PatternId_".repeat(200)
val qrCode = qrCode1.copy(transactionId = longTransactionId)
val result = qrCode.toEncodedString()
val expected = value1.replace("\u0000\u000DMaTransaction", "\u0007\u00D0$longTransactionId")
result shouldEqual expected
// Reverse operation
expected.toQrCodeData() shouldEqual qrCode
}
@Test
fun testAnyTransactionId() {
for (qty in 0 until 0x1FFF step 200) {
val longTransactionId = "a".repeat(qty)
val qrCode = qrCode1.copy(transactionId = longTransactionId)
// Symmetric operation
qrCode.toEncodedString().toQrCodeData() shouldEqual qrCode
}
}
// Error cases
@Test
fun testErrorHeader() {
value1.replace("MATRIX", "MOTRIX").toQrCodeData().shouldBeNull()
value1.replace("MATRIX", "MATRI").toQrCodeData().shouldBeNull()
value1.replace("MATRIX", "").toQrCodeData().shouldBeNull()
}
@Test
fun testErrorVersion() {
value1.replace("MATRIX\u0002", "MATRIX\u0000").toQrCodeData().shouldBeNull()
value1.replace("MATRIX\u0002", "MATRIX\u0001").toQrCodeData().shouldBeNull()
value1.replace("MATRIX\u0002", "MATRIX\u0003").toQrCodeData().shouldBeNull()
value1.replace("MATRIX\u0002", "MATRIX").toQrCodeData().shouldBeNull()
}
@Test
fun testErrorSecretTooShort() {
value1.replace("12345678", "1234567").toQrCodeData().shouldBeNull()
}
@Test
fun testErrorNoTransactionNoKeyNoSecret() {
// But keep transaction length
"MATRIX\u0002\u0000\u0000\u000D".toQrCodeData().shouldBeNull()
}
@Test
fun testErrorNoKeyNoSecret() {
"MATRIX\u0002\u0000\u0000\u000DMaTransaction".toQrCodeData().shouldBeNull()
}
@Test
fun testErrorTransactionLengthTooShort() {
// In this case, the secret will be longer, so this is not an error, but it will lead to keys mismatch
value1.replace("\u000DMaTransaction", "\u000CMaTransaction").toQrCodeData().shouldNotBeNull()
}
@Test
fun testErrorTransactionLengthTooBig() {
value1.replace("\u000DMaTransaction", "\u000EMaTransaction").toQrCodeData().shouldBeNull()
}
private fun compareArray(actual: ByteArray, expected: ByteArray) {
actual.size shouldEqual expected.size
for (i in actual.indices) {
actual[i] shouldEqualTo expected[i]
}
}
private fun checkHeader(byteArray: ByteArray) {
// MATRIX
byteArray[0] shouldEqualTo 'M'.toByte()
byteArray[1] shouldEqualTo 'A'.toByte()
byteArray[2] shouldEqualTo 'T'.toByte()
byteArray[3] shouldEqualTo 'R'.toByte()
byteArray[4] shouldEqualTo 'I'.toByte()
byteArray[5] shouldEqualTo 'X'.toByte()
// Version
byteArray[6] shouldEqualTo 2
}
private fun checkSizeAndTransaction(byteArray: ByteArray) {
// Size
byteArray[8] shouldEqualTo 0
byteArray[9] shouldEqualTo 13
// Transaction
byteArray.copyOfRange(10, 10 + "MaTransaction".length).toString(Charsets.ISO_8859_1) shouldEqual "MaTransaction"
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 New Vector Ltd
* Copyright (c) 2020 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.
@ -32,14 +32,14 @@ class SharedSecretTest : InstrumentedTest {
@Test
fun testSharedSecretLengthCase() {
repeat(100) {
generateSharedSecret().length shouldBe 43
generateSharedSecretV2().length shouldBe 11
}
}
@Test
fun testSharedDiffCase() {
val sharedSecret1 = generateSharedSecret()
val sharedSecret2 = generateSharedSecret()
val sharedSecret1 = generateSharedSecretV2()
val sharedSecret2 = generateSharedSecretV2()
sharedSecret1 shouldNotBeEqualTo sharedSecret2
}

View File

@ -144,14 +144,6 @@ object MatrixPatterns {
* @return null if not found or if matrixId is null
*/
fun extractServerNameFromId(matrixId: String?): String? {
if (matrixId == null) {
return null
}
val index = matrixId.indexOf(":")
return if (index == -1) {
null
} else matrixId.substring(index + 1)
return matrixId?.substringAfter(":", missingDelimiterValue = "")?.takeIf { it.isNotEmpty() }
}
}

View File

@ -35,9 +35,9 @@ interface RoomDirectoryService {
callback: MatrixCallback<PublicRoomsResponse>): Cancelable
/**
* Join a room by id
* Join a room by id, or room alias
*/
fun joinRoom(roomId: String,
fun joinRoom(roomIdOrAlias: String,
reason: String? = null,
callback: MatrixCallback<Unit>): Cancelable

View File

@ -36,11 +36,11 @@ interface RoomService {
/**
* Join a room by id
* @param roomId the roomId of the room to join
* @param roomIdOrAlias the roomId or the room alias of the room to join
* @param reason optional reason for joining the room
* @param viaServers the servers to attempt to join the room through. One of the servers must be participating in the room.
*/
fun joinRoom(roomId: String,
fun joinRoom(roomIdOrAlias: String,
reason: String? = null,
viaServers: List<String> = emptyList(),
callback: MatrixCallback<Unit>): Cancelable

View File

@ -17,6 +17,7 @@ package im.vector.matrix.android.api.session.room.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.session.room.VerificationState
/**
* Contains an aggregated summary info of the references.
@ -26,6 +27,6 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class ReferencesAggregatedContent(
// Verification status info for m.key.verification.request msgType events
@Json(name = "verif_sum") val verificationSummary: String
@Json(name = "verif_sum") val verificationState: VerificationState
// Add more fields for future summary info.
)

View File

@ -21,5 +21,10 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class CreateRoomResponse(
@Json(name = "room_id") var roomId: String? = null
/**
* Required. The created room's ID.
*/
@Json(name = "room_id") var roomId: String
)
internal typealias JoinRoomResponse = CreateRoomResponse

View File

@ -23,66 +23,71 @@ import com.squareup.moshi.JsonClass
*/
@JsonClass(generateAdapter = true)
data class PublicRoom(
/**
* Aliases of the room. May be empty.
*/
@Json(name = "aliases")
var aliases: List<String>? = null,
val aliases: List<String>? = null,
/**
* The canonical alias of the room, if any.
*/
@Json(name = "canonical_alias")
var canonicalAlias: String? = null,
val canonicalAlias: String? = null,
/**
* The name of the room, if any.
*/
@Json(name = "name")
var name: String? = null,
val name: String? = null,
/**
* Required. The number of members joined to the room.
*/
@Json(name = "num_joined_members")
var numJoinedMembers: Int = 0,
val numJoinedMembers: Int = 0,
/**
* Required. The ID of the room.
*/
@Json(name = "room_id")
var roomId: String,
val roomId: String,
/**
* The topic of the room, if any.
*/
@Json(name = "topic")
var topic: String? = null,
val topic: String? = null,
/**
* Required. Whether the room may be viewed by guest users without joining.
*/
@Json(name = "world_readable")
var worldReadable: Boolean = false,
val worldReadable: Boolean = false,
/**
* Required. Whether guest users may join the room and participate in it. If they can,
* they will be subject to ordinary power level rules like any other user.
*/
@Json(name = "guest_can_join")
var guestCanJoin: Boolean = false,
val guestCanJoin: Boolean = false,
/**
* The URL for the room's avatar, if one is set.
*/
@Json(name = "avatar_url")
var avatarUrl: String? = null,
val avatarUrl: String? = null,
/**
* Undocumented item
*/
@Json(name = "m.federate")
var isFederated: Boolean = false
)
val isFederated: Boolean = false
) {
/**
* Return the canonical alias, or the first alias from the list of aliases, or null
*/
fun getPrimaryAlias(): String? {
return canonicalAlias ?: aliases?.firstOrNull()
}
}

View File

@ -23,6 +23,13 @@ import com.squareup.moshi.JsonClass
*/
@JsonClass(generateAdapter = true)
data class RoomTombstoneContent(
/**
* Required. A server-defined message.
*/
@Json(name = "body") val body: String? = null,
@Json(name = "replacement_room") val replacementRoom: String?
/**
* Required. The new room the client should be visiting.
*/
@Json(name = "replacement_room") val replacementRoomId: String?
)

View File

@ -151,6 +151,6 @@ fun RoomSummary.toMatrixItem() = MatrixItem.RoomItem(roomId, displayName, avatar
fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlias ?: roomId, displayName, avatarUrl)
// If no name is available, use room alias as Riot-Web does
fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: canonicalAlias, avatarUrl)
fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: getPrimaryAlias() ?: "", avatarUrl)
fun RoomMemberSummary.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)

View File

@ -66,7 +66,7 @@ import im.vector.matrix.android.internal.crypto.model.rest.toValue
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.verification.qrcode.DefaultQrCodeVerificationTransaction
import im.vector.matrix.android.internal.crypto.verification.qrcode.QrCodeData
import im.vector.matrix.android.internal.crypto.verification.qrcode.generateSharedSecret
import im.vector.matrix.android.internal.crypto.verification.qrcode.generateSharedSecretV2
import im.vector.matrix.android.internal.di.DeviceId
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.session.SessionScope
@ -796,17 +796,17 @@ internal class DefaultVerificationService @Inject constructor(
return when {
userId != otherUserId ->
createQrCodeDataForDistinctUser(requestId, otherUserId, otherDeviceId)
createQrCodeDataForDistinctUser(requestId, otherUserId)
crossSigningService.isCrossSigningVerified() ->
// This is a self verification and I am the old device (Osborne2)
createQrCodeDataForVerifiedDevice(requestId, otherDeviceId)
else ->
// This is a self verification and I am the new device (Dynabook)
createQrCodeDataForUnVerifiedDevice(requestId, otherDeviceId)
createQrCodeDataForUnVerifiedDevice(requestId)
}
}
private fun createQrCodeDataForDistinctUser(requestId: String, otherUserId: String, otherDeviceId: String?): QrCodeData? {
private fun createQrCodeDataForDistinctUser(requestId: String, otherUserId: String): QrCodeData.VerifyingAnotherUser? {
val myMasterKey = crossSigningService.getMyCrossSigningKeys()
?.masterKey()
?.unpaddedBase64PublicKey
@ -823,39 +823,16 @@ internal class DefaultVerificationService @Inject constructor(
return null
}
val myDeviceId = deviceId
?: run {
Timber.w("## Unable to get my deviceId")
return null
}
val myDeviceKey = myDeviceInfoHolder.get().myDevice.fingerprint()
?: run {
Timber.w("## Unable to get my fingerprint")
return null
}
val otherDeviceKey = otherDeviceId
?.let {
cryptoStore.getUserDevice(userId, otherDeviceId)?.fingerprint()
}
return QrCodeData(
userId = userId,
requestId = requestId,
action = QrCodeData.ACTION_VERIFY,
keys = hashMapOf(
myMasterKey to myMasterKey,
myDeviceId to myDeviceKey
),
sharedSecret = generateSharedSecret(),
otherUserKey = otherUserMasterKey,
otherDeviceKey = otherDeviceKey
return QrCodeData.VerifyingAnotherUser(
transactionId = requestId,
userMasterCrossSigningPublicKey = myMasterKey,
otherUserMasterCrossSigningPublicKey = otherUserMasterKey,
sharedSecret = generateSharedSecretV2()
)
}
// Create a QR code to display on the old device (Osborne2)
private fun createQrCodeDataForVerifiedDevice(requestId: String, otherDeviceId: String?): QrCodeData? {
private fun createQrCodeDataForVerifiedDevice(requestId: String, otherDeviceId: String?): QrCodeData.SelfVerifyingMasterKeyTrusted? {
val myMasterKey = crossSigningService.getMyCrossSigningKeys()
?.masterKey()
?.unpaddedBase64PublicKey
@ -873,34 +850,16 @@ internal class DefaultVerificationService @Inject constructor(
return null
}
val myDeviceId = deviceId
?: run {
Timber.w("## Unable to get my deviceId")
return null
}
val myDeviceKey = myDeviceInfoHolder.get().myDevice.fingerprint()
?: run {
Timber.w("## Unable to get my fingerprint")
return null
}
return QrCodeData(
userId = userId,
requestId = requestId,
action = QrCodeData.ACTION_VERIFY,
keys = hashMapOf(
myMasterKey to myMasterKey,
myDeviceId to myDeviceKey
),
sharedSecret = generateSharedSecret(),
otherUserKey = null,
otherDeviceKey = otherDeviceKey
return QrCodeData.SelfVerifyingMasterKeyTrusted(
transactionId = requestId,
userMasterCrossSigningPublicKey = myMasterKey,
otherDeviceKey = otherDeviceKey,
sharedSecret = generateSharedSecretV2()
)
}
// Create a QR code to display on the new device (Dynabook)
private fun createQrCodeDataForUnVerifiedDevice(requestId: String, otherDeviceId: String?): QrCodeData? {
private fun createQrCodeDataForUnVerifiedDevice(requestId: String): QrCodeData.SelfVerifyingMasterKeyNotTrusted? {
val myMasterKey = crossSigningService.getMyCrossSigningKeys()
?.masterKey()
?.unpaddedBase64PublicKey
@ -909,34 +868,17 @@ internal class DefaultVerificationService @Inject constructor(
return null
}
val myDeviceId = deviceId
?: run {
Timber.w("## Unable to get my deviceId")
return null
}
val myDeviceKey = myDeviceInfoHolder.get().myDevice.fingerprint()
?: run {
Timber.w("## Unable to get my fingerprint")
return null
}
val otherDeviceKey = otherDeviceId
?.let {
cryptoStore.getUserDevice(userId, otherDeviceId)?.fingerprint()
}
return QrCodeData(
userId = userId,
requestId = requestId,
action = QrCodeData.ACTION_VERIFY,
keys = hashMapOf(
// Note: no master key here
myDeviceId to myDeviceKey
),
sharedSecret = generateSharedSecret(),
otherUserKey = myMasterKey,
otherDeviceKey = otherDeviceKey
return QrCodeData.SelfVerifyingMasterKeyNotTrusted(
transactionId = requestId,
deviceKey = myDeviceKey,
userMasterCrossSigningPublicKey = myMasterKey,
sharedSecret = generateSharedSecretV2()
)
}

View File

@ -28,7 +28,7 @@ import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationTransaction
import im.vector.matrix.android.internal.crypto.verification.VerificationInfo
import im.vector.matrix.android.internal.crypto.verification.VerificationInfoStart
import im.vector.matrix.android.internal.util.withoutPrefix
import im.vector.matrix.android.internal.util.exhaustive
import timber.log.Timber
internal class DefaultQrCodeVerificationTransaction(
@ -46,7 +46,7 @@ internal class DefaultQrCodeVerificationTransaction(
) : DefaultVerificationTransaction(transactionId, otherUserId, otherDeviceId, isIncoming), QrCodeVerificationTransaction {
override val qrCodeText: String?
get() = qrCodeData?.toUrl()
get() = qrCodeData?.toEncodedString()
override var state: VerificationTxState = VerificationTxState.None
set(newState) {
@ -69,89 +69,76 @@ internal class DefaultQrCodeVerificationTransaction(
}
// Perform some checks
if (otherQrCodeData.action != QrCodeData.ACTION_VERIFY) {
Timber.d("## Verification QR: Invalid action ${otherQrCodeData.action}")
cancel(CancelCode.QrCodeInvalid)
return
}
if (otherQrCodeData.userId != otherUserId) {
Timber.d("## Verification QR: Mismatched user ${otherQrCodeData.userId}")
cancel(CancelCode.MismatchedUser)
return
}
if (otherQrCodeData.requestId != transactionId) {
Timber.d("## Verification QR: Invalid transaction actual ${otherQrCodeData.requestId} expected:$transactionId")
if (otherQrCodeData.transactionId != transactionId) {
Timber.d("## Verification QR: Invalid transaction actual ${otherQrCodeData.transactionId} expected:$transactionId")
cancel(CancelCode.QrCodeInvalid)
return
}
// check master key
if (otherQrCodeData.userId != userId
&& otherQrCodeData.otherUserKey == null) {
// Verification with other user, other_user_key is mandatory in this case
Timber.d("## Verification QR: Invalid, missing other_user_key")
cancel(CancelCode.QrCodeInvalid)
return
}
if (otherQrCodeData.otherUserKey != null
&& otherQrCodeData.otherUserKey != crossSigningService.getUserCrossSigningKeys(userId)?.masterKey()?.unpaddedBase64PublicKey) {
Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.otherUserKey}")
when (otherQrCodeData) {
is QrCodeData.VerifyingAnotherUser -> {
if (otherQrCodeData.otherUserMasterCrossSigningPublicKey
!= crossSigningService.getUserCrossSigningKeys(userId)?.masterKey()?.unpaddedBase64PublicKey) {
Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.otherUserMasterCrossSigningPublicKey}")
cancel(CancelCode.MismatchedKeys)
return
} else Unit
}
// Check device key if available
if (otherQrCodeData.otherDeviceKey != null
&& otherQrCodeData.otherDeviceKey != cryptoStore.getUserDevice(userId, deviceId)?.fingerprint()) {
Timber.d("## Verification QR: Invalid other device key")
is QrCodeData.SelfVerifyingMasterKeyTrusted -> {
if (otherQrCodeData.userMasterCrossSigningPublicKey
!= crossSigningService.getUserCrossSigningKeys(userId)?.masterKey()?.unpaddedBase64PublicKey) {
Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.userMasterCrossSigningPublicKey}")
cancel(CancelCode.MismatchedKeys)
return
} else Unit
}
is QrCodeData.SelfVerifyingMasterKeyNotTrusted -> {
if (otherQrCodeData.userMasterCrossSigningPublicKey
!= crossSigningService.getUserCrossSigningKeys(userId)?.masterKey()?.unpaddedBase64PublicKey) {
Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.userMasterCrossSigningPublicKey}")
cancel(CancelCode.MismatchedKeys)
return
} else Unit
}
}.exhaustive
val toVerifyDeviceIds = mutableListOf<String>()
var canTrustOtherUserMasterKey = false
val otherDevices = cryptoStore.getUserDevices(otherUserId)
otherQrCodeData.keys.keys.forEach { key ->
Timber.w("## Verification QR: Checking key $key")
when (val keyNoPrefix = key.withoutPrefix("ed25519:")) {
otherQrCodeData.keys[key] -> {
// Maybe master key?
if (otherQrCodeData.keys[key] == crossSigningService.getUserCrossSigningKeys(otherUserId)?.masterKey()?.unpaddedBase64PublicKey) {
canTrustOtherUserMasterKey = true
// Check device key if available
when (otherQrCodeData) {
is QrCodeData.VerifyingAnotherUser -> {
if (otherQrCodeData.userMasterCrossSigningPublicKey
!= crossSigningService.getUserCrossSigningKeys(otherUserId)?.masterKey()?.unpaddedBase64PublicKey) {
Timber.d("## Verification QR: Invalid user master key ${otherQrCodeData.userMasterCrossSigningPublicKey}")
cancel(CancelCode.MismatchedKeys)
return
} else {
canTrustOtherUserMasterKey = true
Unit
}
}
is QrCodeData.SelfVerifyingMasterKeyTrusted -> {
if (otherQrCodeData.otherDeviceKey
!= cryptoStore.getUserDevice(userId, deviceId)?.fingerprint()) {
Timber.d("## Verification QR: Invalid other device key ${otherQrCodeData.otherDeviceKey}")
cancel(CancelCode.MismatchedKeys)
return
} else Unit
}
}
else -> {
when (val otherDevice = otherDevices?.get(keyNoPrefix)) {
null -> {
// Unknown device, ignore
}
else -> {
when (otherDevice.fingerprint()) {
null -> {
// Ignore
}
otherQrCodeData.keys[key] -> {
// Store the deviceId to verify after
toVerifyDeviceIds.add(key)
}
else -> {
is QrCodeData.SelfVerifyingMasterKeyNotTrusted -> {
if (otherQrCodeData.deviceKey
!= cryptoStore.getUserDevice(otherUserId, otherDeviceId ?: "")?.fingerprint()) {
Timber.d("## Verification QR: Invalid device key ${otherQrCodeData.deviceKey}")
cancel(CancelCode.MismatchedKeys)
return
} else {
toVerifyDeviceIds.add(otherQrCodeData.deviceKey)
Unit
}
}
}
}
}
}
}
}.exhaustive
if (!canTrustOtherUserMasterKey && toVerifyDeviceIds.isEmpty()) {
// Nothing to verify
@ -164,13 +151,6 @@ internal class DefaultQrCodeVerificationTransaction(
// qrCodeData.sharedSecret will be used to send the start request
start(otherQrCodeData.sharedSecret)
val safeOtherDeviceId = otherDeviceId
if (!otherQrCodeData.otherDeviceKey.isNullOrBlank()
&& safeOtherDeviceId != null) {
// Locally verify the device
toVerifyDeviceIds.add(safeOtherDeviceId)
}
// Trust the other user
trust(canTrustOtherUserMasterKey, toVerifyDeviceIds.distinct())
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 New Vector Ltd
* Copyright (c) 2020 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.
@ -16,116 +16,112 @@
package im.vector.matrix.android.internal.crypto.verification.qrcode
import im.vector.matrix.android.api.MatrixPatterns
import im.vector.matrix.android.api.permalinks.PermalinkFactory
import java.net.URLDecoder
import java.net.URLEncoder
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64NoPadding
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
import im.vector.matrix.android.internal.extensions.toUnsignedInt
private const val ENCODING = "utf-8"
// MATRIX
private val prefix = "MATRIX".toByteArray(Charsets.ISO_8859_1)
/**
* Generate an URL to generate a QR code of the form:
* <pre>
* https://matrix.to/#/<user-id>?
* request=<event-id>
* &action=verify
* &key_<keyid>=<key-in-base64>...
* &secret=<shared_secret>
* &other_user_key=<master-key-in-base64>
* &other_device_key=<device-key-in-base64>
*
* Example:
* https://matrix.to/#/@user:matrix.org?
* request=%24pBeIfm7REDACTEDSQJbgqvi-yYiwmPB8_H_W_O974
* &action=verify
* &key_VJEDVKUYTQ=DL7LWIw7Qp%2B4AREDACTEDOwy2BjygumSWAGfzaWY
* &key_fsh%2FfQ08N3xvh4ySXsINB%2BJ2hREDACTEDVcVOG4qqo=fsh%2FfQ08N3xvh4ySXsINB%2BJ2hREDACTEDVcVOG4qqo
* &secret=AjQqw51Fp6UBuPolZ2FAD5WnXc22ZhJG6iGslrVvIdw%3D
* &other_user_key=WqSVLkBCS%2Fi5NqRREDACTEDRPxBIuqK8Usl6Y3big
* &other_device_key=WqSVLkBREDACTEDBsfszdvsdBEvefqsdcsfBvsfcsFb
* </pre>
*/
fun QrCodeData.toUrl(): String {
return buildString {
append(PermalinkFactory.createPermalink(userId))
append("?request=")
append(URLEncoder.encode(requestId, ENCODING))
append("&action=")
append(URLEncoder.encode(action, ENCODING))
fun QrCodeData.toEncodedString(): String {
var result = ByteArray(0)
for ((keyId, key) in keys) {
append("&key_${URLEncoder.encode(keyId, ENCODING)}=")
append(URLEncoder.encode(key, ENCODING))
// MATRIX
for (i in prefix.indices) {
result += prefix[i]
}
append("&secret=")
append(URLEncoder.encode(sharedSecret, ENCODING))
// Version
result += 2
if (!otherUserKey.isNullOrBlank()) {
append("&other_user_key=")
append(URLEncoder.encode(otherUserKey, ENCODING))
// Mode
result += when (this) {
is QrCodeData.VerifyingAnotherUser -> 0
is QrCodeData.SelfVerifyingMasterKeyTrusted -> 1
is QrCodeData.SelfVerifyingMasterKeyNotTrusted -> 2
}.toByte()
// TransactionId length
val length = transactionId.length
result += ((length and 0xFF00) shr 8).toByte()
result += length.toByte()
// TransactionId
transactionId.forEach {
result += it.toByte()
}
if (!otherDeviceKey.isNullOrBlank()) {
append("&other_device_key=")
append(URLEncoder.encode(otherDeviceKey, ENCODING))
// Keys
firstKey.fromBase64NoPadding().forEach {
result += it
}
secondKey.fromBase64NoPadding().forEach {
result += it
}
// Secret
sharedSecret.fromBase64NoPadding().forEach {
result += it
}
return result.toString(Charsets.ISO_8859_1)
}
fun String.toQrCodeData(): QrCodeData? {
if (!startsWith(PermalinkFactory.MATRIX_TO_URL_BASE)) {
val byteArray = toByteArray(Charsets.ISO_8859_1)
// Size should be min 6 + 1 + 1 + 2 + ? + 32 + 32 + ? = 74 + transactionLength + secretLength
// Check header
// MATRIX
if (byteArray.size < 10) return null
for (i in prefix.indices) {
if (byteArray[i] != prefix[i]) {
return null
}
}
var cursor = prefix.size // 6
// Version
if (byteArray[cursor] != 2.toByte()) {
return null
}
cursor++
// Get mode
val mode = byteArray[cursor].toInt()
cursor++
// Get transaction length
val bigEndian1 = byteArray[cursor].toUnsignedInt()
val bigEndian2 = byteArray[cursor + 1].toUnsignedInt()
val transactionLength = bigEndian1 * 0x0100 + bigEndian2
cursor++
cursor++
val secretLength = byteArray.size - 74 - transactionLength
// ensure the secret length is 8 bytes min
if (secretLength < 8) {
return null
}
val fragment = substringAfter("#")
if (fragment.isEmpty()) {
return null
val transactionId = byteArray.copyOfRange(cursor, cursor + transactionLength).toString(Charsets.ISO_8859_1)
cursor += transactionLength
val key1 = byteArray.copyOfRange(cursor, cursor + 32).toBase64NoPadding()
cursor += 32
val key2 = byteArray.copyOfRange(cursor, cursor + 32).toBase64NoPadding()
cursor += 32
val secret = byteArray.copyOfRange(cursor, byteArray.size).toBase64NoPadding()
return when (mode) {
0 -> QrCodeData.VerifyingAnotherUser(transactionId, key1, key2, secret)
1 -> QrCodeData.SelfVerifyingMasterKeyTrusted(transactionId, key1, key2, secret)
2 -> QrCodeData.SelfVerifyingMasterKeyNotTrusted(transactionId, key1, key2, secret)
else -> null
}
val safeFragment = fragment.substringBefore("?")
// we are limiting to 2 params
val params = safeFragment
.split(MatrixPatterns.SEP_REGEX.toRegex())
.filter { it.isNotEmpty() }
if (params.size != 1) {
return null
}
val userId = params.getOrNull(0)
?.let { PermalinkFactory.unescape(it) }
?.takeIf { MatrixPatterns.isUserId(it) } ?: return null
val urlParams = fragment.substringAfter("?")
.split("&".toRegex())
.filter { it.isNotEmpty() }
val keyValues = urlParams.map {
(it.substringBefore("=") to it.substringAfter("=").let { value -> URLDecoder.decode(value, ENCODING) })
}.toMap()
val action = keyValues["action"]?.takeIf { it.isNotBlank() } ?: return null
val requestEventId = keyValues["request"]?.takeIf { it.isNotBlank() } ?: return null
val sharedSecret = keyValues["secret"]?.takeIf { it.isNotBlank() } ?: return null
val otherUserKey = keyValues["other_user_key"]
val otherDeviceKey = keyValues["other_device_key"]
val keys = keyValues.keys
.filter { it.startsWith("key_") }
.map {
URLDecoder.decode(it.substringAfter("key_"), ENCODING) to (keyValues[it] ?: return null)
}
.toMap()
return QrCodeData(
userId,
requestEventId,
action,
keys,
sharedSecret,
otherUserKey,
otherDeviceKey
)
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 New Vector Ltd
* Copyright (c) 2020 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.
@ -19,27 +19,84 @@ package im.vector.matrix.android.internal.crypto.verification.qrcode
/**
* Ref: https://github.com/uhoreg/matrix-doc/blob/qr_key_verification/proposals/1543-qr_code_key_verification.md#qr-code-format
*/
data class QrCodeData(
val userId: String,
// Request Id. Can be an arbitrary value. In DM, it will be the event ID of the associated verification request event.
val requestId: String,
// The action
val action: String,
// key_<key_id>: each key that the user wants verified will have an entry of this form, where the value is the key in unpadded base64.
// The QR code should contain at least the user's master cross-signing key. In the case where a device does not have a cross-signing key
// (as in the case where a user logs in to a new device, and is verifying against another device), thin the QR code should contain at
// least the device's key.
val keys: Map<String, String>,
// random single-use shared secret in unpadded base64. It must be at least 256-bits long (43 characters when base64-encoded).
val sharedSecret: String,
// the other user's master cross-signing key, in unpadded base64. In other words, if Alice is displaying the QR code,
// this would be the copy of Bob's master cross-signing key that Alice has.
val otherUserKey: String?,
// The other device's key, in unpadded base64
// This is only needed when a user is verifying their own devices, where the other device has not yet been signed with the cross-signing key.
val otherDeviceKey: String?
sealed class QrCodeData(
/**
* the event ID or transaction_id of the associated verification
*/
open val transactionId: String,
/**
* First key (32 bytes, in base64 no padding)
*/
val firstKey: String,
/**
* Second key (32 bytes, in base64 no padding)
*/
val secondKey: String,
/**
* a random shared secret (in base64 no padding)
*/
open val sharedSecret: String
) {
companion object {
const val ACTION_VERIFY = "verify"
}
/**
* verifying another user with cross-signing
* QR code verification mode: 0x00
*/
data class VerifyingAnotherUser(
override val transactionId: String,
/**
* the user's own master cross-signing public key
*/
val userMasterCrossSigningPublicKey: String,
/**
* what the device thinks the other user's master cross-signing key is
*/
val otherUserMasterCrossSigningPublicKey: String,
override val sharedSecret: String
) : QrCodeData(
transactionId,
userMasterCrossSigningPublicKey,
otherUserMasterCrossSigningPublicKey,
sharedSecret)
/**
* self-verifying in which the current device does trust the master key
* QR code verification mode: 0x01
*/
data class SelfVerifyingMasterKeyTrusted(
override val transactionId: String,
/**
* the user's own master cross-signing public key
*/
val userMasterCrossSigningPublicKey: String,
/**
* what the device thinks the other device's device key is
*/
val otherDeviceKey: String,
override val sharedSecret: String
) : QrCodeData(
transactionId,
userMasterCrossSigningPublicKey,
otherDeviceKey,
sharedSecret)
/**
* self-verifying in which the current device does not yet trust the master key
* QR code verification mode: 0x02
*/
data class SelfVerifyingMasterKeyNotTrusted(
override val transactionId: String,
/**
* the current device's device key
*/
val deviceKey: String,
/**
* what the device thinks the user's master cross-signing key is
*/
val userMasterCrossSigningPublicKey: String,
override val sharedSecret: String
) : QrCodeData(
transactionId,
deviceKey,
userMasterCrossSigningPublicKey,
sharedSecret)
}

View File

@ -19,11 +19,11 @@ package im.vector.matrix.android.internal.crypto.verification.qrcode
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
import java.security.SecureRandom
fun generateSharedSecret(): String {
fun generateSharedSecretV2(): String {
val secureRandom = SecureRandom()
// 256 bits long
val secretBytes = ByteArray(32)
// 8 bytes long
val secretBytes = ByteArray(8)
secureRandom.nextBytes(secretBytes)
return secretBytes.toBase64NoPadding()
}

View File

@ -44,9 +44,9 @@ internal class DefaultRoomDirectoryService @Inject constructor(private val getPu
.executeBy(taskExecutor)
}
override fun joinRoom(roomId: String, reason: String?, callback: MatrixCallback<Unit>): Cancelable {
override fun joinRoom(roomIdOrAlias: String, reason: String?, callback: MatrixCallback<Unit>): Cancelable {
return joinRoomTask
.configureWith(JoinRoomTask.Params(roomId, reason)) {
.configureWith(JoinRoomTask.Params(roomIdOrAlias, reason)) {
this.callback = callback
}
.executeBy(taskExecutor)

View File

@ -139,9 +139,9 @@ internal class DefaultRoomService @Inject constructor(
.executeBy(taskExecutor)
}
override fun joinRoom(roomId: String, reason: String?, viaServers: List<String>, callback: MatrixCallback<Unit>): Cancelable {
override fun joinRoom(roomIdOrAlias: String, reason: String?, viaServers: List<String>, callback: MatrixCallback<Unit>): Cancelable {
return joinRoomTask
.configureWith(JoinRoomTask.Params(roomId, reason, viaServers)) {
.configureWith(JoinRoomTask.Params(roomIdOrAlias, reason, viaServers)) {
this.callback = callback
}
.executeBy(taskExecutor)

View File

@ -73,6 +73,21 @@ fun VerificationState.isCanceled(): Boolean {
return this == VerificationState.CANCELED_BY_ME || this == VerificationState.CANCELED_BY_OTHER
}
// State transition with control
private fun VerificationState?.toState(newState: VerificationState): VerificationState {
// Cancel is always prioritary ?
// Eg id i found that mac or keys mismatch and send a cancel and the other send a done, i have to
// consider as canceled
if (newState.isCanceled()) {
return newState
}
// never move out of cancel
if (this?.isCanceled() == true) {
return this
}
return newState
}
/**
* Called by EventRelationAggregationUpdater, when new events that can affect relations are inserted in base.
*/
@ -550,38 +565,26 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
} else {
ContentMapper.map(verifSummary.content)?.toModel<ReferencesAggregatedContent>()
var data = ContentMapper.map(verifSummary.content)?.toModel<ReferencesAggregatedContent>()
?: ReferencesAggregatedContent(VerificationState.REQUEST.name)
?: ReferencesAggregatedContent(VerificationState.REQUEST)
// TODO ignore invalid messages? e.g a START after a CANCEL?
// i.e. never change state if already canceled/done
val currentState = VerificationState.values().firstOrNull { data.verificationSummary == it.name }
val currentState = data.verificationState
val newState = when (event.getClearType()) {
EventType.KEY_VERIFICATION_START -> {
updateVerificationState(currentState, VerificationState.WAITING)
}
EventType.KEY_VERIFICATION_ACCEPT -> {
updateVerificationState(currentState, VerificationState.WAITING)
}
EventType.KEY_VERIFICATION_READY -> {
updateVerificationState(currentState, VerificationState.WAITING)
}
EventType.KEY_VERIFICATION_KEY -> {
updateVerificationState(currentState, VerificationState.WAITING)
}
EventType.KEY_VERIFICATION_MAC -> {
updateVerificationState(currentState, VerificationState.WAITING)
}
EventType.KEY_VERIFICATION_CANCEL -> {
updateVerificationState(currentState, if (event.senderId == userId) {
EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_READY,
EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_MAC -> currentState.toState(VerificationState.WAITING)
EventType.KEY_VERIFICATION_CANCEL -> currentState.toState(if (event.senderId == userId) {
VerificationState.CANCELED_BY_ME
} else VerificationState.CANCELED_BY_OTHER)
}
EventType.KEY_VERIFICATION_DONE -> {
updateVerificationState(currentState, VerificationState.DONE)
}
} else {
VerificationState.CANCELED_BY_OTHER
})
EventType.KEY_VERIFICATION_DONE -> currentState.toState(VerificationState.DONE)
else -> VerificationState.REQUEST
}
data = data.copy(verificationSummary = newState.name)
data = data.copy(verificationState = newState)
verifSummary.content = ContentMapper.map(data.toContent())
}
@ -592,18 +595,4 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
verifSummary.sourceEvents.add(event.eventId)
}
}
private fun updateVerificationState(oldState: VerificationState?, newState: VerificationState): VerificationState {
// Cancel is always prioritary ?
// Eg id i found that mac or keys mismatch and send a cancel and the other send a done, i have to
// consider as canceled
if (newState == VerificationState.CANCELED_BY_OTHER || newState == VerificationState.CANCELED_BY_ME) {
return newState
}
// never move out of cancel
if (oldState == VerificationState.CANCELED_BY_OTHER || oldState == VerificationState.CANCELED_BY_ME) {
return oldState
}
return newState
}
}

View File

@ -20,6 +20,7 @@ import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse
import im.vector.matrix.android.api.session.room.model.create.JoinRoomResponse
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsParams
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsResponse
import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProtocol
@ -223,13 +224,13 @@ internal interface RoomAPI {
* Join the given room.
*
* @param roomIdOrAlias the room id or alias
* @param server_name the servers to attempt to join the room through
* @param viaServers the servers to attempt to join the room through
* @param params the request body
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "join/{roomIdOrAlias}")
fun join(@Path("roomIdOrAlias") roomIdOrAlias: String,
@Query("server_name") viaServers: List<String>,
@Body params: Map<String, String?>): Call<Unit>
@Body params: Map<String, String?>): Call<JoinRoomResponse>
/**
* Leave the given room.

View File

@ -66,7 +66,7 @@ internal class DefaultCreateRoomTask @Inject constructor(
val createRoomResponse = executeRequest<CreateRoomResponse>(eventBus) {
apiCall = roomAPI.createRoom(createRoomParams)
}
val roomId = createRoomResponse.roomId!!
val roomId = createRoomResponse.roomId
// Wait for room to come back from the sync (but it can maybe be in the DB if the sync response is received before)
try {
awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.session.room.membership.joining
import im.vector.matrix.android.api.session.room.failure.JoinRoomFailure
import im.vector.matrix.android.api.session.room.model.create.JoinRoomResponse
import im.vector.matrix.android.internal.database.awaitNotEmptyResult
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomEntityFields
@ -33,7 +34,7 @@ import javax.inject.Inject
internal interface JoinRoomTask : Task<JoinRoomTask.Params, Unit> {
data class Params(
val roomId: String,
val roomIdOrAlias: String,
val reason: String?,
val viaServers: List<String> = emptyList()
)
@ -48,19 +49,20 @@ internal class DefaultJoinRoomTask @Inject constructor(
) : JoinRoomTask {
override suspend fun execute(params: JoinRoomTask.Params) {
executeRequest<Unit>(eventBus) {
apiCall = roomAPI.join(params.roomId, params.viaServers, mapOf("reason" to params.reason))
val joinRoomResponse = executeRequest<JoinRoomResponse>(eventBus) {
apiCall = roomAPI.join(params.roomIdOrAlias, params.viaServers, mapOf("reason" to params.reason))
}
// Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before)
val roomId = joinRoomResponse.roomId
try {
awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->
realm.where(RoomEntity::class.java)
.equalTo(RoomEntityFields.ROOM_ID, params.roomId)
.equalTo(RoomEntityFields.ROOM_ID, roomId)
}
} catch (exception: TimeoutCancellationException) {
throw JoinRoomFailure.JoinedWithTimeout
}
setReadMarkers(params.roomId)
setReadMarkers(roomId)
}
private suspend fun setReadMarkers(roomId: String) {

View File

@ -62,7 +62,7 @@ internal class RoomTombstoneEventLiveObserver @Inject constructor(@SessionDataba
for (event in tombstoneEvents) {
if (event.roomId == null) continue
val createRoomContent = event.getClearContent().toModel<RoomTombstoneContent>()
if (createRoomContent?.replacementRoom == null) continue
if (createRoomContent?.replacementRoomId == null) continue
val predecessorRoomSummary = RoomSummaryEntity.where(realm, event.roomId).findFirst()
?: RoomSummaryEntity(event.roomId)

View File

@ -0,0 +1,20 @@
/*
* Copyright (c) 2020 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.util
// Trick to ensure that when block is exhaustive
internal val <T> T.exhaustive: T get() = this

View File

@ -0,0 +1,52 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.verification.qrcode
import org.amshove.kluent.shouldEqualTo
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runners.MethodSorters
@FixMethodOrder(MethodSorters.JVM)
class BinaryStringTest {
/**
* I want to put bytes to a String, and vice versa
*/
@Test
fun testNominalCase() {
val byteArray = ByteArray(256)
for (i in byteArray.indices) {
byteArray[i] = i.toByte() // Random.nextInt(255).toByte()
}
val str = byteArray.toString(Charsets.ISO_8859_1)
str.length shouldEqualTo 256
// Ok convert back to bytearray
val result = str.toByteArray(Charsets.ISO_8859_1)
result.size shouldEqualTo 256
for (i in 0..255) {
result[i] shouldEqualTo i.toByte()
result[i] shouldEqualTo byteArray[i]
}
}
}

View File

@ -1,246 +0,0 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.verification.qrcode
import org.amshove.kluent.shouldBe
import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldBeNull
import org.amshove.kluent.shouldNotBeNull
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runners.MethodSorters
@Suppress("SpellCheckingInspection")
@FixMethodOrder(MethodSorters.JVM)
class QrCodeTest {
private val basicQrCodeData = QrCodeData(
userId = "@benoit:matrix.org",
requestId = "\$azertyazerty",
action = QrCodeData.ACTION_VERIFY,
keys = mapOf(
"1" to "abcdef",
"2" to "ghijql"
),
sharedSecret = "sharedSecret",
otherUserKey = "otherUserKey",
otherDeviceKey = "otherDeviceKey"
)
private val basicUrl = "https://matrix.to/#/@benoit:matrix.org" +
"?request=%24azertyazerty" +
"&action=verify" +
"&key_1=abcdef" +
"&key_2=ghijql" +
"&secret=sharedSecret" +
"&other_user_key=otherUserKey" +
"&other_device_key=otherDeviceKey"
@Test
fun testNominalCase() {
val url = basicQrCodeData.toUrl()
url shouldBeEqualTo basicUrl
val decodedData = url.toQrCodeData()
decodedData.shouldNotBeNull()
decodedData.userId shouldBeEqualTo "@benoit:matrix.org"
decodedData.requestId shouldBeEqualTo "\$azertyazerty"
decodedData.keys["1"]?.shouldBeEqualTo("abcdef")
decodedData.keys["2"]?.shouldBeEqualTo("ghijql")
decodedData.sharedSecret shouldBeEqualTo "sharedSecret"
decodedData.otherUserKey?.shouldBeEqualTo("otherUserKey")
decodedData.otherDeviceKey?.shouldBeEqualTo("otherDeviceKey")
}
@Test
fun testSlashCase() {
val url = basicQrCodeData
.copy(
userId = "@benoit/foo:matrix.org",
requestId = "\$azertyazerty/bar"
)
.toUrl()
url shouldBeEqualTo basicUrl
.replace("@benoit", "@benoit%2Ffoo")
.replace("azertyazerty", "azertyazerty%2Fbar")
val decodedData = url.toQrCodeData()
decodedData.shouldNotBeNull()
decodedData.userId shouldBeEqualTo "@benoit/foo:matrix.org"
decodedData.requestId shouldBeEqualTo "\$azertyazerty/bar"
decodedData.keys["1"]?.shouldBeEqualTo("abcdef")
decodedData.keys["2"]?.shouldBeEqualTo("ghijql")
decodedData.sharedSecret shouldBeEqualTo "sharedSecret"
decodedData.otherUserKey!! shouldBeEqualTo "otherUserKey"
decodedData.otherDeviceKey!! shouldBeEqualTo "otherDeviceKey"
}
@Test
fun testNoOtherUserKey() {
val url = basicQrCodeData
.copy(
otherUserKey = null
)
.toUrl()
url shouldBeEqualTo basicUrl
.replace("&other_user_key=otherUserKey", "")
val decodedData = url.toQrCodeData()
decodedData.shouldNotBeNull()
decodedData.userId shouldBeEqualTo "@benoit:matrix.org"
decodedData.requestId shouldBeEqualTo "\$azertyazerty"
decodedData.keys["1"]?.shouldBeEqualTo("abcdef")
decodedData.keys["2"]?.shouldBeEqualTo("ghijql")
decodedData.sharedSecret shouldBeEqualTo "sharedSecret"
decodedData.otherUserKey shouldBe null
decodedData.otherDeviceKey?.shouldBeEqualTo("otherDeviceKey")
}
@Test
fun testNoOtherDeviceKey() {
val url = basicQrCodeData
.copy(
otherDeviceKey = null
)
.toUrl()
url shouldBeEqualTo basicUrl
.replace("&other_device_key=otherDeviceKey", "")
val decodedData = url.toQrCodeData()
decodedData.shouldNotBeNull()
decodedData.userId shouldBeEqualTo "@benoit:matrix.org"
decodedData.requestId shouldBeEqualTo "\$azertyazerty"
decodedData.keys["1"]?.shouldBeEqualTo("abcdef")
decodedData.keys["2"]?.shouldBeEqualTo("ghijql")
decodedData.sharedSecret shouldBeEqualTo "sharedSecret"
decodedData.otherUserKey?.shouldBeEqualTo("otherUserKey")
decodedData.otherDeviceKey shouldBe null
}
@Test
fun testUrlCharInKeys() {
val url = basicQrCodeData
.copy(
keys = mapOf(
"/=" to "abcdef",
"&?" to "ghijql"
)
)
.toUrl()
url shouldBeEqualTo basicUrl
.replace("key_1=abcdef", "key_%2F%3D=abcdef")
.replace("key_2=ghijql", "key_%26%3F=ghijql")
val decodedData = url.toQrCodeData()
decodedData.shouldNotBeNull()
decodedData.keys["/="]?.shouldBeEqualTo("abcdef")
decodedData.keys["&&"]?.shouldBeEqualTo("ghijql")
}
@Test
fun testMissingActionCase() {
basicUrl.replace("&action=verify", "")
.toQrCodeData()
.shouldBeNull()
}
@Test
fun testEmptyActionCase() {
basicUrl.replace("&action=verify", "&action=")
.toQrCodeData()
.shouldBeNull()
}
@Test
fun testOtherActionCase() {
basicUrl.replace("&action=verify", "&action=confirm")
.toQrCodeData()
?.action
?.shouldBeEqualTo("confirm")
}
@Test
fun testMissingRequestId() {
basicUrl.replace("request=%24azertyazerty", "")
.toQrCodeData()
.shouldBeNull()
}
@Test
fun testEmptyRequestId() {
basicUrl.replace("request=%24azertyazerty", "request=")
.toQrCodeData()
.shouldBeNull()
}
@Test
fun testMissingUserId() {
basicUrl.replace("@benoit:matrix.org", "")
.toQrCodeData()
.shouldBeNull()
}
@Test
fun testBadUserId() {
basicUrl.replace("@benoit:matrix.org", "@benoit")
.toQrCodeData()
.shouldBeNull()
}
@Test
fun testMissingSecret() {
basicUrl.replace("&secret=sharedSecret", "")
.toQrCodeData()
.shouldBeNull()
}
@Test
fun testEmptySecret() {
basicUrl.replace("&secret=sharedSecret", "&secret=")
.toQrCodeData()
.shouldBeNull()
}
@Test
fun testSelfSigning() {
// request is not an eventId in this case
val url = "https://matrix.to/#/@benoit0815:matrix.org" +
"?request=local.4dff40e1-7bf1-4e80-81ed-c6090d43bf20" +
"&action=verify" +
"&key_utbSRFcFjFDYf0KcNv3FoBHFSbvUPXtCYutuOg6WQ%2Bs=utbSRFcFjFDYf0KcNv3FoBHFSbvUPXtCYutuOg6WQ%2Bs" +
"&key_YSOXZVBXIZ=F0XWqgUePgwm5HMYG3yhBNneHmscrAxxlooLHjy8YQc" +
"&secret=LYVcEQmfdorbJ3vbQnq7nbNZc%2BGmDxUen1rByV9hRM4" +
"&other_device_key=eGoUqZqAroCYpjp7FLGIkTEzYHBFED4uUAfJ267gqQQ"
url.toQrCodeData()!!.requestId shouldBeEqualTo "local.4dff40e1-7bf1-4e80-81ed-c6090d43bf20"
}
}

View File

@ -378,8 +378,11 @@ dependencies {
// TESTS
testImplementation 'junit:junit:4.12'
testImplementation 'org.amshove.kluent:kluent-android:1.44'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
androidTestImplementation 'org.amshove.kluent:kluent-android:1.44'
}
if (getGradle().getStartParameter().getTaskRequests().toString().contains("Gplay")) {

View File

@ -37,6 +37,7 @@ import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.debug.sas.DebugSasEmojiActivity
import im.vector.riotx.features.qrcode.QrCodeScannerActivity
import kotlinx.android.synthetic.debug.activity_debug_menu.*
import timber.log.Timber
import javax.inject.Inject
class DebugMenuActivity : VectorBaseActivity() {
@ -50,8 +51,19 @@ class DebugMenuActivity : VectorBaseActivity() {
injector.inject(this)
}
private lateinit var buffer: ByteArray
override fun initUiAndData() {
renderQrCode("https://www.example.org")
// renderQrCode("https://www.example.org")
buffer = ByteArray(256)
for (i in buffer.indices) {
buffer[i] = i.toByte()
}
val string = buffer.toString(Charsets.ISO_8859_1)
renderQrCode(string)
}
private fun renderQrCode(text: String) {
@ -194,7 +206,20 @@ class DebugMenuActivity : VectorBaseActivity() {
toast("QrCode: " + QrCodeScannerActivity.getResultText(data) + " is QRCode: " + QrCodeScannerActivity.getResultIsQrCode(data))
// Also update the current QR Code (reverse operation)
renderQrCode(QrCodeScannerActivity.getResultText(data) ?: "")
// renderQrCode(QrCodeScannerActivity.getResultText(data) ?: "")
val result = QrCodeScannerActivity.getResultText(data)!!
if (result.length != buffer.size) {
Timber.e("Error, length are not the same")
} else {
// Convert to ByteArray
val byteArrayResult = result.toByteArray(Charsets.ISO_8859_1)
for (i in byteArrayResult.indices) {
if (buffer[i] != byteArrayResult[i]) {
Timber.e("Error for byte $i, expecting ${buffer[i]} and get ${byteArrayResult[i]}")
}
}
}
}
}
}

View File

@ -242,7 +242,7 @@ class EllipsizingTextView @JvmOverloads constructor(context: Context, attrs: Att
@Suppress("DEPRECATION")
protected fun createWorkingLayout(workingText: CharSequence?): Layout {
return StaticLayout(
workingText,
workingText ?: "",
paint,
width - compoundPaddingLeft - compoundPaddingRight,
Layout.Alignment.ALIGN_NORMAL,

View File

@ -292,20 +292,16 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun handleTombstoneEvent(action: RoomDetailAction.HandleTombstoneEvent) {
val tombstoneContent = action.event.getClearContent().toModel<RoomTombstoneContent>() ?: return
val roomId = tombstoneContent.replacementRoom ?: ""
val roomId = tombstoneContent.replacementRoomId ?: ""
val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN
if (isRoomJoined) {
setState { copy(tombstoneEventHandling = Success(roomId)) }
} else {
val viaServer = MatrixPatterns.extractServerNameFromId(action.event.senderId).let {
if (it.isNullOrBlank()) {
emptyList()
} else {
listOf(it)
}
}
val viaServers = MatrixPatterns.extractServerNameFromId(action.event.senderId)
?.let { listOf(it) }
.orEmpty()
session.rx()
.joinRoom(roomId, viaServers = viaServer)
.joinRoom(roomId, viaServers = viaServers)
.map { roomId }
.execute {
copy(tombstoneEventHandling = it)

View File

@ -104,11 +104,9 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
}
.toList(),
referencesInfoData = event.annotations?.referencesAggregatedSummary?.let { referencesAggregatedSummary ->
val stateStr = referencesAggregatedSummary.content.toModel<ReferencesAggregatedContent>()?.verificationSummary
ReferencesInfoData(
VerificationState.values().firstOrNull { stateStr == it.name }
val verificationState = referencesAggregatedSummary.content.toModel<ReferencesAggregatedContent>()?.verificationState
?: VerificationState.REQUEST
)
ReferencesInfoData(verificationState)
},
sentByMe = event.root.senderId == session.myUserId
)

View File

@ -31,6 +31,7 @@ import com.airbnb.epoxy.EpoxyModelClass
import im.vector.matrix.android.api.session.crypto.sas.VerificationService
import im.vector.matrix.android.internal.session.room.VerificationState
import im.vector.riotx.R
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.features.home.AvatarRenderer
@ -102,12 +103,7 @@ abstract class VerificationRequestItem : AbsBaseMessageItem<VerificationRequestI
}
holder.statusTextView.isVisible = true
}
else -> {
holder.buttonBar.isVisible = false
holder.statusTextView.text = null
holder.statusTextView.isVisible = false
}
}
}.exhaustive
// Always hide buttons if request is too old
if (!VerificationService.isValidRequest(attributes.informationData.ageLocalTS)) {

View File

@ -105,6 +105,7 @@ class RoomListFragment @Inject constructor(
is RoomListViewEvents.Loading -> showLoading(it.message)
is RoomListViewEvents.Failure -> showFailure(it.throwable)
is RoomListViewEvents.SelectRoom -> handleSelectRoom(it)
is RoomListViewEvents.Done -> Unit
}.exhaustive
}

View File

@ -28,4 +28,5 @@ sealed class RoomListViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : RoomListViewEvents()
data class SelectRoom(val roomSummary: RoomSummary) : RoomListViewEvents()
object Done : RoomListViewEvents()
}

View File

@ -197,6 +197,10 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
private fun handleLeaveRoom(action: RoomListAction.LeaveRoom) {
_viewEvents.post(RoomListViewEvents.Loading(null))
session.getRoom(action.roomId)?.leave(null, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
_viewEvents.post(RoomListViewEvents.Done)
}
override fun onFailure(failure: Throwable) {
_viewEvents.post(RoomListViewEvents.Failure(failure))
}

View File

@ -22,6 +22,7 @@ import android.os.Bundle
import androidx.fragment.app.Fragment
import com.google.zxing.BarcodeFormat
import com.google.zxing.Result
import com.google.zxing.ResultMetadataType
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.replaceFragment
@ -43,15 +44,33 @@ class QrCodeScannerActivity : VectorBaseActivity() {
}
fun setResultAndFinish(result: Result?) {
result?.let {
if (result != null) {
val rawBytes = getRawBytes(result)
val rawBytesStr = rawBytes?.toString(Charsets.ISO_8859_1)
setResult(RESULT_OK, Intent().apply {
putExtra(EXTRA_OUT_TEXT, it.text)
putExtra(EXTRA_OUT_IS_QR_CODE, it.barcodeFormat == BarcodeFormat.QR_CODE)
putExtra(EXTRA_OUT_TEXT, rawBytesStr ?: result.text)
putExtra(EXTRA_OUT_IS_QR_CODE, result.barcodeFormat == BarcodeFormat.QR_CODE)
})
}
finish()
}
// Copied from https://github.com/markusfisch/BinaryEye/blob/
// 9d57889b810dcaa1a91d7278fc45c262afba1284/app/src/main/kotlin/de/markusfisch/android/binaryeye/activity/CameraActivity.kt#L434
private fun getRawBytes(result: Result): ByteArray? {
val metadata = result.resultMetadata ?: return null
val segments = metadata[ResultMetadataType.BYTE_SEGMENTS] ?: return null
var bytes = ByteArray(0)
@Suppress("UNCHECKED_CAST")
for (seg in segments as Iterable<ByteArray>) {
bytes += seg
}
// byte segments can never be shorter than the text.
// Zxing cuts off content prefixes like "WIFI:"
return if (bytes.size >= result.text.length) bytes else null
}
companion object {
private const val EXTRA_OUT_TEXT = "EXTRA_OUT_TEXT"
private const val EXTRA_OUT_IS_QR_CODE = "EXTRA_OUT_IS_QR_CODE"

View File

@ -85,7 +85,7 @@ class PublicRoomsController @Inject constructor(private val stringProvider: Stri
avatarRenderer(avatarRenderer)
id(publicRoom.roomId)
matrixItem(publicRoom.toMatrixItem())
roomAlias(publicRoom.canonicalAlias)
roomAlias(publicRoom.getPrimaryAlias())
roomTopic(publicRoom.topic)
nbOfMembers(publicRoom.numJoinedMembers)

View File

@ -134,7 +134,7 @@ class PublicRoomsFragment @Inject constructor(
override fun onPublicRoomJoin(publicRoom: PublicRoom) {
Timber.v("PublicRoomJoinClicked: $publicRoom")
viewModel.handle(RoomDirectoryAction.JoinRoom(publicRoom.roomId))
viewModel.handle(RoomDirectoryAction.JoinRoom(publicRoom.getPrimaryAlias(), publicRoom.roomId))
}
override fun loadMore() {

View File

@ -23,5 +23,5 @@ sealed class RoomDirectoryAction : VectorViewModelAction {
data class SetRoomDirectoryData(val roomDirectoryData: RoomDirectoryData) : RoomDirectoryAction()
data class FilterWith(val filter: String) : RoomDirectoryAction()
object LoadMore : RoomDirectoryAction()
data class JoinRoom(val roomId: String) : RoomDirectoryAction()
data class JoinRoom(val roomAlias: String?, val roomId: String) : RoomDirectoryAction()
}

View File

@ -216,7 +216,7 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
)
}
session.joinRoom(action.roomId, callback = object : MatrixCallback<Unit> {
session.joinRoom(action.roomAlias ?: action.roomId, callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
// We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data.
// Instead, we wait for the room to be joined

View File

@ -19,5 +19,5 @@ package im.vector.riotx.features.roomdirectory.roompreview
import im.vector.riotx.core.platform.VectorViewModelAction
sealed class RoomPreviewAction : VectorViewModelAction {
object Join : RoomPreviewAction()
data class Join(val roomAlias: String?) : RoomPreviewAction()
}

View File

@ -32,12 +32,13 @@ import kotlinx.android.parcel.Parcelize
data class RoomPreviewData(
val roomId: String,
val roomName: String?,
val roomAlias: String?,
val topic: String?,
val worldReadable: Boolean,
val avatarUrl: String?
) : Parcelable {
val matrixItem: MatrixItem
get() = MatrixItem.RoomItem(roomId, roomName, avatarUrl)
get() = MatrixItem.RoomItem(roomId, roomName ?: roomAlias, avatarUrl)
}
class RoomPreviewActivity : VectorBaseActivity(), ToolbarConfigurable {
@ -50,6 +51,7 @@ class RoomPreviewActivity : VectorBaseActivity(), ToolbarConfigurable {
putExtra(ARG, RoomPreviewData(
roomId = publicRoom.roomId,
roomName = publicRoom.name,
roomAlias = publicRoom.getPrimaryAlias(),
topic = publicRoom.topic,
worldReadable = publicRoom.worldReadable,
avatarUrl = publicRoom.avatarUrl

View File

@ -50,11 +50,11 @@ class RoomPreviewNoPreviewFragment @Inject constructor(
setupToolbar(roomPreviewNoPreviewToolbar)
// Toolbar
avatarRenderer.render(roomPreviewData.matrixItem, roomPreviewNoPreviewToolbarAvatar)
roomPreviewNoPreviewToolbarTitle.text = roomPreviewData.roomName
roomPreviewNoPreviewToolbarTitle.text = roomPreviewData.roomName ?: roomPreviewData.roomAlias
// Screen
avatarRenderer.render(roomPreviewData.matrixItem, roomPreviewNoPreviewAvatar)
roomPreviewNoPreviewName.text = roomPreviewData.roomName
roomPreviewNoPreviewName.text = roomPreviewData.roomName ?: roomPreviewData.roomAlias
roomPreviewNoPreviewTopic.setTextOrHide(roomPreviewData.topic)
if (roomPreviewData.worldReadable) {
@ -65,7 +65,7 @@ class RoomPreviewNoPreviewFragment @Inject constructor(
roomPreviewNoPreviewJoin.callback = object : ButtonStateView.Callback {
override fun onButtonClicked() {
roomPreviewViewModel.handle(RoomPreviewAction.Join)
roomPreviewViewModel.handle(RoomPreviewAction.Join(roomPreviewData.roomAlias))
}
override fun onRetryClicked() {

View File

@ -26,6 +26,7 @@ import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
import im.vector.matrix.rx.rx
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.roomdirectory.JoinState
@ -82,11 +83,11 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted initialState: R
override fun handle(action: RoomPreviewAction) {
when (action) {
RoomPreviewAction.Join -> joinRoom()
}
is RoomPreviewAction.Join -> handleJoinRoom(action)
}.exhaustive
}
private fun joinRoom() = withState { state ->
private fun handleJoinRoom(action: RoomPreviewAction.Join) = withState { state ->
if (state.roomJoinState == JoinState.JOINING) {
// Request already sent, should not happen
Timber.w("Try to join an already joining room. Should not happen")
@ -100,7 +101,7 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted initialState: R
)
}
session.joinRoom(state.roomId, callback = object : MatrixCallback<Unit> {
session.joinRoom(action.roomAlias ?: state.roomId, callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
// We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data.
// Instead, we wait for the room to be joined