From dbe78f160b8b7a7293ecf93739f2ef8ad0f6a521 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 13 May 2020 17:30:06 +0200 Subject: [PATCH 01/15] WithHeld key support initial commit --- .../matrix/android/common/CommonTestHelper.kt | 2 +- .../matrix/android/common/CryptoTestHelper.kt | 155 ++++++++++++++++++ .../crypto/gossiping/KeyShareTests.kt | 4 + .../crypto/gossiping/WithHeldTests.kt | 126 ++++++++++++++ .../api/session/crypto/MXCryptoError.kt | 4 +- .../android/api/session/events/model/Event.kt | 5 + .../api/session/events/model/EventType.kt | 1 + .../internal/crypto/DefaultCryptoService.kt | 23 ++- .../crypto/algorithms/IMXDecrypting.kt | 2 +- .../crypto/algorithms/IMXWithHeldExtension.kt | 24 +++ .../algorithms/megolm/MXMegolmDecryption.kt | 93 +++++++---- .../algorithms/megolm/MXMegolmEncryption.kt | 135 +++++++++++---- .../megolm/MXMegolmEncryptionFactory.kt | 8 +- .../crypto/algorithms/olm/MXOlmDecryption.kt | 2 +- .../crypto/model/MXUsersDevicesMap.kt | 10 ++ .../model/event/RoomKeyWithHeldContent.kt | 103 ++++++++++++ .../internal/crypto/store/IMXCryptoStore.kt | 5 + .../crypto/store/db/RealmCryptoStore.kt | 32 ++++ .../store/db/RealmCryptoStoreMigration.kt | 18 +- .../crypto/store/db/RealmCryptoStoreModule.kt | 4 +- .../store/db/model/WithHeldSessionEntity.kt | 48 ++++++ .../store/db/query/WithHeldSessionQueries.kt | 42 +++++ .../internal/database/mapper/EventMapper.kt | 7 + .../internal/database/model/EventEntity.kt | 2 + .../room/timeline/TimelineEventDecryptor.kt | 3 +- .../internal/session/sync/RoomSyncHandler.kt | 37 ++++- .../riotx/core/resources/DrawableProvider.kt | 37 +++++ .../timeline/factory/EncryptedItemFactory.kt | 68 ++++++-- .../VectorSettingsSecurityPrivacyFragment.kt | 1 - vector/src/main/res/drawable/ic_clock.xml | 21 +++ vector/src/main/res/drawable/ic_forbidden.xml | 11 ++ vector/src/main/res/values/strings.xml | 5 + .../xml/vector_settings_security_privacy.xml | 2 +- 33 files changed, 953 insertions(+), 87 deletions(-) create mode 100644 matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/WithHeldTests.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXWithHeldExtension.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/RoomKeyWithHeldContent.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/WithHeldSessionEntity.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/WithHeldSessionQueries.kt create mode 100644 vector/src/main/java/im/vector/riotx/core/resources/DrawableProvider.kt create mode 100644 vector/src/main/res/drawable/ic_clock.xml create mode 100644 vector/src/main/res/drawable/ic_forbidden.xml diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt index 600bcf2983..fd06acdcb3 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt @@ -150,7 +150,7 @@ class CommonTestHelper(context: Context) { timeline.dispose() // Check that all events has been created - assertEquals(nbOfMessages.toLong(), sentEvents.size.toLong()) + assertEquals("Message number do not match ${sentEvents}", nbOfMessages.toLong(), sentEvents.size.toLong()) return sentEvents } diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt index 4eda7c60c8..10a40944ae 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt @@ -17,8 +17,14 @@ package im.vector.matrix.android.common import android.os.SystemClock +import android.util.Log import androidx.lifecycle.Observer import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction +import im.vector.matrix.android.api.session.crypto.verification.OutgoingSasVerificationTransaction +import im.vector.matrix.android.api.session.crypto.verification.SasVerificationTransaction +import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod +import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState 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.toContent @@ -34,6 +40,8 @@ import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupAuthData import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo +import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth +import im.vector.matrix.android.internal.crypto.verification.SASDefaultVerificationTransaction import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -41,6 +49,8 @@ import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import java.util.UUID import java.util.concurrent.CountDownLatch class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { @@ -274,4 +284,149 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { authData = createFakeMegolmBackupAuthData() ) } + + fun createDM(alice: Session, bob: Session): String { + val roomId = mTestHelper.doSync { + alice.createRoom( + CreateRoomParams(invitedUserIds = listOf(bob.myUserId)) + .setDirectMessage() + .enableEncryptionIfInvitedUsersSupportIt(), + it + ) + } + + + mTestHelper.waitWithLatch { latch -> + val bobRoomSummariesLive = runBlocking(Dispatchers.Main) { + bob.getRoomSummariesLive(roomSummaryQueryParams { }) + } + + val newRoomObserver = object : Observer> { + override fun onChanged(t: List?) { + val indexOfFirst = t?.indexOfFirst { it.roomId == roomId } ?: -1 + if (indexOfFirst != -1) { + latch.countDown() + bobRoomSummariesLive.removeObserver(this) + } + } + } + + GlobalScope.launch(Dispatchers.Main) { + bobRoomSummariesLive.observeForever(newRoomObserver) + } + + } + + + mTestHelper.waitWithLatch { latch -> + val bobRoomSummariesLive = runBlocking(Dispatchers.Main) { + bob.getRoomSummariesLive(roomSummaryQueryParams { }) + } + + val newRoomObserver = object : Observer> { + override fun onChanged(t: List?) { + if (bob.getRoom(roomId) + ?.getRoomMember(bob.myUserId) + ?.membership == Membership.JOIN) { + latch.countDown() + bobRoomSummariesLive.removeObserver(this) + } + } + } + + GlobalScope.launch(Dispatchers.Main) { + bobRoomSummariesLive.observeForever(newRoomObserver) + } + + mTestHelper.doSync { bob.joinRoom(roomId, callback = it) } + + } + + + return roomId + } + + fun initializeCrossSigning(session: Session) { + mTestHelper.doSync { + session.cryptoService().crossSigningService() + .initializeCrossSigning(UserPasswordAuth( + user = session.myUserId, + password = TestConstants.PASSWORD + ), it) + } + } + + fun verifySASCrossSign(alice: Session, bob: Session, roomId: String) { + + assertTrue(alice.cryptoService().crossSigningService().canCrossSign()) + assertTrue(bob.cryptoService().crossSigningService().canCrossSign()) + + val requestID = UUID.randomUUID().toString() + val aliceVerificationService = alice.cryptoService().verificationService() + val bobVerificationService = bob.cryptoService().verificationService() + + aliceVerificationService.beginKeyVerificationInDMs( + VerificationMethod.SAS, + requestID, + roomId, + bob.myUserId, + bob.sessionParams.credentials.deviceId!!, + null) + + + // we should reach SHOW SAS on both + var alicePovTx: OutgoingSasVerificationTransaction? = null + var bobPovTx: IncomingSasVerificationTransaction? = null + + // wait for alice to get the ready + mTestHelper.waitWithLatch { + mTestHelper.retryPeriodicallyWithLatch(it) { + bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? IncomingSasVerificationTransaction + Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}") + if (bobPovTx?.state == VerificationTxState.OnStarted) { + bobPovTx?.performAccept() + true + } else { + false + } + } + } + + mTestHelper.waitWithLatch { + mTestHelper.retryPeriodicallyWithLatch(it) { + alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID) as? OutgoingSasVerificationTransaction + Log.v("TEST", "== alicePovTx is ${alicePovTx?.uxState}") + alicePovTx?.state == VerificationTxState.ShortCodeReady + } + } + // wait for alice to get the ready + mTestHelper.waitWithLatch { + mTestHelper.retryPeriodicallyWithLatch(it) { + bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? IncomingSasVerificationTransaction + Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}") + if (bobPovTx?.state == VerificationTxState.OnStarted) { + bobPovTx?.performAccept() + } + bobPovTx?.state == VerificationTxState.ShortCodeReady + } + } + + assertEquals("SAS code do not match", alicePovTx!!.getDecimalCodeRepresentation(), bobPovTx!!.getDecimalCodeRepresentation()) + + bobPovTx!!.userHasVerifiedShortCode() + alicePovTx!!.userHasVerifiedShortCode() + + mTestHelper.waitWithLatch { + mTestHelper.retryPeriodicallyWithLatch(it) { + alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId) + } + } + + mTestHelper.waitWithLatch { + mTestHelper.retryPeriodicallyWithLatch(it) { + alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId) + } + } + + } } diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt index 444278d67e..a25a5abb22 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt @@ -285,5 +285,9 @@ class KeyShareTests : InstrumentedTest { keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey == creationInfo.recoveryKey } } + + + mTestHelper.signOutAndClose(aliceSession1) + mTestHelper.signOutAndClose(aliceSession2) } } diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/WithHeldTests.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/WithHeldTests.kt new file mode 100644 index 0000000000..f73692ea65 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/WithHeldTests.kt @@ -0,0 +1,126 @@ +/* + * 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.gossiping + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import im.vector.matrix.android.InstrumentedTest +import im.vector.matrix.android.api.session.crypto.MXCryptoError +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.common.CommonTestHelper +import im.vector.matrix.android.common.CryptoTestHelper +import im.vector.matrix.android.common.SessionTestParams +import im.vector.matrix.android.common.TestConstants +import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode +import org.junit.Assert +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 WithHeldTests : InstrumentedTest { + + private val mTestHelper = CommonTestHelper(context()) + private val mCryptoTestHelper = CryptoTestHelper(mTestHelper) + + @Test + fun test_WithHeldUnverifiedReason() { + + //============================= + // ARRANGE + //============================= + + val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + val bobSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + + // Initialize cross signing on both + mCryptoTestHelper.initializeCrossSigning(aliceSession) + mCryptoTestHelper.initializeCrossSigning(bobSession) + + val roomId = mCryptoTestHelper.createDM(aliceSession, bobSession) + mCryptoTestHelper.verifySASCrossSign(aliceSession, bobSession, roomId) + + val roomAlicePOV = aliceSession.getRoom(roomId)!! + + val bobUnverifiedSession = mTestHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true)) + + //============================= + // ACT + //============================= + + // Alice decide to not send to unverified sessions + aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(true) + + val timelineEvent = mTestHelper.sendTextMessage(roomAlicePOV, "Hello Bob", 1).first() + + // await for bob unverified session to get the message + mTestHelper.waitWithLatch { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + bobUnverifiedSession.getRoom(roomId)?.getTimeLineEvent(timelineEvent.eventId) != null + } + } + + val eventBobPOV = bobUnverifiedSession.getRoom(roomId)?.getTimeLineEvent(timelineEvent.eventId)!! + + //============================= + // ASSERT + //============================= + + // Bob should not be able to decrypt because the keys is withheld + try { + // .. might need to wait a bit for stability? + bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") + Assert.fail("This session should not be able to decrypt") + } catch (failure: Throwable) { + val type = (failure as MXCryptoError.Base).errorType + val technicalMessage = failure.technicalMessage + Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) + Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage) + } + + // enable back sending to unverified + aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(false) + + val secondEvent = mTestHelper.sendTextMessage(roomAlicePOV, "Verify your device!!", 1).first() + + mTestHelper.waitWithLatch { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + val ev = bobUnverifiedSession.getRoom(roomId)?.getTimeLineEvent(secondEvent.eventId) + // wait until it's decrypted + ev?.root?.getClearType() == EventType.MESSAGE + } + } + + // Previous message should still be undecryptable (partially withheld session) + try { + // .. might need to wait a bit for stability? + bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") + Assert.fail("This session should not be able to decrypt") + } catch (failure: Throwable) { + val type = (failure as MXCryptoError.Base).errorType + val technicalMessage = failure.technicalMessage + Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) + Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage) + } + + + mTestHelper.signOutAndClose(aliceSession) + mTestHelper.signOutAndClose(bobSession) + mTestHelper.signOutAndClose(bobUnverifiedSession) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/MXCryptoError.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/MXCryptoError.kt index be817c70cb..9610390bd1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/MXCryptoError.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/MXCryptoError.kt @@ -20,6 +20,7 @@ package im.vector.matrix.android.api.session.crypto import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap +import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode import org.matrix.olm.OlmException /** @@ -59,7 +60,8 @@ sealed class MXCryptoError : Throwable() { MISSING_PROPERTY, OLM, UNKNOWN_DEVICES, - UNKNOWN_MESSAGE_INDEX + UNKNOWN_MESSAGE_INDEX, + KEYS_WITHHELD } companion object { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt index 1dd8abc858..1e178484e9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt @@ -82,6 +82,9 @@ data class Event( @Transient var mCryptoError: MXCryptoError.ErrorType? = null + @Transient + var mCryptoErrorReason: String? = null + @Transient var sendState: SendState = SendState.UNKNOWN @@ -182,6 +185,7 @@ data class Event( if (redacts != other.redacts) return false if (mxDecryptionResult != other.mxDecryptionResult) return false if (mCryptoError != other.mCryptoError) return false + if (mCryptoErrorReason != other.mCryptoErrorReason) return false if (sendState != other.sendState) return false return true @@ -200,6 +204,7 @@ data class Event( result = 31 * result + (redacts?.hashCode() ?: 0) result = 31 * result + (mxDecryptionResult?.hashCode() ?: 0) result = 31 * result + (mCryptoError?.hashCode() ?: 0) + result = 31 * result + (mCryptoErrorReason?.hashCode() ?: 0) result = 31 * result + sendState.hashCode() return result } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt index 801a9bb7d0..066a65323c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt @@ -66,6 +66,7 @@ object EventType { // Key share events const val ROOM_KEY_REQUEST = "m.room_key_request" const val FORWARDED_ROOM_KEY = "m.forwarded_room_key" + const val ROOM_KEY_WITHHELD = "org.matrix.room_key.withheld" const val REQUEST_SECRET = "m.secret.request" const val SEND_SECRET = "m.secret.send" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt index 012a1d9c12..2269b95791 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt @@ -52,6 +52,7 @@ import im.vector.matrix.android.internal.crypto.actions.MegolmSessionDataImporte import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting +import im.vector.matrix.android.internal.crypto.algorithms.IMXWithHeldExtension import im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmEncryptionFactory import im.vector.matrix.android.internal.crypto.crosssigning.DefaultCrossSigningService @@ -65,6 +66,7 @@ import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.android.internal.crypto.model.event.OlmEventContent import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent +import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldContent import im.vector.matrix.android.internal.crypto.model.event.SecretSendEventContent import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse @@ -807,6 +809,9 @@ internal class DefaultCryptoService @Inject constructor( cryptoStore.saveGossipingEvent(event) onSecretSendReceived(event) } + EventType.ROOM_KEY_WITHHELD -> { + onKeyWithHeldReceived(event) + } else -> { // ignore } @@ -834,6 +839,22 @@ internal class DefaultCryptoService @Inject constructor( alg.onRoomKeyEvent(event, keysBackupService) } + + private fun onKeyWithHeldReceived(event: Event) { + val withHeldContent = event.getClearContent().toModel() ?: return Unit.also { + Timber.e("## CRYPTO | Malformed onKeyWithHeldReceived() : missing fields") + } + Timber.d("## CRYPTO | onKeyWithHeldReceived() received : content <${withHeldContent}>") + val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(withHeldContent.roomId, withHeldContent.algorithm) + if (alg is IMXWithHeldExtension) { + alg.onRoomKeyWithHeldEvent(withHeldContent) + } else { + Timber.e("## CRYPTO | onKeyWithHeldReceived() : Unable to handle WithHeldContent for ${withHeldContent.algorithm}") + return + } + + } + private fun onSecretSendReceived(event: Event) { Timber.i("## CRYPTO | GOSSIP onSecretSend() : onSecretSendReceived ${event.content?.get("sender_key")}") if (!event.isEncrypted()) { @@ -1197,7 +1218,7 @@ internal class DefaultCryptoService @Inject constructor( // } roomDecryptorProvider .getOrCreateRoomDecryptor(event.roomId, wireContent.algorithm) - ?.requestKeysForEvent(event) ?: run { + ?.requestKeysForEvent(event, false) ?: run { Timber.v("## CRYPTO | requestRoomKeyForEvent() : No room decryptor for roomId:${event.roomId} algorithm:${wireContent.algorithm}") } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXDecrypting.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXDecrypting.kt index 0babb73842..be62bb33c1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXDecrypting.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXDecrypting.kt @@ -71,5 +71,5 @@ internal interface IMXDecrypting { fun shareSecretWithDevice(request: IncomingSecretShareRequest, secretValue : String) {} - fun requestKeysForEvent(event: Event) + fun requestKeysForEvent(event: Event, withHeld: Boolean) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXWithHeldExtension.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXWithHeldExtension.kt new file mode 100644 index 0000000000..8fa05ca648 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXWithHeldExtension.kt @@ -0,0 +1,24 @@ +/* + * 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.algorithms + +import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldContent + +internal interface IMXWithHeldExtension { + fun onRoomKeyWithHeldEvent(withHeldInfo: RoomKeyWithHeldContent) +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt index 4c66f63d6a..36cc4a6030 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt @@ -30,10 +30,12 @@ import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevicesAction import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter import im.vector.matrix.android.internal.crypto.algorithms.IMXDecrypting +import im.vector.matrix.android.internal.crypto.algorithms.IMXWithHeldExtension import im.vector.matrix.android.internal.crypto.keysbackup.DefaultKeysBackupService import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent +import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldContent import im.vector.matrix.android.internal.crypto.model.rest.ForwardedRoomKeyContent import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore @@ -53,7 +55,7 @@ internal class MXMegolmDecryption(private val userId: String, private val sendToDeviceTask: SendToDeviceTask, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val cryptoCoroutineScope: CoroutineScope -) : IMXDecrypting { +) : IMXDecrypting, IMXWithHeldExtension { var newSessionListener: NewSessionListener? = null @@ -61,7 +63,7 @@ internal class MXMegolmDecryption(private val userId: String, * Events which we couldn't decrypt due to unknown sessions / indexes: map from * senderKey|sessionId to timelines to list of MatrixEvents. */ - private var pendingEvents: MutableMap>> = HashMap() +// private var pendingEvents: MutableMap>> = HashMap() @Throws(MXCryptoError::class) override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { @@ -113,9 +115,21 @@ internal class MXMegolmDecryption(private val userId: String, if (throwable is MXCryptoError.OlmError) { // TODO Check the value of .message if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") { - addEventToPendingList(event, timeline) + //addEventToPendingList(event, timeline) + // The session might has been partially withheld (and only pass ratcheted) + val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId) + if (withHeldInfo != null) { + if (requestKeysOnFail) { + requestKeysForEvent(event, true) + } + // Encapsulate as withHeld exception + throw MXCryptoError.Base(MXCryptoError.ErrorType.KEYS_WITHHELD, + withHeldInfo.code?.value ?: "", + withHeldInfo.reason) + } + if (requestKeysOnFail) { - requestKeysForEvent(event) + requestKeysForEvent(event, false) } } @@ -128,10 +142,25 @@ internal class MXMegolmDecryption(private val userId: String, detailedReason) } if (throwable is MXCryptoError.Base) { - if (throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) { - addEventToPendingList(event, timeline) - if (requestKeysOnFail) { - requestKeysForEvent(event) + if ( + /** if the session is unknown*/ + throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID + ) { + val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId) + if (withHeldInfo != null) { + if (requestKeysOnFail) { + requestKeysForEvent(event, true) + } + // Encapsulate as withHeld exception + throw MXCryptoError.Base(MXCryptoError.ErrorType.KEYS_WITHHELD, + withHeldInfo.code?.value ?: "", + withHeldInfo.reason) + } else { + // This is un-used in riotX SDK, not sure if needed + //addEventToPendingList(event, timeline) + if (requestKeysOnFail) { + requestKeysForEvent(event, false) + } } } } @@ -147,12 +176,12 @@ internal class MXMegolmDecryption(private val userId: String, * * @param event the event */ - override fun requestKeysForEvent(event: Event) { + override fun requestKeysForEvent(event: Event, withHeld: Boolean) { val sender = event.senderId ?: return val encryptedEventContent = event.content.toModel() val senderDevice = encryptedEventContent?.deviceId ?: return - val recipients = if (event.senderId == userId) { + val recipients = if (event.senderId == userId || withHeld) { mapOf( userId to listOf("*") ) @@ -176,25 +205,25 @@ internal class MXMegolmDecryption(private val userId: String, outgoingGossipingRequestManager.sendRoomKeyRequest(requestBody, recipients) } - /** - * Add an event to the list of those we couldn't decrypt the first time we - * saw them. - * - * @param event the event to try to decrypt later - * @param timelineId the timeline identifier - */ - private fun addEventToPendingList(event: Event, timelineId: String) { - val encryptedEventContent = event.content.toModel() ?: return - val pendingEventsKey = "${encryptedEventContent.senderKey}|${encryptedEventContent.sessionId}" - - val timeline = pendingEvents.getOrPut(pendingEventsKey) { HashMap() } - val events = timeline.getOrPut(timelineId) { ArrayList() } - - if (event !in events) { - Timber.v("## CRYPTO | addEventToPendingList() : add Event ${event.eventId} in room id ${event.roomId}") - events.add(event) - } - } +// /** +// * Add an event to the list of those we couldn't decrypt the first time we +// * saw them. +// * +// * @param event the event to try to decrypt later +// * @param timelineId the timeline identifier +// */ +// private fun addEventToPendingList(event: Event, timelineId: String) { +// val encryptedEventContent = event.content.toModel() ?: return +// val pendingEventsKey = "${encryptedEventContent.senderKey}|${encryptedEventContent.sessionId}" +// +// val timeline = pendingEvents.getOrPut(pendingEventsKey) { HashMap() } +// val events = timeline.getOrPut(timelineId) { ArrayList() } +// +// if (event !in events) { +// Timber.v("## CRYPTO | addEventToPendingList() : add Event ${event.eventId} in room id ${event.roomId}") +// events.add(event) +// } +// } /** * Handle a key event. @@ -349,4 +378,10 @@ internal class MXMegolmDecryption(private val userId: String, } } } + + override fun onRoomKeyWithHeldEvent(withHeldInfo: RoomKeyWithHeldContent) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + cryptoStore.addWithHeldMegolmSession(withHeldInfo) + } + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt index 3800e3c4f2..e75ea9d06a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.crypto.algorithms.megolm +import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.events.model.Content @@ -31,9 +32,14 @@ import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting import im.vector.matrix.android.internal.crypto.keysbackup.DefaultKeysBackupService import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap +import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldContent +import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode +import im.vector.matrix.android.internal.crypto.model.forEach import im.vector.matrix.android.internal.crypto.repository.WarnOnUnknownDeviceRepository import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.JsonCanonicalizer import im.vector.matrix.android.internal.util.convertToUTF8 import timber.log.Timber @@ -49,7 +55,8 @@ internal class MXMegolmEncryption( private val credentials: Credentials, private val sendToDeviceTask: SendToDeviceTask, private val messageEncrypter: MessageEncrypter, - private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository + private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, + private val taskExecutor: TaskExecutor ) : IMXEncrypting { // OutboundSessionInfo. Null if we haven't yet started setting one up. Note @@ -69,9 +76,26 @@ internal class MXMegolmEncryption( val ts = System.currentTimeMillis() Timber.v("## CRYPTO | encryptEventContent : getDevicesInRoom") val devices = getDevicesInRoom(userIds) - Timber.v("## CRYPTO | encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.map}") - val outboundSession = ensureOutboundSession(devices) + Timber.v("## CRYPTO | encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.map}") + val outboundSession = ensureOutboundSession(devices.allowedDevices) + return encryptContent(outboundSession, eventType, eventContent) + .also { + notifyWithheldForSession(devices.withHeldDevices, outboundSession) + } + } + + private fun notifyWithheldForSession(devices: MXUsersDevicesMap, outboundSession: MXOutboundSessionInfo) { + ArrayList>().apply { + devices.forEach { userId, deviceId, withheldCode -> + this.add(UserDevice(userId, deviceId) to withheldCode) + } + }.groupBy( + { it.second }, + { it.first } + ).forEach { (code, targets) -> + notifyKeyWithHeld(targets, outboundSession.sessionId, code) + } } override fun discardSessionKey() { @@ -198,15 +222,16 @@ internal class MXMegolmEncryption( if (sessionResult?.sessionId == null) { // no session with this device, probably because there // were no one-time keys. - // - // we could send them a to_device message anyway, as a - // signal that they have missed out on the key sharing - // message because of the lack of keys, but there's not - // much point in that really; it will mostly serve to clog - // up to_device inboxes. - // - // ensureOlmSessionsForUsers has already done the logging, - // so just skip it. + + // MSC 2399 + // send withheld m.no_olm: an olm session could not be established. + // This may happen, for example, if the sender was unable to obtain a one-time key from the recipient. + notifyKeyWithHeld( + listOf(UserDevice(userId, deviceID)), + session.sessionId, + WithHeldCode.NO_OLM + ) + continue } Timber.v("## CRYPTO | shareUserDevicesKey() : Sharing keys with device $userId:$deviceID") @@ -214,29 +239,59 @@ internal class MXMegolmEncryption( haveTargets = true } } + + // Add the devices we have shared with to session.sharedWithDevices. + // we deliberately iterate over devicesByUser (ie, the devices we + // attempted to share with) rather than the contentMap (those we did + // share with), because we don't want to try to claim a one-time-key + // for dead devices on every message. + for ((userId, devicesToShareWith) in devicesByUser) { + for ((deviceId) in devicesToShareWith) { + session.sharedWithDevices.setObject(userId, deviceId, chainIndex) + } + } + if (haveTargets) { t0 = System.currentTimeMillis() Timber.v("## CRYPTO | shareUserDevicesKey() : has target") val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap) - sendToDeviceTask.execute(sendToDeviceParams) - Timber.v("## CRYPTO | shareUserDevicesKey() : sendToDevice succeeds after " - + (System.currentTimeMillis() - t0) + " ms") - - // Add the devices we have shared with to session.sharedWithDevices. - // we deliberately iterate over devicesByUser (ie, the devices we - // attempted to share with) rather than the contentMap (those we did - // share with), because we don't want to try to claim a one-time-key - // for dead devices on every message. - for ((userId, devicesToShareWith) in devicesByUser) { - for ((deviceId) in devicesToShareWith) { - session.sharedWithDevices.setObject(userId, deviceId, chainIndex) - } + try { + sendToDeviceTask.execute(sendToDeviceParams) + Timber.v("## CRYPTO | shareUserDevicesKey() : sendToDevice succeeds after ${System.currentTimeMillis() - t0} ms") + } catch (failure: Throwable) { + // What to do here... + Timber.e("## CRYPTO | shareUserDevicesKey() : Failed to share session <${session.sessionId}> with $devicesByUser ") } } else { Timber.v("## CRYPTO | shareUserDevicesKey() : no need to sharekey") } } + private fun notifyKeyWithHeld(targets: List, sessionId: String, code: WithHeldCode) { + val withHeldContent = RoomKeyWithHeldContent( + roomId = roomId, + senderKey = olmDevice.deviceCurve25519Key, + algorithm = MXCRYPTO_ALGORITHM_MEGOLM, + sessionId = sessionId, + codeString = code.value + ) + val params = SendToDeviceTask.Params( + EventType.ROOM_KEY_WITHHELD, + MXUsersDevicesMap().apply { + targets.forEach { + setObject(it.userId, it.deviceId, withHeldContent) + } + } + ) + sendToDeviceTask.configureWith(params) { + callback = object : MatrixCallback { + override fun onFailure(failure: Throwable) { + Timber.e("## CRYPTO | notifyKeyWithHeld() : Failed to notify withheld key for $targets session: $sessionId ") + } + } + }.executeBy(taskExecutor) + } + /** * process the pending encryptions */ @@ -271,7 +326,7 @@ internal class MXMegolmEncryption( * * @param userIds the user ids whose devices must be checked. */ - private suspend fun getDevicesInRoom(userIds: List): MXUsersDevicesMap { + private suspend fun getDevicesInRoom(userIds: List): DeviceInRoomInfo { // We are happy to use a cached version here: we assume that if we already // have a list of the user's devices, then we already share an e2e room // with them, which means that they will have announced any new devices via @@ -280,9 +335,10 @@ internal class MXMegolmEncryption( val encryptToVerifiedDevicesOnly = cryptoStore.getGlobalBlacklistUnverifiedDevices() || cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(roomId) - val devicesInRoom = MXUsersDevicesMap() + val devicesInRoom = DeviceInRoomInfo() val unknownDevices = MXUsersDevicesMap() + for (userId in keys.userIds) { val deviceIds = keys.getUserDeviceIds(userId) ?: continue for (deviceId in deviceIds) { @@ -294,10 +350,12 @@ internal class MXMegolmEncryption( } if (deviceInfo.isBlocked) { // Remove any blocked devices + devicesInRoom.withHeldDevices.setObject(userId, deviceId, WithHeldCode.BLACKLISTED) continue } if (!deviceInfo.isVerified && encryptToVerifiedDevicesOnly) { + devicesInRoom.withHeldDevices.setObject(userId, deviceId, WithHeldCode.UNVERIFIED) continue } @@ -305,7 +363,7 @@ internal class MXMegolmEncryption( // Don't bother sending to ourself continue } - devicesInRoom.setObject(userId, deviceId, deviceInfo) + devicesInRoom.allowedDevices.setObject(userId, deviceId, deviceInfo) } } if (unknownDevices.isEmpty) { @@ -354,9 +412,24 @@ internal class MXMegolmEncryption( val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) val sendToDeviceMap = MXUsersDevicesMap() sendToDeviceMap.setObject(userId, deviceId, encodedPayload) - Timber.v("## CRYPTO | CRYPTO | shareKeysWithDevice() : sending to $userId:$deviceId") + Timber.v("## CRYPTO | CRYPTO | reshareKey() : sending to $userId:$deviceId") val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) - sendToDeviceTask.execute(sendToDeviceParams) - return true + return try { + sendToDeviceTask.execute(sendToDeviceParams) + true + } catch (failure: Throwable) { + Timber.v("## CRYPTO | CRYPTO | reshareKey() : fail to send <$sessionId> to $userId:$deviceId") + false + } } + + data class DeviceInRoomInfo( + val allowedDevices: MXUsersDevicesMap = MXUsersDevicesMap(), + val withHeldDevices: MXUsersDevicesMap = MXUsersDevicesMap() + ) + + data class UserDevice( + val userId: String, + val deviceId: String + ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt index e9fe902f1f..3cc321621b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt @@ -25,6 +25,7 @@ import im.vector.matrix.android.internal.crypto.keysbackup.DefaultKeysBackupServ import im.vector.matrix.android.internal.crypto.repository.WarnOnUnknownDeviceRepository import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask +import im.vector.matrix.android.internal.task.TaskExecutor import javax.inject.Inject internal class MXMegolmEncryptionFactory @Inject constructor( @@ -36,7 +37,8 @@ internal class MXMegolmEncryptionFactory @Inject constructor( private val credentials: Credentials, private val sendToDeviceTask: SendToDeviceTask, private val messageEncrypter: MessageEncrypter, - private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository) { + private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, + private val taskExecutor: TaskExecutor) { fun create(roomId: String): MXMegolmEncryption { return MXMegolmEncryption( @@ -49,6 +51,8 @@ internal class MXMegolmEncryptionFactory @Inject constructor( credentials, sendToDeviceTask, messageEncrypter, - warnOnUnknownDevicesRepository) + warnOnUnknownDevicesRepository, + taskExecutor + ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmDecryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmDecryption.kt index 8ef527fa05..5607e54fd6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmDecryption.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmDecryption.kt @@ -212,7 +212,7 @@ internal class MXOlmDecryption( return res["payload"] } - override fun requestKeysForEvent(event: Event) { + override fun requestKeysForEvent(event: Event, withHeld: Boolean) { // nop } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXUsersDevicesMap.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXUsersDevicesMap.kt index bbad25ef51..4188078522 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXUsersDevicesMap.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXUsersDevicesMap.kt @@ -119,3 +119,13 @@ class MXUsersDevicesMap { return "MXUsersDevicesMap $map" } } + +inline fun MXUsersDevicesMap.forEach(action: (String, String, T) -> Unit) { + userIds.forEach { userId -> + getUserDeviceIds(userId)?.forEach { deviceId -> + getObject(userId, deviceId)?.let { + action(userId, deviceId, it) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/RoomKeyWithHeldContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/RoomKeyWithHeldContent.kt new file mode 100644 index 0000000000..56fc451671 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/RoomKeyWithHeldContent.kt @@ -0,0 +1,103 @@ +/* + * 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.model.event + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.extensions.tryThis + +/** + * Class representing an sharekey content + */ +@JsonClass(generateAdapter = true) +data class RoomKeyWithHeldContent( + + /** + * Required if code is not m.no_olm. The ID of the room that the session belongs to. + */ + @Json(name = "room_id") val roomId: String? = null, + + /** + * Required. The encryption algorithm that the key is for. + */ + @Json(name = "algorithm") val algorithm: String? = null, + + /** + * Required if code is not m.no_olm. The ID of the session. + */ + @Json(name = "session_id") val sessionId: String? = null, + + /** + * Required. The key of the session creator. + */ + @Json(name = "sender_key") val senderKey: String? = null, + + /** + * Required. A machine-readable code for why the key was not sent + */ + @Json(name = "code") val codeString: String? = null, + + /** + * A human-readable reason for why the key was not sent. The receiving client should only use this string if it does not understand the code. + */ + @Json(name = "reason") val reason: String? = null + +) { + val code: WithHeldCode? + get() { + return WithHeldCode.fromCode(codeString) + } +} + +enum class WithHeldCode(val value: String) { + /** + * the user/device was blacklisted + */ + BLACKLISTED("m.blacklisted"), + /** + * the user/devices is unverified + */ + UNVERIFIED("m.unverified"), + /** + * the user/device is not allowed have the key. For example, this would usually be sent in response + * to a key request if the user was not in the room when the message was sent + */ + UNAUTHORISED("m.unauthorised"), + /** + * Sent in reply to a key request if the device that the key is requested from does not have the requested key + */ + UNAVAILABLE("m.unavailable"), + /** + * An olm session could not be established. + * This may happen, for example, if the sender was unable to obtain a one-time key from the recipient. + */ + NO_OLM("m.no_olm"); + + companion object { + fun fromCode(code: String?): WithHeldCode? { + return when (code) { + BLACKLISTED.value -> BLACKLISTED + UNVERIFIED.value -> UNVERIFIED + UNAUTHORISED.value -> UNAUTHORISED + UNAVAILABLE.value -> UNAVAILABLE + NO_OLM.value -> NO_OLM + else -> null + } + } + } +} + + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt index 3d12835a41..ddb98fd1ec 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt @@ -1,3 +1,4 @@ + /* * Copyright 2016 OpenMarket Ltd * Copyright 2018 New Vector Ltd @@ -32,6 +33,7 @@ import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper2 import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper +import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldContent import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity @@ -416,6 +418,9 @@ internal interface IMXCryptoStore { fun updateUsersTrust(check: (String) -> Boolean) + fun addWithHeldMegolmSession(withHeldContent: RoomKeyWithHeldContent) + fun getWithHeldMegolmSession(roomId: String, sessionId: String) : RoomKeyWithHeldContent? + // Dev tools fun getOutgoingRoomKeyRequests(): List diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt index 5e4d82bb67..badf82f696 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt @@ -31,6 +31,7 @@ import im.vector.matrix.android.internal.crypto.GossipingRequestState import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest import im.vector.matrix.android.internal.crypto.IncomingShareRequestCommon +import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import im.vector.matrix.android.internal.crypto.NewSessionListener import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestState import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest @@ -40,6 +41,8 @@ import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper2 import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper +import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldContent +import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody import im.vector.matrix.android.internal.crypto.model.toEntity @@ -69,6 +72,7 @@ import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossiping import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntity import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields +import im.vector.matrix.android.internal.crypto.store.db.model.WithHeldSessionEntity import im.vector.matrix.android.internal.crypto.store.db.model.createPrimaryKey import im.vector.matrix.android.internal.crypto.store.db.query.delete import im.vector.matrix.android.internal.crypto.store.db.query.get @@ -1427,4 +1431,32 @@ internal class RealmCryptoStore @Inject constructor( return existing } } + + override fun addWithHeldMegolmSession(withHeldContent: RoomKeyWithHeldContent) { + val roomId = withHeldContent.roomId ?: return + val sessionId = withHeldContent.sessionId ?: return + if (withHeldContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return + doRealmTransaction(realmConfiguration) { realm -> + WithHeldSessionEntity.getOrCreate(realm, roomId, sessionId)?.let { + it.code = withHeldContent.code + it.senderKey = withHeldContent.senderKey + it.reason = withHeldContent.reason + } + } + } + + override fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent? { + return doWithRealm(realmConfiguration) { realm -> + WithHeldSessionEntity.get(realm, roomId, sessionId)?.let { + RoomKeyWithHeldContent( + roomId = roomId, + sessionId = sessionId, + algorithm = it.algorithm, + codeString = it.codeString, + reason = it.reason, + senderKey = it.senderKey + ) + } + } + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt index 6a21355743..dbcdf1e0fa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt @@ -38,6 +38,7 @@ import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntityF import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields +import im.vector.matrix.android.internal.crypto.store.db.model.WithHeldSessionEntityFields import im.vector.matrix.android.internal.di.SerializeNulls import io.realm.DynamicRealm import io.realm.RealmMigration @@ -52,7 +53,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi // 0, 1, 2: legacy Riot-Android // 3: migrate to RiotX schema // 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6) - const val CRYPTO_STORE_SCHEMA_VERSION = 9L + const val CRYPTO_STORE_SCHEMA_VERSION = 10L } override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { @@ -67,6 +68,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi if (oldVersion <= 6) migrateTo7(realm) if (oldVersion <= 7) migrateTo8(realm) if (oldVersion <= 8) migrateTo9(realm) + if (oldVersion <= 9) migrateTo10(realm) } private fun migrateTo1Legacy(realm: DynamicRealm) { @@ -416,4 +418,18 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi } } } + + // Version 10L added WithHeld Keys Info (MSC2399) + private fun migrateTo10(realm: DynamicRealm) { + Timber.d("Step 9 -> 10") + realm.schema.create("WithHeldSessionEntity") + .addField(WithHeldSessionEntityFields.ROOM_ID, String::class.java) + .addField(WithHeldSessionEntityFields.ALGORITHM, String::class.java) + .addField(WithHeldSessionEntityFields.SESSION_ID, String::class.java) + .addIndex(WithHeldSessionEntityFields.SESSION_ID) + .addField(WithHeldSessionEntityFields.SENDER_KEY, String::class.java) + .addIndex(WithHeldSessionEntityFields.SENDER_KEY) + .addField(WithHeldSessionEntityFields.CODE_STRING, String::class.java) + .addField(WithHeldSessionEntityFields.REASON, String::class.java) + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreModule.kt index a8eb1db612..aa03636871 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreModule.kt @@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntity import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntity import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity +import im.vector.matrix.android.internal.crypto.store.db.model.WithHeldSessionEntity import io.realm.annotations.RealmModule /** @@ -50,6 +51,7 @@ import io.realm.annotations.RealmModule GossipingEventEntity::class, IncomingGossipingRequestEntity::class, OutgoingGossipingRequestEntity::class, - MyDeviceLastSeenInfoEntity::class + MyDeviceLastSeenInfoEntity::class, + WithHeldSessionEntity::class ]) internal class RealmCryptoStoreModule diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/WithHeldSessionEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/WithHeldSessionEntity.kt new file mode 100644 index 0000000000..ad2945cfb3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/WithHeldSessionEntity.kt @@ -0,0 +1,48 @@ +/* + * 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.store.db.model + +import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode +import io.realm.RealmObject +import io.realm.annotations.Index + +/** + * When an encrypted message is sent in a room, the megolm key might not be sent to all devices present in the room. + * Sometimes this may be inadvertent (for example, if the sending device is not aware of some devices that have joined), + * but some times, this may be purposeful. + * For example, the sender may have blacklisted certain devices or users, + * or may be choosing to not send the megolm key to devices that they have not verified yet. + */ +internal open class WithHeldSessionEntity( + var roomId: String? = null, + var algorithm: String? = null, + @Index var sessionId: String? = null, + @Index var senderKey: String? = null, + var codeString: String? = null, + var reason: String? = null +) : RealmObject() { + + var code: WithHeldCode? + get() { + return WithHeldCode.fromCode(codeString) + } + set(code) { + codeString = code?.value + } + + companion object +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/WithHeldSessionQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/WithHeldSessionQueries.kt new file mode 100644 index 0000000000..be88200016 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/WithHeldSessionQueries.kt @@ -0,0 +1,42 @@ +/* + * 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.store.db.query + +import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import im.vector.matrix.android.internal.crypto.store.db.model.WithHeldSessionEntity +import im.vector.matrix.android.internal.crypto.store.db.model.WithHeldSessionEntityFields +import io.realm.Realm +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +internal fun WithHeldSessionEntity.Companion.get(realm: Realm, roomId: String, sessionId: String): WithHeldSessionEntity? { + return realm.where() + .equalTo(WithHeldSessionEntityFields.ROOM_ID, roomId) + .equalTo(WithHeldSessionEntityFields.SESSION_ID, sessionId) + .equalTo(WithHeldSessionEntityFields.ALGORITHM, MXCRYPTO_ALGORITHM_MEGOLM) + .findFirst() +} + +internal fun WithHeldSessionEntity.Companion.getOrCreate(realm: Realm, roomId: String, sessionId: String): WithHeldSessionEntity? { + return get(realm, roomId, sessionId) + ?: realm.createObject().apply { + this.roomId = roomId + this.algorithm = MXCRYPTO_ALGORITHM_MEGOLM + this.sessionId = sessionId + } +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt index cd646c8703..141403b6d4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt @@ -45,6 +45,12 @@ internal object EventMapper { eventEntity.redacts = event.redacts eventEntity.age = event.unsignedData?.age ?: event.originServerTs eventEntity.unsignedData = uds + + eventEntity.decryptionResultJson = event.mxDecryptionResult?.let { + MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(it) + } + eventEntity.decryptionErrorReason = event.mCryptoErrorReason + eventEntity.decryptionErrorCode = event.mCryptoError?.name return eventEntity } @@ -85,6 +91,7 @@ internal object EventMapper { it.mCryptoError = eventEntity.decryptionErrorCode?.let { errorCode -> MXCryptoError.ErrorType.valueOf(errorCode) } + it.mCryptoErrorReason = eventEntity.decryptionErrorReason } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt index 7e69e84840..8ee5aeea8b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt @@ -37,6 +37,7 @@ internal open class EventEntity(@Index var eventId: String = "", var redacts: String? = null, var decryptionResultJson: String? = null, var decryptionErrorCode: String? = null, + var decryptionErrorReason: String? = null, var ageLocalTs: Long? = null ) : RealmObject() { @@ -62,5 +63,6 @@ internal open class EventEntity(@Index var eventId: String = "", val adapter = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java) decryptionResultJson = adapter.toJson(decryptionResult) decryptionErrorCode = null + decryptionErrorReason = null } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt index e129513d4b..2f9af828ae 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt @@ -115,9 +115,10 @@ internal class TimelineEventDecryptor @Inject constructor( eventEntity.setDecryptionResult(result) } catch (e: MXCryptoError) { Timber.v(e, "Failed to decrypt event $eventId") - if (e is MXCryptoError.Base && e.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) { + if (e is MXCryptoError.Base /*&& e.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID*/) { // Keep track of unknown sessions to automatically try to decrypt on new session eventEntity.decryptionErrorCode = e.errorType.name + eventEntity.decryptionErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription event.content?.toModel()?.let { content -> content.sessionId?.let { sessionId -> synchronized(unknownSessionsFailure) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt index e778e3455e..5c3b68cf4f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.session.sync import im.vector.matrix.android.R +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 @@ -51,6 +52,7 @@ import im.vector.matrix.android.internal.session.room.membership.RoomMemberEvent import im.vector.matrix.android.internal.session.room.read.FullyReadContent import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection +import im.vector.matrix.android.internal.session.room.timeline.TimelineEventDecryptor import im.vector.matrix.android.internal.session.room.typing.TypingEventContent import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync import im.vector.matrix.android.internal.session.sync.model.RoomSync @@ -71,7 +73,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle private val roomMemberEventHandler: RoomMemberEventHandler, private val roomTypingUsersHandler: RoomTypingUsersHandler, @UserId private val userId: String, - private val eventBus: EventBus) { + private val eventBus: EventBus, + private val timelineEventDecryptor: TimelineEventDecryptor) { sealed class HandlingStrategy { data class JOINED(val data: Map) : HandlingStrategy() @@ -162,7 +165,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle roomSync.timeline.events, roomSync.timeline.prevToken, roomSync.timeline.limited, - syncLocalTimestampMillis + syncLocalTimestampMillis, + !isInitialSync ) roomEntity.addOrUpdate(chunkEntity) } @@ -259,8 +263,9 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle eventList: List, prevToken: String? = null, isLimited: Boolean = true, - syncLocalTimestampMillis: Long): ChunkEntity { - val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId) + syncLocalTimestampMillis: Long, + decryptOnTheFly: Boolean): ChunkEntity { + val lastChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomEntity.roomId) val chunkEntity = if (!isLimited && lastChunk != null) { lastChunk } else { @@ -278,6 +283,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle continue } eventIds.add(event.eventId) + + if (event.isEncrypted() && decryptOnTheFly) { + decryptIfNeeded(event, roomId) + } + val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm) if (event.stateKey != null) { @@ -295,9 +305,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle val rootStateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root ContentMapper.map(rootStateEvent?.content).toModel() } + chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser) // Give info to crypto module cryptoService.onLiveEvent(roomEntity.roomId, event) + // Try to remove local echo event.unsignedData?.transactionId?.also { val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it) @@ -324,6 +336,23 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle return chunkEntity } + private fun decryptIfNeeded(event: Event, roomId: String) { + try { + val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "") + event.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) + } catch (e: MXCryptoError) { + if (e is MXCryptoError.Base) { + event.mCryptoError = e.errorType + event.mCryptoErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription + } + } + } + data class EphemeralResult( val typingUserIds: List = emptyList() ) diff --git a/vector/src/main/java/im/vector/riotx/core/resources/DrawableProvider.kt b/vector/src/main/java/im/vector/riotx/core/resources/DrawableProvider.kt new file mode 100644 index 0000000000..c71def27f2 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/resources/DrawableProvider.kt @@ -0,0 +1,37 @@ +/* + * 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.riotx.core.resources + +import android.content.Context +import android.graphics.drawable.Drawable +import androidx.annotation.ColorInt +import androidx.annotation.DrawableRes +import androidx.core.content.ContextCompat +import im.vector.riotx.features.themes.ThemeUtils +import javax.inject.Inject + +class DrawableProvider @Inject constructor(private val context: Context) { + + fun getDrawable(@DrawableRes colorRes: Int): Drawable? { + return ContextCompat.getDrawable(context, colorRes) + } + fun getDrawable(@DrawableRes colorRes: Int, @ColorInt color: Int): Drawable? { + return ContextCompat.getDrawable(context, colorRes)?.let { + ThemeUtils.tintDrawableWithColor(it, color) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt index 512fffa29e..1c82dc0b1c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt @@ -19,9 +19,11 @@ package im.vector.riotx.features.home.room.detail.timeline.factory 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.room.timeline.TimelineEvent +import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.resources.DrawableProvider import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider @@ -29,6 +31,7 @@ import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformat import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_ import im.vector.riotx.features.home.room.detail.timeline.tools.createLinkMovementMethod +import me.gujun.android.span.image import me.gujun.android.span.span import javax.inject.Inject @@ -37,6 +40,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat private val colorProvider: ColorProvider, private val stringProvider: StringProvider, private val avatarSizeProvider: AvatarSizeProvider, + private val drawableProvider: DrawableProvider, private val attributesFactory: MessageItemAttributesFactory) { fun create(event: TimelineEvent, @@ -48,20 +52,62 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat return when { EventType.ENCRYPTED == event.root.getClearType() -> { val cryptoError = event.root.mCryptoError - val errorDescription = - if (cryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) { - stringProvider.getString(R.string.notice_crypto_error_unkwown_inbound_session_id) - } else { - // TODO i18n - cryptoError?.name +// val errorDescription = +// if (cryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) { +// stringProvider.getString(R.string.notice_crypto_error_unkwown_inbound_session_id) +// } else { +// // TODO i18n +// cryptoError?.name +// } + + val colorFromAttribute = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) + val spannableStr = if(cryptoError == null) { + span(stringProvider.getString(R.string.encrypted_message)) { + textStyle = "italic" + textColor = colorFromAttribute + } + } else { + when(cryptoError) { + MXCryptoError.ErrorType.KEYS_WITHHELD -> { +// val why = when (event.root.mCryptoErrorReason) { +// WithHeldCode.BLACKLISTED.value -> stringProvider.getString(R.string.crypto_error_withheld_blacklisted) +// WithHeldCode.UNVERIFIED.value -> stringProvider.getString(R.string.crypto_error_withheld_unverified) +// else -> stringProvider.getString(R.string.crypto_error_withheld_generic) +// } + //stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, why) + span { + apply { + drawableProvider.getDrawable(R.drawable.ic_forbidden, colorFromAttribute)?.let { + image(it, "baseline") + } + } + span(stringProvider.getString(R.string.notice_crypto_unable_to_decrypt_final)) { + textStyle = "italic" + textColor = colorFromAttribute + } + } + } + else -> { + span { + apply { + drawableProvider.getDrawable(R.drawable.ic_clock, colorFromAttribute)?.let { + image(it, "baseline") + } + } + span(stringProvider.getString(R.string.notice_crypto_unable_to_decrypt_friendly)) { + textStyle = "italic" + textColor = colorFromAttribute + } + } + + } } - val message = stringProvider.getString(R.string.encrypted_message).takeIf { cryptoError == null } - ?: stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, errorDescription) - val spannableStr = span(message) { - textStyle = "italic" - textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) } +// val spannableStr = span(message) { +// textStyle = "italic" +// textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) +// } // TODO This is not correct format for error, change it diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt index 3a1d4859af..e4745a1a31 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt @@ -385,7 +385,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( // } // } - sendToUnverifiedDevicesPref.isChecked = false sendToUnverifiedDevicesPref.isChecked = session.cryptoService().getGlobalBlacklistUnverifiedDevices() diff --git a/vector/src/main/res/drawable/ic_clock.xml b/vector/src/main/res/drawable/ic_clock.xml new file mode 100644 index 0000000000..7102ba2e9e --- /dev/null +++ b/vector/src/main/res/drawable/ic_clock.xml @@ -0,0 +1,21 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_forbidden.xml b/vector/src/main/res/drawable/ic_forbidden.xml new file mode 100644 index 0000000000..eb102d07cb --- /dev/null +++ b/vector/src/main/res/drawable/ic_forbidden.xml @@ -0,0 +1,11 @@ + + + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 3b3af55d1f..525a7efa02 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2501,4 +2501,9 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming Topic You changed room settings successfully + You cannot access this message + Waiting for this message, this may take a while + You have been blocked + Session not trusted by sender + Sender purposely did not send the keys diff --git a/vector/src/main/res/xml/vector_settings_security_privacy.xml b/vector/src/main/res/xml/vector_settings_security_privacy.xml index f394e319c7..8b4823eac9 100644 --- a/vector/src/main/res/xml/vector_settings_security_privacy.xml +++ b/vector/src/main/res/xml/vector_settings_security_privacy.xml @@ -29,7 +29,7 @@ From f9d931960b428d6364e886e8dded6a4cbc092bc0 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 18 May 2020 18:47:16 +0200 Subject: [PATCH 02/15] rename param --- .../matrix/android/internal/session/sync/RoomSyncHandler.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt index 5c3b68cf4f..3be357920c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt @@ -166,7 +166,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle roomSync.timeline.prevToken, roomSync.timeline.limited, syncLocalTimestampMillis, - !isInitialSync + isInitialSync ) roomEntity.addOrUpdate(chunkEntity) } @@ -264,7 +264,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle prevToken: String? = null, isLimited: Boolean = true, syncLocalTimestampMillis: Long, - decryptOnTheFly: Boolean): ChunkEntity { + isInitialSync: Boolean): ChunkEntity { val lastChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomEntity.roomId) val chunkEntity = if (!isLimited && lastChunk != null) { lastChunk @@ -284,7 +284,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle } eventIds.add(event.eventId) - if (event.isEncrypted() && decryptOnTheFly) { + if (event.isEncrypted() && !isInitialSync) { decryptIfNeeded(event, roomId) } From 36de891451e4c15abd99a5bbb4a2ca03797c32c1 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 19 May 2020 09:44:34 +0200 Subject: [PATCH 03/15] E2e lab error display --- .../home/room/detail/RoomDetailAction.kt | 1 + .../home/room/detail/RoomDetailFragment.kt | 30 ++++- .../home/room/detail/RoomDetailViewEvents.kt | 2 + .../home/room/detail/RoomDetailViewModel.kt | 17 +++ .../timeline/TimelineEventController.kt | 16 +-- .../action/MessageActionsEpoxyController.kt | 3 + .../action/MessageActionsViewModel.kt | 2 + .../timeline/factory/EncryptedItemFactory.kt | 53 +++++---- .../factory/MergedHeaderItemFactory.kt | 96 ++++++++++++++- .../helper/MessageItemAttributesFactory.kt | 3 +- .../detail/timeline/item/MergedUTDItem.kt | 112 ++++++++++++++++++ .../features/settings/VectorPreferences.kt | 5 + .../item_timeline_event_base_noinfo.xml | 7 ++ .../item_timeline_event_merged_utd_stub.xml | 77 ++++++++++++ vector/src/main/res/values/strings.xml | 10 +- .../src/main/res/xml/vector_settings_labs.xml | 4 + 16 files changed, 393 insertions(+), 45 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedUTDItem.kt create mode 100644 vector/src/main/res/layout/item_timeline_event_merged_utd_stub.xml diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt index 2cca3c013d..354591b618 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt @@ -75,6 +75,7 @@ sealed class RoomDetailAction : VectorViewModelAction { data class DeclineVerificationRequest(val transactionId: String, val otherUserId: String) : RoomDetailAction() data class RequestVerification(val userId: String) : RoomDetailAction() data class ResumeVerification(val transactionId: String, val otherUserId: String?) : RoomDetailAction() + data class TapOnFailedToDecrypt(val eventId: String) : RoomDetailAction() data class ReRequestKeys(val eventId: String) : RoomDetailAction() object SelectStickerAttachment : RoomDetailAction() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 4995c16bf9..a450e10be6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -86,6 +86,8 @@ import im.vector.matrix.android.api.session.widgets.model.WidgetType import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt +import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent +import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode import im.vector.riotx.R import im.vector.riotx.core.dialogs.ConfirmationDialogBuilder import im.vector.riotx.core.dialogs.withColoredButton @@ -340,6 +342,7 @@ class RoomDetailFragment @Inject constructor( is RoomDetailViewEvents.DownloadFileState -> handleDownloadFileState(it) is RoomDetailViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it) is RoomDetailViewEvents.SendMessageResult -> renderSendMessageResult(it) + is RoomDetailViewEvents.ShowE2EErrorMessage -> displayE2eError(it.withHeldCode) RoomDetailViewEvents.DisplayPromptForIntegrationManager -> displayPromptForIntegrationManager() is RoomDetailViewEvents.OpenStickerPicker -> openStickerPicker(it) is RoomDetailViewEvents.DisplayEnableIntegrationsWarning -> displayDisabledIntegrationDialog() @@ -941,6 +944,20 @@ class RoomDetailFragment @Inject constructor( .show() } + private fun displayE2eError(withHeldCode: WithHeldCode?) { + val msgId = when (withHeldCode) { + WithHeldCode.BLACKLISTED -> R.string.crypto_error_withheld_blacklisted + WithHeldCode.UNVERIFIED -> R.string.crypto_error_withheld_unverified + WithHeldCode.UNAUTHORISED, + WithHeldCode.UNAVAILABLE -> R.string.crypto_error_withheld_generic + else -> R.string.notice_crypto_unable_to_decrypt_friendly_desc + } + AlertDialog.Builder(requireActivity()) + .setMessage(msgId) + .setPositiveButton(R.string.ok, null) + .show() + } + private fun promptReasonToReportContent(action: EventSharedAction.ReportContentCustom) { val inflater = requireActivity().layoutInflater val layout = inflater.inflate(R.layout.dialog_report_content, null) @@ -1205,13 +1222,18 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.handle(RoomDetailAction.LoadMoreTimelineEvents(direction)) } - override fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View) { - if (messageContent is MessageVerificationRequestContent) { - roomDetailViewModel.handle(RoomDetailAction.ResumeVerification(informationData.eventId, null)) + override fun onEventCellClicked(informationData: MessageInformationData, messageContent: Any?, view: View) { + when (messageContent) { + is MessageVerificationRequestContent -> { + roomDetailViewModel.handle(RoomDetailAction.ResumeVerification(informationData.eventId, null)) + } + is EncryptedEventContent -> { + roomDetailViewModel.handle(RoomDetailAction.TapOnFailedToDecrypt(informationData.eventId)) + } } } - override fun onEventLongClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View): Boolean { + override fun onEventLongClicked(informationData: MessageInformationData, messageContent: Any?, view: View): Boolean { view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) val roomId = roomDetailArgs.roomId diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewEvents.kt index 560da2e116..6ed5373b58 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewEvents.kt @@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.detail import androidx.annotation.StringRes import im.vector.matrix.android.api.session.widgets.model.Widget +import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode import im.vector.riotx.core.platform.VectorViewEvents import im.vector.riotx.features.command.Command import java.io.File @@ -33,6 +34,7 @@ sealed class RoomDetailViewEvents : VectorViewEvents { data class ActionFailure(val action: RoomDetailAction, val throwable: Throwable) : RoomDetailViewEvents() data class ShowMessage(val message: String) : RoomDetailViewEvents() + data class ShowE2EErrorMessage(val withHeldCode: WithHeldCode?) : RoomDetailViewEvents() data class NavigateToEvent(val eventId: String) : RoomDetailViewEvents() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 3fd383c5cd..7a3f77039e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -32,6 +32,7 @@ import im.vector.matrix.android.api.MatrixPatterns import im.vector.matrix.android.api.NoOpMatrixCallback import im.vector.matrix.android.api.query.QueryStringValue import im.vector.matrix.android.api.session.Session +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.isImageMessage import im.vector.matrix.android.api.session.events.model.isTextMessage @@ -59,6 +60,8 @@ import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent import im.vector.matrix.android.api.util.toOptional import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent +import im.vector.matrix.rx.asObservable +import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode import im.vector.matrix.rx.rx import im.vector.matrix.rx.unwrap import im.vector.riotx.R @@ -259,6 +262,7 @@ class RoomDetailViewModel @AssistedInject constructor( is RoomDetailAction.RequestVerification -> handleRequestVerification(action) is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action) is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action) + is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action) is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment() is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager() is RoomDetailAction.StartCall -> handleStartCall(action) @@ -1034,6 +1038,19 @@ class RoomDetailViewModel @AssistedInject constructor( } } + private fun handleTapOnFailedToDecrypt(action: RoomDetailAction.TapOnFailedToDecrypt) { + room.getTimeLineEvent(action.eventId)?.let { + val code = when (it.root.mCryptoError) { + MXCryptoError.ErrorType.KEYS_WITHHELD -> { + WithHeldCode.fromCode(it.root.mCryptoErrorReason) + } + else -> null + } + + _viewEvents.post(RoomDetailViewEvents.ShowE2EErrorMessage(code)) + } + } + private fun handleReplyToOptions(action: RoomDetailAction.ReplyToOptions) { room.sendOptionsReply(action.eventId, action.optionIndex, action.optionValue) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index e074af1da6..095340caee 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -26,7 +26,6 @@ import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.VisibilityState import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent -import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageFileContent import im.vector.matrix.android.api.session.room.model.message.MessageImageInfoContent import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent @@ -89,8 +88,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } interface BaseCallback { - fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View) - fun onEventLongClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View): Boolean + fun onEventCellClicked(informationData: MessageInformationData, messageContent: Any?, view: View) + fun onEventLongClicked(informationData: MessageInformationData, messageContent: Any?, view: View): Boolean } interface AvatarCallback { @@ -283,13 +282,16 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private fun getModels(): List> { buildCacheItemsIfNeeded() return modelCache - .map { - val eventModel = if (it == null || mergedHeaderItemFactory.isCollapsed(it.localId)) { + .map { cacheItemData -> + val eventModel = if (cacheItemData == null || mergedHeaderItemFactory.isCollapsed(cacheItemData.localId)) { null } else { - it.eventModel + cacheItemData.eventModel } - listOf(eventModel, it?.mergedHeaderModel, it?.formattedDayModel) + listOf(eventModel, + cacheItemData?.mergedHeaderModel, + cacheItemData?.formattedDayModel?.takeIf { eventModel != null || cacheItemData.mergedHeaderModel != null } + ) } .flatten() .filterNotNull() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt index e77d9ec73f..3eeabd801b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt @@ -18,6 +18,9 @@ package im.vector.riotx.features.home.room.detail.timeline.action import android.view.View import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.mvrx.Success +import im.vector.matrix.android.api.session.crypto.MXCryptoError +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode import im.vector.riotx.EmojiCompatFontProvider import im.vector.riotx.R import im.vector.riotx.core.epoxy.bottomsheet.BottomSheetQuickReactionsItem diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 6c192105d7..8aed581804 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -15,6 +15,8 @@ */ package im.vector.riotx.features.home.room.detail.timeline.action +import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.Async import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt index 1c82dc0b1c..6a135d528d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt @@ -18,8 +18,9 @@ package im.vector.riotx.features.home.room.detail.timeline.factory 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.toModel import im.vector.matrix.android.api.session.room.timeline.TimelineEvent -import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode +import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.resources.ColorProvider @@ -31,6 +32,7 @@ import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformat import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_ import im.vector.riotx.features.home.room.detail.timeline.tools.createLinkMovementMethod +import im.vector.riotx.features.settings.VectorPreferences import me.gujun.android.span.image import me.gujun.android.span.span import javax.inject.Inject @@ -41,7 +43,8 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat private val stringProvider: StringProvider, private val avatarSizeProvider: AvatarSizeProvider, private val drawableProvider: DrawableProvider, - private val attributesFactory: MessageItemAttributesFactory) { + private val attributesFactory: MessageItemAttributesFactory, + private val vectorPreferences: VectorPreferences) { fun create(event: TimelineEvent, nextEvent: TimelineEvent?, @@ -52,29 +55,18 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat return when { EventType.ENCRYPTED == event.root.getClearType() -> { val cryptoError = event.root.mCryptoError -// val errorDescription = -// if (cryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) { -// stringProvider.getString(R.string.notice_crypto_error_unkwown_inbound_session_id) -// } else { -// // TODO i18n -// cryptoError?.name -// } - val colorFromAttribute = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) - val spannableStr = if(cryptoError == null) { + val spannableStr = if (vectorPreferences.hideE2ETechnicalErrors()) { + + val colorFromAttribute = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) + if (cryptoError == null) { span(stringProvider.getString(R.string.encrypted_message)) { textStyle = "italic" textColor = colorFromAttribute } } else { - when(cryptoError) { + when (cryptoError) { MXCryptoError.ErrorType.KEYS_WITHHELD -> { -// val why = when (event.root.mCryptoErrorReason) { -// WithHeldCode.BLACKLISTED.value -> stringProvider.getString(R.string.crypto_error_withheld_blacklisted) -// WithHeldCode.UNVERIFIED.value -> stringProvider.getString(R.string.crypto_error_withheld_unverified) -// else -> stringProvider.getString(R.string.crypto_error_withheld_generic) -// } - //stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, why) span { apply { drawableProvider.getDrawable(R.drawable.ic_forbidden, colorFromAttribute)?.let { @@ -87,7 +79,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat } } } - else -> { + else -> { span { apply { drawableProvider.getDrawable(R.drawable.ic_clock, colorFromAttribute)?.let { @@ -99,20 +91,29 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat textColor = colorFromAttribute } } - } } + } + } else { + val errorDescription = + if (cryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) { + stringProvider.getString(R.string.notice_crypto_error_unkwown_inbound_session_id) + } else { + // TODO i18n + cryptoError?.name + } + val message = stringProvider.getString(R.string.encrypted_message).takeIf { cryptoError == null } + ?: stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, errorDescription) + span(message) { + textStyle = "italic" + textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) + } } -// val spannableStr = span(message) { -// textStyle = "italic" -// textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) -// } - // TODO This is not correct format for error, change it val informationData = messageInformationDataFactory.create(event, nextEvent) - val attributes = attributesFactory.create(null, informationData, callback) + val attributes = attributesFactory.create(event.root.content.toModel(), informationData, callback) return MessageTextItem_() .leftGuideline(avatarSizeProvider.leftGuideline) .highlighted(highlight) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index 6616025110..e82531cef7 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -16,6 +16,7 @@ package im.vector.riotx.features.home.room.detail.timeline.factory +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.toModel import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent @@ -36,11 +37,15 @@ import im.vector.riotx.features.home.room.detail.timeline.item.MergedMembershipE import im.vector.riotx.features.home.room.detail.timeline.item.MergedMembershipEventsItem_ import im.vector.riotx.features.home.room.detail.timeline.item.MergedRoomCreationItem import im.vector.riotx.features.home.room.detail.timeline.item.MergedRoomCreationItem_ +import im.vector.riotx.features.home.room.detail.timeline.item.MergedUTDItem +import im.vector.riotx.features.home.room.detail.timeline.item.MergedUTDItem_ +import im.vector.riotx.features.settings.VectorPreferences import javax.inject.Inject -class MergedHeaderItemFactory @Inject constructor(private val avatarRenderer: AvatarRenderer, +class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, + private val avatarRenderer: AvatarRenderer, private val avatarSizeProvider: AvatarSizeProvider, - private val activeSessionHolder: ActiveSessionHolder) { + private val vectorPreferences: VectorPreferences) { private val collapsedEventIds = linkedSetOf() private val mergeItemCollapseStates = HashMap() @@ -58,7 +63,9 @@ class MergedHeaderItemFactory @Inject constructor(private val avatarRenderer: Av callback: TimelineEventController.Callback?, requestModelBuild: () -> Unit) : BasedMergedItem<*>? { - return if (nextEvent?.root?.getClearType() == EventType.STATE_ROOM_CREATE + return if (shouldMergedAsCannotDecryptGroup(event, nextEvent)) { + buildUTDMergedSummary(currentPosition, items, event, eventIdToHighlight, requestModelBuild, callback) + } else if (nextEvent?.root?.getClearType() == EventType.STATE_ROOM_CREATE && event.isRoomConfiguration(nextEvent.root.getClearContent()?.toModel()?.creator)) { // It's the first item before room.create // Collapse all room configuration events @@ -130,6 +137,89 @@ class MergedHeaderItemFactory @Inject constructor(private val avatarRenderer: Av } } + // Event should be UTD + // Next event should not + private fun shouldMergedAsCannotDecryptGroup(event: TimelineEvent, nextEvent: TimelineEvent?) : Boolean { + if (!vectorPreferences.hideE2ETechnicalErrors()) return false + // if event is not UTD return false + if (event.root.getClearType() != EventType.ENCRYPTED || event.root.mCryptoError != MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) return false + // At this point event cannot be decrypted + // Let's check if older event is not UTD + return nextEvent == null || nextEvent.root.getClearType() != EventType.ENCRYPTED + } + + + private fun buildUTDMergedSummary(currentPosition: Int, + items: List, + event: TimelineEvent, + eventIdToHighlight: String?, + requestModelBuild: () -> Unit, + callback: TimelineEventController.Callback?): MergedUTDItem_? { + + var prevEvent = if (currentPosition > 0) items[currentPosition - 1] else null + var tmpPos = currentPosition - 1 + val mergedEvents = ArrayList().also { it.add(event) } + + while (prevEvent != null + && prevEvent.root.getClearType() == EventType.ENCRYPTED + && prevEvent.root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) { + mergedEvents.add(prevEvent) + tmpPos-- + prevEvent = if (tmpPos >= 0) items[tmpPos] else null + } + + if (mergedEvents.size < 3) return null + + var highlighted = false + val mergedData = ArrayList(mergedEvents.size) + mergedEvents.reversed() + .forEach { mergedEvent -> + if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) { + highlighted = true + } + val senderAvatar = mergedEvent.senderAvatar + val senderName = mergedEvent.getDisambiguatedDisplayName() + val data = BasedMergedItem.Data( + userId = mergedEvent.root.senderId ?: "", + avatarUrl = senderAvatar, + memberName = senderName, + localId = mergedEvent.localId, + eventId = mergedEvent.root.eventId ?: "" + ) + mergedData.add(data) + } + val mergedEventIds = mergedEvents.map { it.localId } + // We try to find if one of the item id were used as mergeItemCollapseStates key + // => handle case where paginating from mergeable events and we get more + val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull() + val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey) + ?: true + val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState } + if (isCollapsed) { + collapsedEventIds.addAll(mergedEventIds) + } else { + collapsedEventIds.removeAll(mergedEventIds) + } + val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() } + + val attributes = MergedUTDItem.Attributes( + isCollapsed = isCollapsed, + mergeData = mergedData, + avatarRenderer = avatarRenderer, + onCollapsedStateChanged = { + mergeItemCollapseStates[event.localId] = it + requestModelBuild() + } + ) + return MergedUTDItem_() + .id(mergeId) + .leftGuideline(avatarSizeProvider.leftGuideline) + .highlighted(isCollapsed && highlighted) + .attributes(attributes) + .also { + it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents)) + } + } private fun buildRoomCreationMergedSummary(currentPosition: Int, items: List, event: TimelineEvent, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt index 7433b03408..7646fd37a5 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt @@ -18,7 +18,6 @@ package im.vector.riotx.features.home.room.detail.timeline.helper import android.view.View -import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.riotx.EmojiCompatFontProvider import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.features.home.AvatarRenderer @@ -34,7 +33,7 @@ class MessageItemAttributesFactory @Inject constructor( private val avatarSizeProvider: AvatarSizeProvider, private val emojiCompatFontProvider: EmojiCompatFontProvider) { - fun create(messageContent: MessageContent?, + fun create(messageContent: Any?, informationData: MessageInformationData, callback: TimelineEventController.Callback?): AbsMessageItem.Attributes { return AbsMessageItem.Attributes( diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedUTDItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedUTDItem.kt new file mode 100644 index 0000000000..715d329168 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedUTDItem.kt @@ -0,0 +1,112 @@ +/* + * 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.riotx.features.home.room.detail.timeline.item + +import android.view.ViewGroup +import android.widget.RelativeLayout +import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.R +import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController + +@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo) +abstract class MergedUTDItem : BasedMergedItem() { + + @EpoxyAttribute + override lateinit var attributes: Attributes + + override fun getViewType() = STUB_ID + + override fun bind(holder: Holder) { + super.bind(holder) + + holder.mergedTile.updateLayoutParams { + this.marginEnd = leftGuideline + } +// if (attributes.isCollapsed) { +// // Take the oldest data +// val data = distinctMergeData.lastOrNull() +// +// val summary = holder.expandView.resources.getString(R.string.room_created_summary_item, +// data?.memberName ?: data?.userId ?: "") +// holder.summaryView.text = summary +// holder.summaryView.visibility = View.VISIBLE +// holder.avatarView.visibility = View.VISIBLE +// if (data != null) { +// holder.avatarView.visibility = View.VISIBLE +// attributes.avatarRenderer.render(data.toMatrixItem(), holder.avatarView) +// } else { +// holder.avatarView.visibility = View.GONE +// } +// +// if (attributes.hasEncryptionEvent) { +// holder.encryptionTile.isVisible = true +// holder.encryptionTile.updateLayoutParams { +// this.marginEnd = leftGuideline +// } +// if (attributes.isEncryptionAlgorithmSecure) { +// holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_enabled) +// holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_enabled_tile_description) +// holder.e2eTitleDescriptionView.textAlignment = View.TEXT_ALIGNMENT_CENTER +// holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds( +// ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_black), +// null, null, null +// ) +// } else { +// holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_not_enabled) +// holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_unknown_algorithm_tile_description) +// holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds( +// ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_warning), +// null, null, null +// ) +// } +// } else { +// holder.encryptionTile.isVisible = false +// } +// } else { +// holder.avatarView.visibility = View.INVISIBLE +// holder.summaryView.visibility = View.GONE +// holder.encryptionTile.isGone = true +// } + // No read receipt for this item + holder.readReceiptsView.isVisible = false + } + + class Holder : BasedMergedItem.Holder(STUB_ID) { + // val summaryView by bind(R.id.itemNoticeTextView) +// val avatarView by bind(R.id.itemNoticeAvatarView) + val mergedTile by bind(R.id.mergedUTDTile) +// +// val e2eTitleTextView by bind(R.id.itemVerificationDoneTitleTextView) +// val e2eTitleDescriptionView by bind(R.id.itemVerificationDoneDetailTextView) + } + + companion object { + private const val STUB_ID = R.id.messageContentMergedUTDStub + } + + data class Attributes( + override val isCollapsed: Boolean, + override val mergeData: List, + override val avatarRenderer: AvatarRenderer, + override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null, + override val onCollapsedStateChanged: (Boolean) -> Unit + ) : BasedMergedItem.Attributes +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt index 5f245c883d..9a427b6d9c 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt @@ -144,6 +144,7 @@ class VectorPreferences @Inject constructor(private val context: Context) { private const val SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY = "SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY" private const val SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY = "SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY" private const val SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY = "SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY" + private const val SETTINGS_LABS_HIDE_TECHNICAL_E2E_ERRORS = "SETTINGS_LABS_HIDE_TECHNICAL_E2E_ERRORS" // analytics const val SETTINGS_USE_ANALYTICS_KEY = "SETTINGS_USE_ANALYTICS_KEY" @@ -265,6 +266,10 @@ class VectorPreferences @Inject constructor(private val context: Context) { return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY, true) } + fun hideE2ETechnicalErrors(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_LABS_HIDE_TECHNICAL_E2E_ERRORS, true) + } + fun labAllowedExtendedLogging(): Boolean { return developerMode() && defaultPrefs.getBoolean(SETTINGS_LABS_ALLOW_EXTENDED_LOGS, false) } diff --git a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml index 8d0d9bb513..f8d46bb04e 100644 --- a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml +++ b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml @@ -61,6 +61,13 @@ tools:layout_marginTop="240dp" tools:visibility="visible" /> + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 525a7efa02..29033a2f23 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1734,6 +1734,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming Name or ID (#example:matrix.org) Enable swipe to reply in timeline + Hide failed to decrypt technical details in timeline Link copied to clipboard @@ -2503,7 +2504,10 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming You cannot access this message Waiting for this message, this may take a while - You have been blocked - Session not trusted by sender - Sender purposely did not send the keys + Cannot Decrypt + Due to end-to-end encryption, you might need to wait for someone\'s message to arrive because the encryption keys were not properly sent to you. + You cannot access this message because you have been blocked by the sender + You cannot access this message because your session is not trusted by the sender + You cannot access this message because the sender purposely did not send the keys + Waiting for encryption history diff --git a/vector/src/main/res/xml/vector_settings_labs.xml b/vector/src/main/res/xml/vector_settings_labs.xml index 2661568f77..0379d49d26 100644 --- a/vector/src/main/res/xml/vector_settings_labs.xml +++ b/vector/src/main/res/xml/vector_settings_labs.xml @@ -40,6 +40,10 @@ android:title="@string/labs_swipe_to_reply_in_timeline" /> + \ No newline at end of file From 4ca0c23e2a2b3624800b0829f920351f561a4da4 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 22 May 2020 14:45:12 +0200 Subject: [PATCH 04/15] Mock Http requests for test --- matrix-sdk-android/build.gradle | 1 + .../im/vector/matrix/android/api/Matrix.kt | 102 ++++++++++++++++++ .../matrix/android/common/CommonTestHelper.kt | 4 +- .../android/common/MockOkHttpInterceptor.kt | 9 +- .../android/common/TestMatrixComponent.kt | 38 +++++++ .../matrix/android/common/TestModule.kt | 27 +++++ .../android/common/TestNetworkModule.kt | 39 +++++++ .../store/db/model/SharedSessionEntity.kt | 37 +++++++ .../store/db/query/SharedSessionQueries.kt | 58 ++++++++++ .../internal/di/InterceptorQualifiers.kt | 23 ++++ .../android/internal/di/MatrixComponent.kt | 7 +- .../android/internal/di/NoOpTestModule.kt | 30 ++++++ .../android/internal/session/SessionModule.kt | 23 +++- .../internal/session/TestInterceptor.kt | 23 ++++ 14 files changed, 416 insertions(+), 5 deletions(-) create mode 100644 matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/api/Matrix.kt create mode 100644 matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestMatrixComponent.kt create mode 100644 matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestModule.kt create mode 100644 matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestNetworkModule.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/SharedSessionEntity.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/SharedSessionQueries.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/InterceptorQualifiers.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NoOpTestModule.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/TestInterceptor.kt diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 982e258c3f..ca92df08c5 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -181,6 +181,7 @@ dependencies { // Plant Timber tree for test testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1' + kaptAndroidTest "com.google.dagger:dagger-compiler:$daggerVersion" androidTestImplementation 'androidx.test:core:1.2.0' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:rules:1.2.0' diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/api/Matrix.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/api/Matrix.kt new file mode 100644 index 0000000000..737ca77245 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/api/Matrix.kt @@ -0,0 +1,102 @@ +/* + * 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.api + +import android.content.Context +import androidx.lifecycle.ProcessLifecycleOwner +import androidx.work.Configuration +import androidx.work.WorkManager +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.BuildConfig +import im.vector.matrix.android.api.MatrixConfiguration +import im.vector.matrix.android.api.auth.AuthenticationService +import im.vector.matrix.android.common.DaggerTestMatrixComponent +import im.vector.matrix.android.internal.SessionManager +import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt +import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments +import im.vector.matrix.android.internal.di.MockHttpInterceptor +import im.vector.matrix.android.internal.network.UserAgentHolder +import im.vector.matrix.android.internal.util.BackgroundDetectionObserver +import okhttp3.Interceptor +import org.matrix.olm.OlmManager +import java.io.InputStream +import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicBoolean +import javax.inject.Inject + +/** + * This is the main entry point to the matrix sdk. + * To get the singleton instance, use getInstance static method. + */ +class Matrix private constructor(context: Context, matrixConfiguration: MatrixConfiguration) { + + @Inject internal lateinit var authenticationService: AuthenticationService + @Inject internal lateinit var userAgentHolder: UserAgentHolder + @Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver + @Inject internal lateinit var olmManager: OlmManager + @Inject internal lateinit var sessionManager: SessionManager + + + init { + Monarchy.init(context) + DaggerTestMatrixComponent.factory().create(context, matrixConfiguration).inject(this) + if (context.applicationContext !is Configuration.Provider) { + WorkManager.initialize(context, Configuration.Builder().setExecutor(Executors.newCachedThreadPool()).build()) + } + ProcessLifecycleOwner.get().lifecycle.addObserver(backgroundDetectionObserver) + } + + fun getUserAgent() = userAgentHolder.userAgent + + fun authenticationService(): AuthenticationService { + return authenticationService + } + + companion object { + + private lateinit var instance: Matrix + private val isInit = AtomicBoolean(false) + + fun initialize(context: Context, matrixConfiguration: MatrixConfiguration) { + if (isInit.compareAndSet(false, true)) { + instance = Matrix(context.applicationContext, matrixConfiguration) + } + } + + fun getInstance(context: Context): Matrix { + if (isInit.compareAndSet(false, true)) { + val appContext = context.applicationContext + if (appContext is MatrixConfiguration.Provider) { + val matrixConfiguration = (appContext as MatrixConfiguration.Provider).providesMatrixConfiguration() + instance = Matrix(appContext, matrixConfiguration) + } else { + throw IllegalStateException("Matrix is not initialized properly." + + " You should call Matrix.initialize or let your application implements MatrixConfiguration.Provider.") + } + } + return instance + } + + fun getSdkVersion(): String { + return BuildConfig.VERSION_NAME + " (" + BuildConfig.GIT_SDK_REVISION + ")" + } + + fun decryptStream(inputStream: InputStream?, elementToDecrypt: ElementToDecrypt): InputStream? { + return MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt) + } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt index fd06acdcb3..f2edc84324 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt @@ -36,6 +36,7 @@ import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.api.session.sync.SyncState +import im.vector.matrix.android.internal.session.TestInterceptor import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay @@ -57,9 +58,10 @@ class CommonTestHelper(context: Context) { val matrix: Matrix + fun getTestInterceptor(session: Session): MockOkHttpInterceptor? = TestNetworkModule.interceptorForSession(session.sessionId) as? MockOkHttpInterceptor + init { Matrix.initialize(context, MatrixConfiguration("TestFlavor")) - matrix = Matrix.getInstance(context) } diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/MockOkHttpInterceptor.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/MockOkHttpInterceptor.kt index a1f95424a6..6747e52e73 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/MockOkHttpInterceptor.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/MockOkHttpInterceptor.kt @@ -15,6 +15,7 @@ */ package im.vector.matrix.android.common +import im.vector.matrix.android.internal.session.TestInterceptor import okhttp3.Interceptor import okhttp3.Protocol import okhttp3.Request @@ -37,7 +38,7 @@ import javax.net.ssl.HttpsURLConnection * AutoDiscovery().findClientConfig("matrix.org", ) * */ -class MockOkHttpInterceptor : Interceptor { +class MockOkHttpInterceptor : TestInterceptor { private var rules: ArrayList = ArrayList() @@ -45,6 +46,12 @@ class MockOkHttpInterceptor : Interceptor { rules.add(rule) } + fun clearRules() { + rules.clear() + } + + override var sessionId: String? = null + override fun intercept(chain: Interceptor.Chain): Response { val originalRequest = chain.request() diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestMatrixComponent.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestMatrixComponent.kt new file mode 100644 index 0000000000..27d00e5896 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestMatrixComponent.kt @@ -0,0 +1,38 @@ +/* + * 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.common + +import android.content.Context +import dagger.BindsInstance +import dagger.Component +import im.vector.matrix.android.api.MatrixConfiguration +import im.vector.matrix.android.internal.auth.AuthModule +import im.vector.matrix.android.internal.di.MatrixComponent +import im.vector.matrix.android.internal.di.MatrixModule +import im.vector.matrix.android.internal.di.MatrixScope +import im.vector.matrix.android.internal.di.NetworkModule + +@Component(modules = [TestModule::class, MatrixModule::class, NetworkModule::class, AuthModule::class, TestNetworkModule::class]) +@MatrixScope +internal interface TestMatrixComponent : MatrixComponent { + + @Component.Factory + interface Factory { + fun create(@BindsInstance context: Context, + @BindsInstance matrixConfiguration: MatrixConfiguration): TestMatrixComponent + } +} diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestModule.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestModule.kt new file mode 100644 index 0000000000..4290df3973 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestModule.kt @@ -0,0 +1,27 @@ +/* + * 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.common + +import dagger.Binds +import dagger.Module +import im.vector.matrix.android.internal.di.MatrixComponent + +@Module +internal abstract class TestModule { + @Binds + abstract fun providesMatrixComponent(testMatrixComponent: TestMatrixComponent): MatrixComponent +} diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestNetworkModule.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestNetworkModule.kt new file mode 100644 index 0000000000..3b3ec47768 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestNetworkModule.kt @@ -0,0 +1,39 @@ +/* + * 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.common + +import dagger.Module +import dagger.Provides +import im.vector.matrix.android.internal.di.MockHttpInterceptor +import im.vector.matrix.android.internal.session.TestInterceptor + +@Module +internal object TestNetworkModule { + + val interceptors = ArrayList() + + fun interceptorForSession(sessionId: String): TestInterceptor? = interceptors.firstOrNull { it.sessionId == sessionId } + + @Provides + @JvmStatic + @MockHttpInterceptor + fun providesTestInterceptor(): TestInterceptor? { + return MockOkHttpInterceptor().also { + interceptors.add(it) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/SharedSessionEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/SharedSessionEntity.kt new file mode 100644 index 0000000000..fe4c1ced6b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/SharedSessionEntity.kt @@ -0,0 +1,37 @@ +/* + * 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.store.db.model + +import io.realm.RealmObject +import io.realm.annotations.Index + +/** + * Keep a record of to whom (user/device) a given session should have been shared. + * It will be used to reply to keyshare requests from other users, in order to see if + * this session was originaly shared with a given user + */ +internal open class SharedSessionEntity( + var roomId: String? = null, + var algorithm: String? = null, + @Index var sessionId: String? = null, + @Index var userId: String? = null, + @Index var deviceId: String? = null, + var chainIndex: Int? = null +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/SharedSessionQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/SharedSessionQueries.kt new file mode 100644 index 0000000000..48b84174ac --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/SharedSessionQueries.kt @@ -0,0 +1,58 @@ +/* + * 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.store.db.query + +import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import im.vector.matrix.android.internal.crypto.store.db.model.SharedSessionEntity +import im.vector.matrix.android.internal.crypto.store.db.model.SharedSessionEntityFields +import io.realm.Realm +import io.realm.RealmResults +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +internal fun SharedSessionEntity.Companion.get(realm: Realm, roomId: String?, sessionId: String, userId: String, deviceId: String) + : SharedSessionEntity? { + return realm.where() + .equalTo(SharedSessionEntityFields.ROOM_ID, roomId) + .equalTo(SharedSessionEntityFields.SESSION_ID, sessionId) + .equalTo(SharedSessionEntityFields.ALGORITHM, MXCRYPTO_ALGORITHM_MEGOLM) + .equalTo(SharedSessionEntityFields.USER_ID, userId) + .equalTo(SharedSessionEntityFields.DEVICE_ID, deviceId) + .findFirst() +} + +internal fun SharedSessionEntity.Companion.get(realm: Realm, roomId: String?, sessionId: String) + : RealmResults { + return realm.where() + .equalTo(SharedSessionEntityFields.ROOM_ID, roomId) + .equalTo(SharedSessionEntityFields.SESSION_ID, sessionId) + .equalTo(SharedSessionEntityFields.ALGORITHM, MXCRYPTO_ALGORITHM_MEGOLM) + .findAll() +} + +internal fun SharedSessionEntity.Companion.create(realm: Realm, roomId: String?, sessionId: String, userId: String, deviceId: String, chainIndex: Int) + : SharedSessionEntity { + return realm.createObject().apply { + this.roomId = roomId + this.algorithm = MXCRYPTO_ALGORITHM_MEGOLM + this.sessionId = sessionId + this.userId = userId + this.deviceId = deviceId + this.chainIndex = chainIndex + } +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/InterceptorQualifiers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/InterceptorQualifiers.kt new file mode 100644 index 0000000000..3a1b7d569b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/InterceptorQualifiers.kt @@ -0,0 +1,23 @@ +/* + * 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.di + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class MockHttpInterceptor diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt index 4d6082d50b..83388f4a57 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt @@ -27,14 +27,16 @@ import im.vector.matrix.android.api.auth.AuthenticationService import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.auth.AuthModule import im.vector.matrix.android.internal.auth.SessionParamsStore +import im.vector.matrix.android.internal.session.TestInterceptor import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.BackgroundDetectionObserver import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers +import okhttp3.Interceptor import okhttp3.OkHttpClient import org.matrix.olm.OlmManager import java.io.File -@Component(modules = [MatrixModule::class, NetworkModule::class, AuthModule::class]) +@Component(modules = [MatrixModule::class, NetworkModule::class, AuthModule::class, NoOpTestModule::class]) @MatrixScope internal interface MatrixComponent { @@ -45,6 +47,9 @@ internal interface MatrixComponent { @Unauthenticated fun okHttpClient(): OkHttpClient + @MockHttpInterceptor + fun testInterceptor(): TestInterceptor? + fun authenticationService(): AuthenticationService fun context(): Context diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NoOpTestModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NoOpTestModule.kt new file mode 100644 index 0000000000..e2e29b1fa5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NoOpTestModule.kt @@ -0,0 +1,30 @@ +/* + * 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.di + +import dagger.Module +import dagger.Provides +import im.vector.matrix.android.internal.session.TestInterceptor + +@Module +internal object NoOpTestModule { + + @Provides + @JvmStatic + @MockHttpInterceptor + fun providesTestInterceptor(): TestInterceptor? = null +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index 7faf2ba172..fc5c43670d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -43,6 +43,7 @@ import im.vector.matrix.android.internal.crypto.verification.VerificationMessage import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory import im.vector.matrix.android.internal.di.Authenticated import im.vector.matrix.android.internal.di.DeviceId +import im.vector.matrix.android.internal.di.MockHttpInterceptor import im.vector.matrix.android.internal.di.SessionCacheDirectory import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.di.SessionFilesDirectory @@ -182,8 +183,26 @@ internal abstract class SessionModule { @SessionScope @Authenticated fun providesOkHttpClient(@Unauthenticated okHttpClient: OkHttpClient, - @Authenticated accessTokenProvider: AccessTokenProvider): OkHttpClient { - return okHttpClient.addAccessTokenInterceptor(accessTokenProvider) + accessTokenInterceptor: AccessTokenInterceptor, + @SessionId sessionId: String, + @MockHttpInterceptor testInterceptor: TestInterceptor?): OkHttpClient { + return okHttpClient.newBuilder() + .apply { + // Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor + val existingCurlInterceptors = interceptors().filterIsInstance() + interceptors().removeAll(existingCurlInterceptors) + + addInterceptor(accessTokenInterceptor) + if (testInterceptor != null) { + testInterceptor.sessionId = sessionId + addInterceptor(testInterceptor) + } + // Re add eventually the curl logging interceptors + existingCurlInterceptors.forEach { + addInterceptor(it) + } + } + .build() } @JvmStatic diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/TestInterceptor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/TestInterceptor.kt new file mode 100644 index 0000000000..fc8589bd28 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/TestInterceptor.kt @@ -0,0 +1,23 @@ +/* + * 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.session + +import okhttp3.Interceptor + +interface TestInterceptor : Interceptor { + var sessionId: String? +} From 102b8f88d0a674905817cefc0807905da9fe86fd Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 22 May 2020 15:04:37 +0200 Subject: [PATCH 05/15] Persist shared session info (enhance key reshare) --- .../crypto/gossiping/KeyShareTests.kt | 7 ++ .../crypto/gossiping/WithHeldTests.kt | 77 +++++++++++++++++++ .../api/session/crypto/CryptoService.kt | 3 + .../internal/crypto/DefaultCryptoService.kt | 8 +- .../algorithms/megolm/MXMegolmEncryption.kt | 10 +-- .../megolm/MXOutboundSessionInfo.kt | 8 +- .../algorithms/megolm/SharedWithHelper.kt | 38 +++++++++ .../internal/crypto/store/IMXCryptoStore.kt | 5 ++ .../crypto/store/db/RealmCryptoStore.kt | 40 +++++++++- .../store/db/RealmCryptoStoreMigration.kt | 14 ++++ .../crypto/store/db/RealmCryptoStoreModule.kt | 4 +- 11 files changed, 199 insertions(+), 15 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/SharedWithHelper.kt diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt index a25a5abb22..ec8a97abe5 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt @@ -29,6 +29,8 @@ import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.common.CommonTestHelper +import im.vector.matrix.android.common.CryptoTestHelper +import im.vector.matrix.android.common.MockOkHttpInterceptor import im.vector.matrix.android.common.SessionTestParams import im.vector.matrix.android.common.TestConstants import im.vector.matrix.android.internal.crypto.GossipingRequestState @@ -44,6 +46,8 @@ import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.assertTrue import junit.framework.TestCase.fail +import kotlinx.coroutines.delay +import okhttp3.internal.waitMillis import org.junit.Assert import org.junit.FixMethodOrder import org.junit.Test @@ -56,6 +60,7 @@ import java.util.concurrent.CountDownLatch class KeyShareTests : InstrumentedTest { private val mTestHelper = CommonTestHelper(context()) + private val mCryptoTestHelper = CryptoTestHelper(mTestHelper) @Test fun test_DoNotSelfShareIfNotTrusted() { @@ -234,6 +239,7 @@ class KeyShareTests : InstrumentedTest { } if (tx.state == VerificationTxState.ShortCodeReady) { session1ShortCode = tx.getDecimalCodeRepresentation() + Thread.sleep(500) tx.userHasVerifiedShortCode() } } @@ -246,6 +252,7 @@ class KeyShareTests : InstrumentedTest { if (tx is SasVerificationTransaction) { if (tx.state == VerificationTxState.ShortCodeReady) { session2ShortCode = tx.getDecimalCodeRepresentation() + Thread.sleep(500) tx.userHasVerifiedShortCode() } } diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/WithHeldTests.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/WithHeldTests.kt index f73692ea65..581624ba16 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/WithHeldTests.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/WithHeldTests.kt @@ -16,14 +16,18 @@ package im.vector.matrix.android.internal.crypto.gossiping +import android.util.Log import androidx.test.ext.junit.runners.AndroidJUnit4 import im.vector.matrix.android.InstrumentedTest 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.toModel import im.vector.matrix.android.common.CommonTestHelper import im.vector.matrix.android.common.CryptoTestHelper +import im.vector.matrix.android.common.MockOkHttpInterceptor import im.vector.matrix.android.common.SessionTestParams import im.vector.matrix.android.common.TestConstants +import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode import org.junit.Assert import org.junit.FixMethodOrder @@ -123,4 +127,77 @@ class WithHeldTests : InstrumentedTest { mTestHelper.signOutAndClose(bobSession) mTestHelper.signOutAndClose(bobUnverifiedSession) } + + + @Test + fun test_WithHeldNoOlm() { + val testData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + val aliceSession = testData.firstSession + val bobSession = testData.secondSession!! + val aliceInterceptor = mTestHelper.getTestInterceptor(aliceSession) + + // Simulate no OTK + aliceInterceptor!!.addRule(MockOkHttpInterceptor.SimpleRule( + "/keys/claim", + 200, + """ + { "one_time_keys" : {} } + """ + )) + Log.d("#TEST", "Recovery :${aliceSession.sessionParams.credentials.accessToken}") + + val roomAlicePov = aliceSession.getRoom(testData.roomId)!! + + // can we force one-time key shortage?? + + val eventId = mTestHelper.sendTextMessage(roomAlicePov, "first message", 1).first().eventId + + // await for bob session to get the message + mTestHelper.waitWithLatch { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + bobSession.getRoom(testData.roomId)?.getTimeLineEvent(eventId) != null + } + } + + // Previous message should still be undecryptable (partially withheld session) + val eventBobPOV = bobSession.getRoom(testData.roomId)?.getTimeLineEvent(eventId) + try { + // .. might need to wait a bit for stability? + bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "") + Assert.fail("This session should not be able to decrypt") + } catch (failure: Throwable) { + val type = (failure as MXCryptoError.Base).errorType + val technicalMessage = failure.technicalMessage + Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) + Assert.assertEquals("Cause should be unverified", WithHeldCode.NO_OLM.value, technicalMessage) + } + + // Ensure that alice has marked the session to be shared with bob + val sessionId = eventBobPOV!!.root.content.toModel()!!.sessionId!! + val chainIndex = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject(bobSession.myUserId, bobSession.sessionParams.credentials.deviceId) + + Assert.assertEquals("Alice should have marked bob's device for this session", 0, chainIndex) + // Add a new device for bob + + aliceInterceptor.clearRules() + val bobSecondSession = mTestHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(withInitialSync = true)) + // send a second message + val secondMessageId = mTestHelper.sendTextMessage(roomAlicePov, "second message", 1).first().eventId + + // Check that the + // await for bob SecondSession session to get the message + mTestHelper.waitWithLatch { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + bobSecondSession.getRoom(testData.roomId)?.getTimeLineEvent(secondMessageId) != null + } + } + + val chainIndex2 = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject(bobSecondSession.myUserId, bobSecondSession.sessionParams.credentials.deviceId) + + Assert.assertEquals("Alice should have marked bob's device for this session", 1, chainIndex2) + + mTestHelper.signOutAndClose(bobSecondSession) + testData.cleanUp(mTestHelper) + + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt index b3b07f20fb..5e6eb6d4ff 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt @@ -145,4 +145,7 @@ interface CryptoService { fun getIncomingRoomKeyRequests(): List fun getGossipingEventsTrail(): List + + //For testing shared session + fun getSharedWithInfo(roomId: String?, sessionId: String) : MXUsersDevicesMap } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt index 2269b95791..c0033447bd 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt @@ -809,7 +809,7 @@ internal class DefaultCryptoService @Inject constructor( cryptoStore.saveGossipingEvent(event) onSecretSendReceived(event) } - EventType.ROOM_KEY_WITHHELD -> { + EventType.ROOM_KEY_WITHHELD -> { onKeyWithHeldReceived(event) } else -> { @@ -839,7 +839,6 @@ internal class DefaultCryptoService @Inject constructor( alg.onRoomKeyEvent(event, keysBackupService) } - private fun onKeyWithHeldReceived(event: Event) { val withHeldContent = event.getClearContent().toModel() ?: return Unit.also { Timber.e("## CRYPTO | Malformed onKeyWithHeldReceived() : missing fields") @@ -852,7 +851,6 @@ internal class DefaultCryptoService @Inject constructor( Timber.e("## CRYPTO | onKeyWithHeldReceived() : Unable to handle WithHeldContent for ${withHeldContent.algorithm}") return } - } private fun onSecretSendReceived(event: Event) { @@ -1332,6 +1330,10 @@ internal class DefaultCryptoService @Inject constructor( return cryptoStore.getGossipingEventsTrail() } + override fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap { + return cryptoStore.getSharedWithInfo(roomId, sessionId) + } + /* ========================================================================================== * For test only * ========================================================================================== */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt index e75ea9d06a..83a6bce70e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -119,7 +119,7 @@ internal class MXMegolmEncryption( defaultKeysBackupService.maybeBackupKeys() - return MXOutboundSessionInfo(sessionId) + return MXOutboundSessionInfo(sessionId, SharedWithHelper(roomId, sessionId, cryptoStore)) } /** @@ -145,7 +145,7 @@ internal class MXMegolmEncryption( val deviceIds = devicesInRoom.getUserDeviceIds(userId) for (deviceId in deviceIds!!) { val deviceInfo = devicesInRoom.getObject(userId, deviceId) - if (deviceInfo != null && null == safeSession.sharedWithDevices.getObject(userId, deviceId)) { + if (deviceInfo != null && !cryptoStore.wasSessionSharedWithUser(roomId, safeSession.sessionId, userId, deviceId).found) { val devices = shareMap.getOrPut(userId) { ArrayList() } devices.add(deviceInfo) } @@ -247,7 +247,7 @@ internal class MXMegolmEncryption( // for dead devices on every message. for ((userId, devicesToShareWith) in devicesByUser) { for ((deviceId) in devicesToShareWith) { - session.sharedWithDevices.setObject(userId, deviceId, chainIndex) + session.sharedWithHelper.markedSessionAsShared(userId, deviceId, chainIndex) } } @@ -382,7 +382,7 @@ internal class MXMegolmEncryption( .also { Timber.w("Device not found") } // Get the chain index of the key we previously sent this device - val chainIndex = outboundSession?.sharedWithDevices?.getObject(userId, deviceId)?.toLong() ?: return false + val chainIndex = outboundSession?.sharedWithHelper?.wasSharedWith(userId,deviceId) ?: return false .also { Timber.w("[MXMegolmEncryption] reshareKey : ERROR : Never share megolm with this device") } val devicesByUser = mapOf(userId to listOf(deviceInfo)) @@ -401,7 +401,7 @@ internal class MXMegolmEncryption( .fold( { // TODO - payloadJson["content"] = it.exportKeys(chainIndex) ?: "" + payloadJson["content"] = it.exportKeys(chainIndex.toLong()) ?: "" }, { // TODO diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt index 47ec98e5c9..94c8d4494c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt @@ -23,17 +23,14 @@ import timber.log.Timber internal class MXOutboundSessionInfo( // The id of the session - val sessionId: String) { + val sessionId: String, + val sharedWithHelper: SharedWithHelper) { // When the session was created private val creationTime = System.currentTimeMillis() // Number of times this session has been used var useCount: Int = 0 - // Devices with which we have shared the session key - // userId -> {deviceId -> msgindex} - val sharedWithDevices: MXUsersDevicesMap = MXUsersDevicesMap() - fun needsRotation(rotationPeriodMsgs: Int, rotationPeriodMs: Int): Boolean { var needsRotation = false val sessionLifetime = System.currentTimeMillis() - creationTime @@ -53,6 +50,7 @@ internal class MXOutboundSessionInfo( * @return true if we have shared the session with devices which aren't in devicesInRoom. */ fun sharedWithTooManyDevices(devicesInRoom: MXUsersDevicesMap): Boolean { + val sharedWithDevices = sharedWithHelper.sharedWithDevices() val userIds = sharedWithDevices.userIds for (userId in userIds) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/SharedWithHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/SharedWithHelper.kt new file mode 100644 index 0000000000..9a59db391f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/SharedWithHelper.kt @@ -0,0 +1,38 @@ +/* + * 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.algorithms.megolm + +import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap +import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore + +internal class SharedWithHelper( + private val roomId: String, + private val sessionId: String, + private val cryptoStore: IMXCryptoStore) { + + fun sharedWithDevices(): MXUsersDevicesMap { + return cryptoStore.getSharedWithInfo(roomId, sessionId) + } + + fun wasSharedWith(userId: String, deviceId: String): Int? { + return cryptoStore.wasSessionSharedWithUser(roomId, sessionId, userId, deviceId).chainIndex + } + + fun markedSessionAsShared(userId: String, deviceId: String, chainIndex: Int) { + cryptoStore.markedSessionAsShared(roomId, sessionId, userId, deviceId, chainIndex) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt index ddb98fd1ec..3540f57608 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt @@ -31,6 +31,7 @@ import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest import im.vector.matrix.android.internal.crypto.OutgoingSecretRequest import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo +import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper2 import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldContent @@ -421,6 +422,10 @@ internal interface IMXCryptoStore { fun addWithHeldMegolmSession(withHeldContent: RoomKeyWithHeldContent) fun getWithHeldMegolmSession(roomId: String, sessionId: String) : RoomKeyWithHeldContent? + fun markedSessionAsShared(roomId: String?, sessionId: String, userId: String, deviceId: String, chainIndex: Int) + fun wasSessionSharedWithUser(roomId: String?, sessionId: String, userId: String, deviceId: String) : SharedSessionResult + data class SharedSessionResult(val found: Boolean, val chainIndex: Int?) + fun getSharedWithInfo(roomId: String?, sessionId: String) : MXUsersDevicesMap // Dev tools fun getOutgoingRoomKeyRequests(): List diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt index badf82f696..ec5e64dde8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt @@ -39,10 +39,10 @@ import im.vector.matrix.android.internal.crypto.OutgoingSecretRequest import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo +import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper2 import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldContent -import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody import im.vector.matrix.android.internal.crypto.model.toEntity @@ -69,11 +69,13 @@ import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntity import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields +import im.vector.matrix.android.internal.crypto.store.db.model.SharedSessionEntity import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntity import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.WithHeldSessionEntity import im.vector.matrix.android.internal.crypto.store.db.model.createPrimaryKey +import im.vector.matrix.android.internal.crypto.store.db.query.create import im.vector.matrix.android.internal.crypto.store.db.query.delete import im.vector.matrix.android.internal.crypto.store.db.query.get import im.vector.matrix.android.internal.crypto.store.db.query.getById @@ -1459,4 +1461,40 @@ internal class RealmCryptoStore @Inject constructor( } } } + + override fun markedSessionAsShared(roomId: String?, sessionId: String, userId: String, deviceId: String, chainIndex: Int) { + doRealmTransaction(realmConfiguration) { realm -> + SharedSessionEntity.create( + realm = realm, + roomId = roomId, + sessionId = sessionId, + userId = userId, + deviceId = deviceId, + chainIndex = chainIndex + ) + } + } + + override fun wasSessionSharedWithUser(roomId: String?, sessionId: String, userId: String, deviceId: String): IMXCryptoStore.SharedSessionResult { + return doWithRealm(realmConfiguration) { realm -> + SharedSessionEntity.get(realm, roomId, sessionId, userId, deviceId)?.let { + IMXCryptoStore.SharedSessionResult(true, it.chainIndex) + } ?: IMXCryptoStore.SharedSessionResult(false, null) + } + } + + override fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap { + return doWithRealm(realmConfiguration) { realm -> + val result = MXUsersDevicesMap() + SharedSessionEntity.get(realm, roomId, sessionId) + .groupBy { it.userId } + .forEach { (userId, shared) -> + shared.forEach { + result.setObject(userId, it.deviceId, it.chainIndex) + } + } + + result + } + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt index dbcdf1e0fa..34c320fe72 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt @@ -36,6 +36,7 @@ import im.vector.matrix.android.internal.crypto.store.db.model.MyDeviceLastSeenI import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields +import im.vector.matrix.android.internal.crypto.store.db.model.SharedSessionEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.WithHeldSessionEntityFields @@ -431,5 +432,18 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi .addIndex(WithHeldSessionEntityFields.SENDER_KEY) .addField(WithHeldSessionEntityFields.CODE_STRING, String::class.java) .addField(WithHeldSessionEntityFields.REASON, String::class.java) + + + realm.schema.create("SharedSessionEntity") + .addField(SharedSessionEntityFields.ROOM_ID, String::class.java) + .addField(SharedSessionEntityFields.ALGORITHM, String::class.java) + .addField(SharedSessionEntityFields.SESSION_ID, String::class.java) + .addIndex(SharedSessionEntityFields.SESSION_ID) + .addField(SharedSessionEntityFields.USER_ID, String::class.java) + .addIndex(SharedSessionEntityFields.USER_ID) + .addField(SharedSessionEntityFields.DEVICE_ID, String::class.java) + .addIndex(SharedSessionEntityFields.DEVICE_ID) + .addField(SharedSessionEntityFields.CHAIN_INDEX, Long::class.java) + .setNullable(SharedSessionEntityFields.CHAIN_INDEX, true) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreModule.kt index aa03636871..a096c96e02 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreModule.kt @@ -28,6 +28,7 @@ import im.vector.matrix.android.internal.crypto.store.db.model.MyDeviceLastSeenI import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntity +import im.vector.matrix.android.internal.crypto.store.db.model.SharedSessionEntity import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntity import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity import im.vector.matrix.android.internal.crypto.store.db.model.WithHeldSessionEntity @@ -52,6 +53,7 @@ import io.realm.annotations.RealmModule IncomingGossipingRequestEntity::class, OutgoingGossipingRequestEntity::class, MyDeviceLastSeenInfoEntity::class, - WithHeldSessionEntity::class + WithHeldSessionEntity::class, + SharedSessionEntity::class ]) internal class RealmCryptoStoreModule From c7c35399e5dd07042f55c936d24b4ec50ac8607e Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 22 May 2020 15:10:51 +0200 Subject: [PATCH 06/15] post rebase --- .../im/vector/matrix/android/internal/session/SessionModule.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index fc5c43670d..ff668bbc06 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -52,6 +52,7 @@ import im.vector.matrix.android.internal.di.Unauthenticated import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.UserMd5 import im.vector.matrix.android.internal.eventbus.EventBusTimberLogger +import im.vector.matrix.android.internal.network.AccessTokenInterceptor import im.vector.matrix.android.internal.network.DefaultNetworkConnectivityChecker import im.vector.matrix.android.internal.network.FallbackNetworkCallbackStrategy import im.vector.matrix.android.internal.network.NetworkCallbackStrategy @@ -59,6 +60,7 @@ import im.vector.matrix.android.internal.network.NetworkConnectivityChecker import im.vector.matrix.android.internal.network.PreferredNetworkCallbackStrategy import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.network.httpclient.addAccessTokenInterceptor +import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor import im.vector.matrix.android.internal.network.token.AccessTokenProvider import im.vector.matrix.android.internal.network.token.HomeserverAccessTokenProvider import im.vector.matrix.android.internal.session.call.CallEventObserver From cdb1b8d8f86b938daba538e364c57fabfaaa3999 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 3 Jun 2020 10:38:30 +0200 Subject: [PATCH 07/15] post merge fix --- .../vector/matrix/android/internal/session/SessionModule.kt | 5 ++--- .../matrix/android/internal/session/sync/RoomSyncHandler.kt | 2 +- .../room/detail/timeline/factory/MergedHeaderItemFactory.kt | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index ff668bbc06..c54e6e06e7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -59,7 +59,6 @@ import im.vector.matrix.android.internal.network.NetworkCallbackStrategy import im.vector.matrix.android.internal.network.NetworkConnectivityChecker import im.vector.matrix.android.internal.network.PreferredNetworkCallbackStrategy import im.vector.matrix.android.internal.network.RetrofitFactory -import im.vector.matrix.android.internal.network.httpclient.addAccessTokenInterceptor import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor import im.vector.matrix.android.internal.network.token.AccessTokenProvider import im.vector.matrix.android.internal.network.token.HomeserverAccessTokenProvider @@ -185,7 +184,7 @@ internal abstract class SessionModule { @SessionScope @Authenticated fun providesOkHttpClient(@Unauthenticated okHttpClient: OkHttpClient, - accessTokenInterceptor: AccessTokenInterceptor, + @Authenticated accessTokenProvider: AccessTokenProvider, @SessionId sessionId: String, @MockHttpInterceptor testInterceptor: TestInterceptor?): OkHttpClient { return okHttpClient.newBuilder() @@ -194,7 +193,7 @@ internal abstract class SessionModule { val existingCurlInterceptors = interceptors().filterIsInstance() interceptors().removeAll(existingCurlInterceptors) - addInterceptor(accessTokenInterceptor) + addInterceptor(AccessTokenInterceptor(accessTokenProvider)) if (testInterceptor != null) { testInterceptor.sessionId = sessionId addInterceptor(testInterceptor) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt index 3be357920c..b1d8d7b0b5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt @@ -265,7 +265,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle isLimited: Boolean = true, syncLocalTimestampMillis: Long, isInitialSync: Boolean): ChunkEntity { - val lastChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomEntity.roomId) + val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId) val chunkEntity = if (!isLimited && lastChunk != null) { lastChunk } else { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index e82531cef7..c1b1188ba1 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -177,8 +177,8 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) { highlighted = true } - val senderAvatar = mergedEvent.senderAvatar - val senderName = mergedEvent.getDisambiguatedDisplayName() + val senderAvatar = mergedEvent.senderInfo.avatarUrl + val senderName = mergedEvent.senderInfo.disambiguatedDisplayName val data = BasedMergedItem.Data( userId = mergedEvent.root.senderId ?: "", avatarUrl = senderAvatar, From 3fa2647e925ee27dc4dbb420e5fb31e629b017ff Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 3 Jun 2020 17:58:26 +0200 Subject: [PATCH 08/15] Send with held code on key requests --- .../crypto/gossiping/WithHeldTests.kt | 58 ++++++++++++++++--- .../api/session/crypto/CryptoService.kt | 2 + .../internal/crypto/DefaultCryptoService.kt | 3 + .../algorithms/megolm/MXMegolmEncryption.kt | 13 +++-- 4 files changed, 65 insertions(+), 11 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/WithHeldTests.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/WithHeldTests.kt index 581624ba16..51ae09827a 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/WithHeldTests.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/WithHeldTests.kt @@ -19,6 +19,8 @@ package im.vector.matrix.android.internal.crypto.gossiping import android.util.Log import androidx.test.ext.junit.runners.AndroidJUnit4 import im.vector.matrix.android.InstrumentedTest +import im.vector.matrix.android.api.NoOpMatrixCallback +import im.vector.matrix.android.api.extensions.tryThis 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.toModel @@ -128,9 +130,8 @@ class WithHeldTests : InstrumentedTest { mTestHelper.signOutAndClose(bobUnverifiedSession) } - @Test - fun test_WithHeldNoOlm() { + fun test_WithHeldNoOlm() { val testData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = testData.firstSession val bobSession = testData.secondSession!! @@ -148,8 +149,6 @@ class WithHeldTests : InstrumentedTest { val roomAlicePov = aliceSession.getRoom(testData.roomId)!! - // can we force one-time key shortage?? - val eventId = mTestHelper.sendTextMessage(roomAlicePov, "first message", 1).first().eventId // await for bob session to get the message @@ -176,7 +175,7 @@ class WithHeldTests : InstrumentedTest { val sessionId = eventBobPOV!!.root.content.toModel()!!.sessionId!! val chainIndex = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject(bobSession.myUserId, bobSession.sessionParams.credentials.deviceId) - Assert.assertEquals("Alice should have marked bob's device for this session", 0, chainIndex) + Assert.assertEquals("Alice should have marked bob's device for this session", 0, chainIndex) // Add a new device for bob aliceInterceptor.clearRules() @@ -194,10 +193,55 @@ class WithHeldTests : InstrumentedTest { val chainIndex2 = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject(bobSecondSession.myUserId, bobSecondSession.sessionParams.credentials.deviceId) - Assert.assertEquals("Alice should have marked bob's device for this session", 1, chainIndex2) + Assert.assertEquals("Alice should have marked bob's device for this session", 1, chainIndex2) - mTestHelper.signOutAndClose(bobSecondSession) + aliceInterceptor.clearRules() testData.cleanUp(mTestHelper) + mTestHelper.signOutAndClose(bobSecondSession) + } + @Test + fun test_WithHeldKeyRequest() { + val testData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + val aliceSession = testData.firstSession + val bobSession = testData.secondSession!! + + val roomAlicePov = aliceSession.getRoom(testData.roomId)!! + + val eventId = mTestHelper.sendTextMessage(roomAlicePov, "first message", 1).first().eventId + + mTestHelper.signOutAndClose(bobSession) + + // Create a new session for bob + + val bobSecondSession = mTestHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true)) + // initialize to force request keys if missing + mCryptoTestHelper.initializeCrossSigning(bobSecondSession) + + // Trust bob second device from Alice POV + aliceSession.cryptoService().crossSigningService().trustDevice(bobSecondSession.sessionParams.deviceId!!, NoOpMatrixCallback()) + bobSecondSession.cryptoService().crossSigningService().trustDevice(aliceSession.sessionParams.deviceId!!, NoOpMatrixCallback()) + + var sessionId: String? = null + // Check that the + // await for bob SecondSession session to get the message + mTestHelper.waitWithLatch { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimeLineEvent(eventId)?.also { + // try to decrypt and force key request + tryThis { bobSecondSession.cryptoService().decryptEvent(it.root, "") } + } + sessionId = timeLineEvent?.root?.content?.toModel()?.sessionId + timeLineEvent != null + } + } + + //Check that bob second session requested the key + mTestHelper.waitWithLatch { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + val wc = bobSecondSession.cryptoService().getWithHeldMegolmSession(roomAlicePov.roomId, sessionId!!) + wc?.code == WithHeldCode.UNAUTHORISED + } + } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt index 5e6eb6d4ff..edddf90412 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt @@ -36,6 +36,7 @@ import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap +import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldContent import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody @@ -148,4 +149,5 @@ interface CryptoService { //For testing shared session fun getSharedWithInfo(roomId: String?, sessionId: String) : MXUsersDevicesMap + fun getWithHeldMegolmSession(roomId: String, sessionId: String) : RoomKeyWithHeldContent? } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt index c0033447bd..29517ea6b3 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt @@ -1334,6 +1334,9 @@ internal class DefaultCryptoService @Inject constructor( return cryptoStore.getSharedWithInfo(roomId, sessionId) } + override fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent? { + return cryptoStore.getWithHeldMegolmSession(roomId, sessionId) + } /* ========================================================================================== * For test only * ========================================================================================== */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt index 83a6bce70e..94c4d9ae3e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -94,7 +94,7 @@ internal class MXMegolmEncryption( { it.second }, { it.first } ).forEach { (code, targets) -> - notifyKeyWithHeld(targets, outboundSession.sessionId, code) + notifyKeyWithHeld(targets, outboundSession.sessionId, olmDevice.deviceCurve25519Key, code) } } @@ -229,6 +229,7 @@ internal class MXMegolmEncryption( notifyKeyWithHeld( listOf(UserDevice(userId, deviceID)), session.sessionId, + olmDevice.deviceCurve25519Key, WithHeldCode.NO_OLM ) @@ -267,10 +268,10 @@ internal class MXMegolmEncryption( } } - private fun notifyKeyWithHeld(targets: List, sessionId: String, code: WithHeldCode) { + private fun notifyKeyWithHeld(targets: List, sessionId: String, senderKey: String?, code: WithHeldCode) { val withHeldContent = RoomKeyWithHeldContent( roomId = roomId, - senderKey = olmDevice.deviceCurve25519Key, + senderKey = senderKey, algorithm = MXCRYPTO_ALGORITHM_MEGOLM, sessionId = sessionId, codeString = code.value @@ -383,7 +384,11 @@ internal class MXMegolmEncryption( // Get the chain index of the key we previously sent this device val chainIndex = outboundSession?.sharedWithHelper?.wasSharedWith(userId,deviceId) ?: return false - .also { Timber.w("[MXMegolmEncryption] reshareKey : ERROR : Never share megolm with this device") } + .also { + // Send a room key with held + notifyKeyWithHeld(listOf(UserDevice(userId, deviceId)), sessionId, senderKey, WithHeldCode.UNAUTHORISED) + Timber.w("[MXMegolmEncryption] reshareKey : ERROR : Never share megolm with this device") + } val devicesByUser = mapOf(userId to listOf(deviceInfo)) val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser) From 63499c2f48a8476ac0e4b912b9cb4cd1fdc973e6 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 3 Jun 2020 17:58:49 +0200 Subject: [PATCH 09/15] Add test orchestrator dependency --- matrix-sdk-android/build.gradle | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index ca92df08c5..422a5dac1d 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -31,6 +31,11 @@ android { multiDexEnabled true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + // The following argument makes the Android Test Orchestrator run its + // "pm clear" command after each test invocation. This command ensures + // that the app's state is completely cleared between tests. + testInstrumentationRunnerArguments clearPackageData: 'true' + buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" resValue "string", "git_sdk_revision", "\"${gitRevision()}\"" resValue "string", "git_sdk_revision_unix_date", "\"${gitRevisionUnixDate()}\"" @@ -41,6 +46,10 @@ android { } } + testOptions { + execution 'ANDROIDX_TEST_ORCHESTRATOR' + } + buildTypes { debug { @@ -194,4 +203,7 @@ dependencies { androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" // Plant Timber tree for test androidTestImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1' + + androidTestUtil 'androidx.test:orchestrator:1.2.0' + } From a9191b8fad890fbf9fa727593d76a43e7fa9ccf8 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 3 Jun 2020 18:55:07 +0200 Subject: [PATCH 10/15] klint --- .../im/vector/matrix/android/api/Matrix.kt | 4 --- .../matrix/android/common/CommonTestHelper.kt | 3 +- .../matrix/android/common/CryptoTestHelper.kt | 10 ------ .../internal/crypto/CryptoStoreTest.kt | 34 +++++++++---------- .../crypto/gossiping/KeyShareTests.kt | 4 --- .../crypto/gossiping/WithHeldTests.kt | 16 ++++----- .../api/session/crypto/CryptoService.kt | 2 +- .../api/session/crypto/MXCryptoError.kt | 1 - .../internal/crypto/DefaultCryptoService.kt | 2 +- .../crypto/algorithms/IMXWithHeldExtension.kt | 1 - .../algorithms/megolm/MXMegolmDecryption.kt | 4 +-- .../algorithms/megolm/MXMegolmEncryption.kt | 5 ++- .../model/event/RoomKeyWithHeldContent.kt | 3 -- .../store/db/RealmCryptoStoreMigration.kt | 1 - .../store/db/query/SharedSessionQueries.kt | 1 - .../store/db/query/WithHeldSessionQueries.kt | 1 - .../android/internal/di/MatrixComponent.kt | 1 - .../session/room/timeline/DefaultTimeline.kt | 3 +- .../action/MessageActionsEpoxyController.kt | 3 -- .../action/MessageActionsViewModel.kt | 1 - .../timeline/factory/EncryptedItemFactory.kt | 2 -- .../factory/MergedHeaderItemFactory.kt | 2 -- .../VectorSettingsSecurityPrivacyFragment.kt | 1 - 23 files changed, 32 insertions(+), 73 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/api/Matrix.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/api/Matrix.kt index 737ca77245..d61bb15adb 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/api/Matrix.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/api/Matrix.kt @@ -22,16 +22,13 @@ import androidx.work.Configuration import androidx.work.WorkManager import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.BuildConfig -import im.vector.matrix.android.api.MatrixConfiguration import im.vector.matrix.android.api.auth.AuthenticationService import im.vector.matrix.android.common.DaggerTestMatrixComponent import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments -import im.vector.matrix.android.internal.di.MockHttpInterceptor import im.vector.matrix.android.internal.network.UserAgentHolder import im.vector.matrix.android.internal.util.BackgroundDetectionObserver -import okhttp3.Interceptor import org.matrix.olm.OlmManager import java.io.InputStream import java.util.concurrent.Executors @@ -50,7 +47,6 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo @Inject internal lateinit var olmManager: OlmManager @Inject internal lateinit var sessionManager: SessionManager - init { Monarchy.init(context) DaggerTestMatrixComponent.factory().create(context, matrixConfiguration).inject(this) diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt index f2edc84324..ca653fc9e7 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt @@ -36,7 +36,6 @@ import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.api.session.sync.SyncState -import im.vector.matrix.android.internal.session.TestInterceptor import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay @@ -152,7 +151,7 @@ class CommonTestHelper(context: Context) { timeline.dispose() // Check that all events has been created - assertEquals("Message number do not match ${sentEvents}", nbOfMessages.toLong(), sentEvents.size.toLong()) + assertEquals("Message number do not match $sentEvents", nbOfMessages.toLong(), sentEvents.size.toLong()) return sentEvents } diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt index 10a40944ae..5425f97fc4 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt @@ -22,7 +22,6 @@ import androidx.lifecycle.Observer import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction import im.vector.matrix.android.api.session.crypto.verification.OutgoingSasVerificationTransaction -import im.vector.matrix.android.api.session.crypto.verification.SasVerificationTransaction import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState import im.vector.matrix.android.api.session.events.model.Event @@ -41,7 +40,6 @@ import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupAuthData import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth -import im.vector.matrix.android.internal.crypto.verification.SASDefaultVerificationTransaction import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -295,7 +293,6 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { ) } - mTestHelper.waitWithLatch { latch -> val bobRoomSummariesLive = runBlocking(Dispatchers.Main) { bob.getRoomSummariesLive(roomSummaryQueryParams { }) @@ -314,10 +311,8 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { GlobalScope.launch(Dispatchers.Main) { bobRoomSummariesLive.observeForever(newRoomObserver) } - } - mTestHelper.waitWithLatch { latch -> val bobRoomSummariesLive = runBlocking(Dispatchers.Main) { bob.getRoomSummariesLive(roomSummaryQueryParams { }) @@ -339,10 +334,8 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { } mTestHelper.doSync { bob.joinRoom(roomId, callback = it) } - } - return roomId } @@ -357,7 +350,6 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { } fun verifySASCrossSign(alice: Session, bob: Session, roomId: String) { - assertTrue(alice.cryptoService().crossSigningService().canCrossSign()) assertTrue(bob.cryptoService().crossSigningService().canCrossSign()) @@ -373,7 +365,6 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { bob.sessionParams.credentials.deviceId!!, null) - // we should reach SHOW SAS on both var alicePovTx: OutgoingSasVerificationTransaction? = null var bobPovTx: IncomingSasVerificationTransaction? = null @@ -427,6 +418,5 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId) } } - } } diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/CryptoStoreTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/CryptoStoreTest.kt index 45589a49a4..5df05766ff 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/CryptoStoreTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/CryptoStoreTest.kt @@ -22,10 +22,8 @@ import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import io.realm.Realm import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse import org.junit.Assert.assertNotEquals import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -45,22 +43,22 @@ class CryptoStoreTest : InstrumentedTest { Realm.init(context()) } - @Test - fun test_metadata_realm_ok() { - val cryptoStore: IMXCryptoStore = cryptoStoreHelper.createStore() - - assertFalse(cryptoStore.hasData()) - - cryptoStore.open() - - assertEquals("deviceId_sample", cryptoStore.getDeviceId()) - - assertTrue(cryptoStore.hasData()) - - // Cleanup - cryptoStore.close() - cryptoStore.deleteStore() - } +// @Test +// fun test_metadata_realm_ok() { +// val cryptoStore: IMXCryptoStore = cryptoStoreHelper.createStore() +// +// assertFalse(cryptoStore.hasData()) +// +// cryptoStore.open() +// +// assertEquals("deviceId_sample", cryptoStore.getDeviceId()) +// +// assertTrue(cryptoStore.hasData()) +// +// // Cleanup +// cryptoStore.close() +// cryptoStore.deleteStore() +// } @Test fun test_lastSessionUsed() { diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt index ec8a97abe5..e78ef04050 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt @@ -30,7 +30,6 @@ import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.common.CommonTestHelper import im.vector.matrix.android.common.CryptoTestHelper -import im.vector.matrix.android.common.MockOkHttpInterceptor import im.vector.matrix.android.common.SessionTestParams import im.vector.matrix.android.common.TestConstants import im.vector.matrix.android.internal.crypto.GossipingRequestState @@ -46,8 +45,6 @@ import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.assertTrue import junit.framework.TestCase.fail -import kotlinx.coroutines.delay -import okhttp3.internal.waitMillis import org.junit.Assert import org.junit.FixMethodOrder import org.junit.Test @@ -293,7 +290,6 @@ class KeyShareTests : InstrumentedTest { } } - mTestHelper.signOutAndClose(aliceSession1) mTestHelper.signOutAndClose(aliceSession2) } diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/WithHeldTests.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/WithHeldTests.kt index 51ae09827a..16bd97a975 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/WithHeldTests.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/WithHeldTests.kt @@ -46,10 +46,9 @@ class WithHeldTests : InstrumentedTest { @Test fun test_WithHeldUnverifiedReason() { - - //============================= + // ============================= // ARRANGE - //============================= + // ============================= val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) val bobSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) @@ -65,9 +64,9 @@ class WithHeldTests : InstrumentedTest { val bobUnverifiedSession = mTestHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true)) - //============================= + // ============================= // ACT - //============================= + // ============================= // Alice decide to not send to unverified sessions aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(true) @@ -83,9 +82,9 @@ class WithHeldTests : InstrumentedTest { val eventBobPOV = bobUnverifiedSession.getRoom(roomId)?.getTimeLineEvent(timelineEvent.eventId)!! - //============================= + // ============================= // ASSERT - //============================= + // ============================= // Bob should not be able to decrypt because the keys is withheld try { @@ -124,7 +123,6 @@ class WithHeldTests : InstrumentedTest { Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage) } - mTestHelper.signOutAndClose(aliceSession) mTestHelper.signOutAndClose(bobSession) mTestHelper.signOutAndClose(bobUnverifiedSession) @@ -236,7 +234,7 @@ class WithHeldTests : InstrumentedTest { } } - //Check that bob second session requested the key + // Check that bob second session requested the key mTestHelper.waitWithLatch { latch -> mTestHelper.retryPeriodicallyWithLatch(latch) { val wc = bobSecondSession.cryptoService().getWithHeldMegolmSession(roomAlicePov.roomId, sessionId!!) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt index edddf90412..19635364cd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt @@ -147,7 +147,7 @@ interface CryptoService { fun getGossipingEventsTrail(): List - //For testing shared session + // For testing shared session fun getSharedWithInfo(roomId: String?, sessionId: String) : MXUsersDevicesMap fun getWithHeldMegolmSession(roomId: String, sessionId: String) : RoomKeyWithHeldContent? } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/MXCryptoError.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/MXCryptoError.kt index 9610390bd1..b5d63faaf9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/MXCryptoError.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/MXCryptoError.kt @@ -20,7 +20,6 @@ package im.vector.matrix.android.api.session.crypto import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap -import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode import org.matrix.olm.OlmException /** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt index 29517ea6b3..02197fb5f3 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt @@ -843,7 +843,7 @@ internal class DefaultCryptoService @Inject constructor( val withHeldContent = event.getClearContent().toModel() ?: return Unit.also { Timber.e("## CRYPTO | Malformed onKeyWithHeldReceived() : missing fields") } - Timber.d("## CRYPTO | onKeyWithHeldReceived() received : content <${withHeldContent}>") + Timber.d("## CRYPTO | onKeyWithHeldReceived() received : content <$withHeldContent>") val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(withHeldContent.roomId, withHeldContent.algorithm) if (alg is IMXWithHeldExtension) { alg.onRoomKeyWithHeldEvent(withHeldContent) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXWithHeldExtension.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXWithHeldExtension.kt index 8fa05ca648..e582bbcb04 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXWithHeldExtension.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXWithHeldExtension.kt @@ -21,4 +21,3 @@ import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldConte internal interface IMXWithHeldExtension { fun onRoomKeyWithHeldEvent(withHeldInfo: RoomKeyWithHeldContent) } - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt index 36cc4a6030..89ba87646a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt @@ -115,7 +115,7 @@ internal class MXMegolmDecryption(private val userId: String, if (throwable is MXCryptoError.OlmError) { // TODO Check the value of .message if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") { - //addEventToPendingList(event, timeline) + // addEventToPendingList(event, timeline) // The session might has been partially withheld (and only pass ratcheted) val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId) if (withHeldInfo != null) { @@ -157,7 +157,7 @@ internal class MXMegolmDecryption(private val userId: String, withHeldInfo.reason) } else { // This is un-used in riotX SDK, not sure if needed - //addEventToPendingList(event, timeline) + // addEventToPendingList(event, timeline) if (requestKeysOnFail) { requestKeysForEvent(event, false) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt index 94c4d9ae3e..b131b77ce9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -86,7 +86,7 @@ internal class MXMegolmEncryption( } private fun notifyWithheldForSession(devices: MXUsersDevicesMap, outboundSession: MXOutboundSessionInfo) { - ArrayList>().apply { + mutableListOf>().apply { devices.forEach { userId, deviceId, withheldCode -> this.add(UserDevice(userId, deviceId) to withheldCode) } @@ -339,7 +339,6 @@ internal class MXMegolmEncryption( val devicesInRoom = DeviceInRoomInfo() val unknownDevices = MXUsersDevicesMap() - for (userId in keys.userIds) { val deviceIds = keys.getUserDeviceIds(userId) ?: continue for (deviceId in deviceIds) { @@ -383,7 +382,7 @@ internal class MXMegolmEncryption( .also { Timber.w("Device not found") } // Get the chain index of the key we previously sent this device - val chainIndex = outboundSession?.sharedWithHelper?.wasSharedWith(userId,deviceId) ?: return false + val chainIndex = outboundSession?.sharedWithHelper?.wasSharedWith(userId, deviceId) ?: return false .also { // Send a room key with held notifyKeyWithHeld(listOf(UserDevice(userId, deviceId)), sessionId, senderKey, WithHeldCode.UNAUTHORISED) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/RoomKeyWithHeldContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/RoomKeyWithHeldContent.kt index 56fc451671..9c2b4ceb05 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/RoomKeyWithHeldContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/RoomKeyWithHeldContent.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.crypto.model.event import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import im.vector.matrix.android.api.extensions.tryThis /** * Class representing an sharekey content @@ -99,5 +98,3 @@ enum class WithHeldCode(val value: String) { } } } - - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt index 34c320fe72..eb2fc9ebad 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt @@ -433,7 +433,6 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi .addField(WithHeldSessionEntityFields.CODE_STRING, String::class.java) .addField(WithHeldSessionEntityFields.REASON, String::class.java) - realm.schema.create("SharedSessionEntity") .addField(SharedSessionEntityFields.ROOM_ID, String::class.java) .addField(SharedSessionEntityFields.ALGORITHM, String::class.java) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/SharedSessionQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/SharedSessionQueries.kt index 48b84174ac..53a9d96a67 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/SharedSessionQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/SharedSessionQueries.kt @@ -55,4 +55,3 @@ internal fun SharedSessionEntity.Companion.create(realm: Realm, roomId: String?, this.chainIndex = chainIndex } } - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/WithHeldSessionQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/WithHeldSessionQueries.kt index be88200016..3effc5304e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/WithHeldSessionQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/WithHeldSessionQueries.kt @@ -39,4 +39,3 @@ internal fun WithHeldSessionEntity.Companion.getOrCreate(realm: Realm, roomId: S this.sessionId = sessionId } } - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt index 83388f4a57..26d1329fbc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt @@ -31,7 +31,6 @@ import im.vector.matrix.android.internal.session.TestInterceptor import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.BackgroundDetectionObserver import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers -import okhttp3.Interceptor import okhttp3.OkHttpClient import org.matrix.olm.OlmManager import java.io.File diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index 8c0a75e4a9..2b1f50d000 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -777,7 +777,8 @@ internal class DefaultTimeline( `in`(TimelineEventEntityFields.ROOT.TYPE, settings.allowedTypes.toTypedArray()) } if (settings.filterUseless) { - not().equalTo(TimelineEventEntityFields.ROOT.IS_USELESS, true) + not() + .equalTo(TimelineEventEntityFields.ROOT.IS_USELESS, true) } if (settings.filterEdits) { not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt index 3eeabd801b..e77d9ec73f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt @@ -18,9 +18,6 @@ package im.vector.riotx.features.home.room.detail.timeline.action import android.view.View import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.mvrx.Success -import im.vector.matrix.android.api.session.crypto.MXCryptoError -import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode import im.vector.riotx.EmojiCompatFontProvider import im.vector.riotx.R import im.vector.riotx.core.epoxy.bottomsheet.BottomSheetQuickReactionsItem diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 8aed581804..aac7847302 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -15,7 +15,6 @@ */ package im.vector.riotx.features.home.room.detail.timeline.action -import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Async import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt index 6a135d528d..66eef30037 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt @@ -57,7 +57,6 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat val cryptoError = event.root.mCryptoError val spannableStr = if (vectorPreferences.hideE2ETechnicalErrors()) { - val colorFromAttribute = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) if (cryptoError == null) { span(stringProvider.getString(R.string.encrypted_message)) { @@ -111,7 +110,6 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat } } - val informationData = messageInformationDataFactory.create(event, nextEvent) val attributes = attributesFactory.create(event.root.content.toModel(), informationData, callback) return MessageTextItem_() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index c1b1188ba1..1b794709a3 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -148,14 +148,12 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde return nextEvent == null || nextEvent.root.getClearType() != EventType.ENCRYPTED } - private fun buildUTDMergedSummary(currentPosition: Int, items: List, event: TimelineEvent, eventIdToHighlight: String?, requestModelBuild: () -> Unit, callback: TimelineEventController.Callback?): MergedUTDItem_? { - var prevEvent = if (currentPosition > 0) items[currentPosition - 1] else null var tmpPos = currentPosition - 1 val mergedEvents = ArrayList().also { it.add(event) } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt index e4745a1a31..2b9338ccc8 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt @@ -385,7 +385,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( // } // } - sendToUnverifiedDevicesPref.isChecked = session.cryptoService().getGlobalBlacklistUnverifiedDevices() sendToUnverifiedDevicesPref.onPreferenceClickListener = Preference.OnPreferenceClickListener { From fe235e079187c8176366fb3e13dc9ae84ca216f7 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 4 Jun 2020 09:40:04 +0200 Subject: [PATCH 11/15] ktlint --- .../internal/di/InterceptorQualifiers.kt | 23 ------------------- .../android/internal/di/MatrixComponent.kt | 1 + .../android/internal/di/NoOpTestModule.kt | 1 + .../android/internal/session/SessionModule.kt | 6 ++++- 4 files changed, 7 insertions(+), 24 deletions(-) delete mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/InterceptorQualifiers.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/InterceptorQualifiers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/InterceptorQualifiers.kt deleted file mode 100644 index 3a1b7d569b..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/InterceptorQualifiers.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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.di - -import javax.inject.Qualifier - -@Qualifier -@Retention(AnnotationRetention.RUNTIME) -annotation class MockHttpInterceptor diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt index 26d1329fbc..af8a16b451 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt @@ -27,6 +27,7 @@ import im.vector.matrix.android.api.auth.AuthenticationService import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.auth.AuthModule import im.vector.matrix.android.internal.auth.SessionParamsStore +import im.vector.matrix.android.internal.session.MockHttpInterceptor import im.vector.matrix.android.internal.session.TestInterceptor import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.BackgroundDetectionObserver diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NoOpTestModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NoOpTestModule.kt index e2e29b1fa5..3041070056 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NoOpTestModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NoOpTestModule.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.di import dagger.Module import dagger.Provides +import im.vector.matrix.android.internal.session.MockHttpInterceptor import im.vector.matrix.android.internal.session.TestInterceptor @Module diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index c54e6e06e7..2e9f6174f2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -43,7 +43,6 @@ import im.vector.matrix.android.internal.crypto.verification.VerificationMessage import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory import im.vector.matrix.android.internal.di.Authenticated import im.vector.matrix.android.internal.di.DeviceId -import im.vector.matrix.android.internal.di.MockHttpInterceptor import im.vector.matrix.android.internal.di.SessionCacheDirectory import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.di.SessionFilesDirectory @@ -82,6 +81,11 @@ import org.greenrobot.eventbus.EventBus import retrofit2.Retrofit import java.io.File import javax.inject.Provider +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class MockHttpInterceptor @Module internal abstract class SessionModule { From 5bd448405bdad60b9535762607cb96b624ea7372 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 4 Jun 2020 10:16:20 +0200 Subject: [PATCH 12/15] Fix test compilation --- .../java/im/vector/matrix/android/common/TestNetworkModule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestNetworkModule.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestNetworkModule.kt index 3b3ec47768..f6190441dd 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestNetworkModule.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestNetworkModule.kt @@ -18,7 +18,7 @@ package im.vector.matrix.android.common import dagger.Module import dagger.Provides -import im.vector.matrix.android.internal.di.MockHttpInterceptor +import im.vector.matrix.android.internal.session.MockHttpInterceptor import im.vector.matrix.android.internal.session.TestInterceptor @Module From 668967546c8c236f1181dd9d0bcb045930d7a82e Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 4 Jun 2020 11:23:24 +0200 Subject: [PATCH 13/15] Fix / if listener is not removed messages could be duplicated --- .../java/im/vector/matrix/android/common/CommonTestHelper.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt index ca653fc9e7..bee2815c0a 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt @@ -117,6 +117,7 @@ class CommonTestHelper(context: Context) { * @param nbOfMessages the number of time the message will be sent */ fun sendTextMessage(room: Room, message: String, nbOfMessages: Int): List { + val timeline = room.createTimeline(null, TimelineSettings(10)) val sentEvents = ArrayList(nbOfMessages) val latch = CountDownLatch(1) val timelineListener = object : Timeline.Listener { @@ -135,11 +136,12 @@ class CommonTestHelper(context: Context) { if (newMessages.size == nbOfMessages) { sentEvents.addAll(newMessages) + // Remove listener now, if not at the next update sendEvents could change + timeline.removeListener(this) latch.countDown() } } } - val timeline = room.createTimeline(null, TimelineSettings(10)) timeline.start() timeline.addListener(timelineListener) for (i in 0 until nbOfMessages) { @@ -147,7 +149,6 @@ class CommonTestHelper(context: Context) { } // Wait 3 second more per message await(latch, timeout = TestConstants.timeOutMillis + 3_000L * nbOfMessages) - timeline.removeListener(timelineListener) timeline.dispose() // Check that all events has been created From a1ce245e3a217e9ac15124df6d1c98ca313f6ca0 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 26 Jun 2020 14:07:22 +0200 Subject: [PATCH 14/15] Disable utd merge by default, issue with back paginate --- .../action/MessageActionsViewModel.kt | 1 - .../timeline/factory/EncryptedItemFactory.kt | 32 +++++----- .../factory/MergedHeaderItemFactory.kt | 60 +++++++++---------- .../detail/timeline/item/MergedUTDItem.kt | 15 +++++ .../features/settings/VectorPreferences.kt | 7 ++- .../item_timeline_event_merged_utd_stub.xml | 1 + vector/src/main/res/values/strings.xml | 2 +- .../src/main/res/xml/vector_settings_labs.xml | 6 +- 8 files changed, 68 insertions(+), 56 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index aac7847302..6c192105d7 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -15,7 +15,6 @@ */ package im.vector.riotx.features.home.room.detail.timeline.action -import com.airbnb.mvrx.Async import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt index 66eef30037..997c68cb52 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt @@ -56,7 +56,22 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat EventType.ENCRYPTED == event.root.getClearType() -> { val cryptoError = event.root.mCryptoError - val spannableStr = if (vectorPreferences.hideE2ETechnicalErrors()) { + val spannableStr = if (vectorPreferences.developerMode()) { + val errorDescription = + if (cryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) { + stringProvider.getString(R.string.notice_crypto_error_unkwown_inbound_session_id) + } else { + // TODO i18n + cryptoError?.name + } + + val message = stringProvider.getString(R.string.encrypted_message).takeIf { cryptoError == null } + ?: stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, errorDescription) + span(message) { + textStyle = "italic" + textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) + } + } else { val colorFromAttribute = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) if (cryptoError == null) { span(stringProvider.getString(R.string.encrypted_message)) { @@ -93,21 +108,6 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat } } } - } else { - val errorDescription = - if (cryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) { - stringProvider.getString(R.string.notice_crypto_error_unkwown_inbound_session_id) - } else { - // TODO i18n - cryptoError?.name - } - - val message = stringProvider.getString(R.string.encrypted_message).takeIf { cryptoError == null } - ?: stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, errorDescription) - span(message) { - textStyle = "italic" - textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) - } } val informationData = messageInformationDataFactory.create(event, nextEvent) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index 1b794709a3..d3b7edbde6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -16,7 +16,6 @@ package im.vector.riotx.features.home.room.detail.timeline.factory -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.toModel import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent @@ -40,6 +39,7 @@ import im.vector.riotx.features.home.room.detail.timeline.item.MergedRoomCreatio import im.vector.riotx.features.home.room.detail.timeline.item.MergedUTDItem import im.vector.riotx.features.home.room.detail.timeline.item.MergedUTDItem_ import im.vector.riotx.features.settings.VectorPreferences +import timber.log.Timber import javax.inject.Inject class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, @@ -64,7 +64,8 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde requestModelBuild: () -> Unit) : BasedMergedItem<*>? { return if (shouldMergedAsCannotDecryptGroup(event, nextEvent)) { - buildUTDMergedSummary(currentPosition, items, event, eventIdToHighlight, requestModelBuild, callback) + Timber.v("## MERGE: Candidate for merge, top event ${event.eventId}") + buildUTDMergedSummary(currentPosition, items, event, eventIdToHighlight, /*requestModelBuild,*/ callback) } else if (nextEvent?.root?.getClearType() == EventType.STATE_ROOM_CREATE && event.isRoomConfiguration(nextEvent.root.getClearContent()?.toModel()?.creator)) { // It's the first item before room.create @@ -139,33 +140,37 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde // Event should be UTD // Next event should not - private fun shouldMergedAsCannotDecryptGroup(event: TimelineEvent, nextEvent: TimelineEvent?) : Boolean { - if (!vectorPreferences.hideE2ETechnicalErrors()) return false + private fun shouldMergedAsCannotDecryptGroup(event: TimelineEvent, nextEvent: TimelineEvent?): Boolean { + if (!vectorPreferences.mergeUTDinTimeline()) return false // if event is not UTD return false - if (event.root.getClearType() != EventType.ENCRYPTED || event.root.mCryptoError != MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) return false + if (!isEventUTD(event)) return false // At this point event cannot be decrypted // Let's check if older event is not UTD - return nextEvent == null || nextEvent.root.getClearType() != EventType.ENCRYPTED + return nextEvent == null || !isEventUTD(event) + } + + private fun isEventUTD(event: TimelineEvent): Boolean { + return event.root.getClearType() == EventType.ENCRYPTED && !event.root.isRedacted() } private fun buildUTDMergedSummary(currentPosition: Int, - items: List, - event: TimelineEvent, - eventIdToHighlight: String?, - requestModelBuild: () -> Unit, - callback: TimelineEventController.Callback?): MergedUTDItem_? { - var prevEvent = if (currentPosition > 0) items[currentPosition - 1] else null + items: List, + event: TimelineEvent, + eventIdToHighlight: String?, + // requestModelBuild: () -> Unit, + callback: TimelineEventController.Callback?): MergedUTDItem_? { + Timber.v("## MERGE: buildUTDMergedSummary from position $currentPosition") + var prevEvent = items.prevOrNull(currentPosition) var tmpPos = currentPosition - 1 val mergedEvents = ArrayList().also { it.add(event) } - while (prevEvent != null - && prevEvent.root.getClearType() == EventType.ENCRYPTED - && prevEvent.root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) { + while (prevEvent != null && isEventUTD(prevEvent)) { mergedEvents.add(prevEvent) tmpPos-- prevEvent = if (tmpPos >= 0) items[tmpPos] else null } + Timber.v("## MERGE: buildUTDMergedSummary merge group size ${mergedEvents.size}") if (mergedEvents.size < 3) return null var highlighted = false @@ -187,37 +192,28 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde mergedData.add(data) } val mergedEventIds = mergedEvents.map { it.localId } - // We try to find if one of the item id were used as mergeItemCollapseStates key - // => handle case where paginating from mergeable events and we get more - val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull() - val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey) - ?: true - val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState } - if (isCollapsed) { - collapsedEventIds.addAll(mergedEventIds) - } else { - collapsedEventIds.removeAll(mergedEventIds) - } + + collapsedEventIds.addAll(mergedEventIds) + val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() } val attributes = MergedUTDItem.Attributes( - isCollapsed = isCollapsed, + isCollapsed = true, mergeData = mergedData, avatarRenderer = avatarRenderer, - onCollapsedStateChanged = { - mergeItemCollapseStates[event.localId] = it - requestModelBuild() - } + onCollapsedStateChanged = {} ) return MergedUTDItem_() .id(mergeId) + .big(mergedEventIds.size > 5) .leftGuideline(avatarSizeProvider.leftGuideline) - .highlighted(isCollapsed && highlighted) + .highlighted(highlighted) .attributes(attributes) .also { it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents)) } } + private fun buildRoomCreationMergedSummary(currentPosition: Int, items: List, event: TimelineEvent, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedUTDItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedUTDItem.kt index 715d329168..1955ba16a3 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedUTDItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedUTDItem.kt @@ -16,7 +16,9 @@ package im.vector.riotx.features.home.room.detail.timeline.item +import android.util.TypedValue import android.view.ViewGroup +import android.widget.LinearLayout import android.widget.RelativeLayout import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams @@ -32,6 +34,9 @@ abstract class MergedUTDItem : BasedMergedItem() { @EpoxyAttribute override lateinit var attributes: Attributes + @EpoxyAttribute + var big: Boolean? = false + override fun getViewType() = STUB_ID override fun bind(holder: Holder) { @@ -39,7 +44,17 @@ abstract class MergedUTDItem : BasedMergedItem() { holder.mergedTile.updateLayoutParams { this.marginEnd = leftGuideline + if (big == true) { + this.height = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 800f, + holder.view.context.resources.displayMetrics + ).toInt() + } else { + this.height = LinearLayout.LayoutParams.WRAP_CONTENT + } } + // if (attributes.isCollapsed) { // // Take the oldest data // val data = distinctMergeData.lastOrNull() diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt index 9a427b6d9c..e4a0eb3eb6 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt @@ -144,7 +144,8 @@ class VectorPreferences @Inject constructor(private val context: Context) { private const val SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY = "SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY" private const val SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY = "SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY" private const val SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY = "SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY" - private const val SETTINGS_LABS_HIDE_TECHNICAL_E2E_ERRORS = "SETTINGS_LABS_HIDE_TECHNICAL_E2E_ERRORS" + // SETTINGS_LABS_HIDE_TECHNICAL_E2E_ERRORS + private const val SETTINGS_LABS_MERGE_E2E_ERRORS = "SETTINGS_LABS_MERGE_E2E_ERRORS" // analytics const val SETTINGS_USE_ANALYTICS_KEY = "SETTINGS_USE_ANALYTICS_KEY" @@ -266,8 +267,8 @@ class VectorPreferences @Inject constructor(private val context: Context) { return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY, true) } - fun hideE2ETechnicalErrors(): Boolean { - return defaultPrefs.getBoolean(SETTINGS_LABS_HIDE_TECHNICAL_E2E_ERRORS, true) + fun mergeUTDinTimeline(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_LABS_MERGE_E2E_ERRORS, false) } fun labAllowedExtendedLogging(): Boolean { diff --git a/vector/src/main/res/layout/item_timeline_event_merged_utd_stub.xml b/vector/src/main/res/layout/item_timeline_event_merged_utd_stub.xml index 9259aa3c10..039e0b7960 100644 --- a/vector/src/main/res/layout/item_timeline_event_merged_utd_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_merged_utd_stub.xml @@ -11,6 +11,7 @@ android:layout_height="wrap_content" android:layout_alignParentTop="true" android:orientation="vertical" + android:gravity="center_vertical" android:layout_gravity="center" android:layout_marginTop="2dp" android:layout_marginEnd="52dp" diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 29033a2f23..83e0d847ac 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1734,7 +1734,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming Name or ID (#example:matrix.org) Enable swipe to reply in timeline - Hide failed to decrypt technical details in timeline + Merge failed to decrypt message in timeline Link copied to clipboard diff --git a/vector/src/main/res/xml/vector_settings_labs.xml b/vector/src/main/res/xml/vector_settings_labs.xml index 0379d49d26..9917bb0feb 100644 --- a/vector/src/main/res/xml/vector_settings_labs.xml +++ b/vector/src/main/res/xml/vector_settings_labs.xml @@ -41,9 +41,9 @@ + android:defaultValue="false" + android:key="SETTINGS_LABS_MERGE_E2E_ERRORS" + android:title="@string/labs_merge_e2e_in_timeline" /> \ No newline at end of file From 6e66c319116659e4cf33a4872a1a2c8048a27059 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 30 Jun 2020 18:03:25 +0200 Subject: [PATCH 15/15] Small cleanup --- tools/check/forbidden_strings_in_code.txt | 2 +- .../home/room/detail/RoomDetailViewModel.kt | 1 - .../item_timeline_event_base_noinfo.xml | 2 +- .../item_timeline_event_merged_utd_stub.xml | 23 ++++++++++--------- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index f7eb7f9c51..68ee0b2dd2 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -164,7 +164,7 @@ Formatter\.formatShortFileSize===1 # android\.text\.TextUtils ### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If it is ok, change the value in file forbidden_strings_in_code.txt -enum class===72 +enum class===73 ### Do not import temporary legacy classes import im.vector.matrix.android.internal.legacy.riot===3 diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 7a3f77039e..9a79ea6a0a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -60,7 +60,6 @@ import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent import im.vector.matrix.android.api.util.toOptional import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent -import im.vector.matrix.rx.asObservable import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode import im.vector.matrix.rx.rx import im.vector.matrix.rx.unwrap diff --git a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml index f8d46bb04e..d509d4e88b 100644 --- a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml +++ b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml @@ -65,7 +65,7 @@ android:id="@+id/messageContentMergedUTDStub" style="@style/TimelineContentStubBaseParams" android:layout="@layout/item_timeline_event_merged_utd_stub" - tools:layout_marginTop="240dp" + tools:layout_marginTop="380dp" tools:visibility="visible" /> diff --git a/vector/src/main/res/layout/item_timeline_event_merged_utd_stub.xml b/vector/src/main/res/layout/item_timeline_event_merged_utd_stub.xml index 039e0b7960..847324884c 100644 --- a/vector/src/main/res/layout/item_timeline_event_merged_utd_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_merged_utd_stub.xml @@ -1,22 +1,22 @@ + android:layout_height="wrap_content"> + app:drawableTint="?riotx_text_secondary" /> + android:textStyle="italic" /> + android:textStyle="italic" + android:visibility="gone" + tools:visibility="visible" />