diff --git a/.travis.yml b/.travis.yml index 6b9e9f9946..85bddac7f3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,10 +23,10 @@ android: - platform-tools # The BuildTools version used by your project - - build-tools-28.0.3 + - build-tools-29.0.3 # The SDK version used to compile your project - - android-28 + - android-29 before_cache: - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock diff --git a/CHANGES.md b/CHANGES.md index 9b3846c41b..4ca393ab73 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,26 +2,60 @@ Changes in RiotX 0.19.0 (2020-XX-XX) =================================================== Features ✨: - - + - Cross-Signing | Support SSSS secret sharing (#944) + - Cross-Signing | Verify new session from existing session (#1134) + - Cross-Signing | Bootstraping cross signing with 4S from mobile (#985) + Improvements 🙌: - - + - Verification DM / Handle concurrent .start after .ready (#794) + - Reimplementation of multiple attachment picker + - Cross-Signing | Update Shield Logic for DM (#963) + - Cross-Signing | Complete security new session design update (#1135) + - Cross-Signing | Setup key backup as part of SSSS bootstrapping (#1201) + - Cross-Signing | Gossip key backup recovery key (#1200) + - Show room encryption status as a bubble tile (#1078) + - UX/UI | Add indicator to home tab on invite (#957) + - Cross-Signing | Restore history after recover from passphrase (#1214) + - Cross-Sign | QR code scan confirmation screens design update (#1187) + - Emoji Verification | It's not the same butterfly! (#1220) + - Cross-Signing | Composer decoration: shields (#1077) Bugfix 🐛: - - Message transitions in encrypted rooms are jarring #518 - Fix summary notification staying after "mark as read" + - Missing avatar/displayname after verification request message (#841) + - Crypto | RiotX sometimes rotate the current device keys (#1170) + - RiotX can't restore cross signing keys saved by web in SSSS (#1174) + - Cross- Signing | After signin in new session, verification paper trail in DM is off (#1191) + - Failed to encrypt message in room (message stays in red), [thanks to pwr22] (#925) + - Cross-Signing | web <-> riotX After QR code scan, gossiping fails (#1210) + - Fix crash when trying to download file without internet connection (#1229) + - Local echo are not updated in timeline (for failed & encrypted states) Translations 🗣: - SDK API changes ⚠️: - - + - Increase targetSdkVersion to 29 Build 🧱: - - + - Compile with Android SDK 29 (Android Q) Other changes: - - + - Increase File Logger capacities ( + use dev log preferences) + +Changes in RiotX 0.18.1 (2020-03-17) +=================================================== + +Improvements 🙌: + - Implementation of /join command + +Bugfix 🐛: + - Message transitions in encrypted rooms are jarring #518 + - Images that failed to send are waiting to be sent forever #1145 + - Fix / Crashed when trying to send a gif from the Gboard #1136 + - Fix / Cannot click on key backup banner when new keys are available + Changes in RiotX 0.18.0 (2020-03-11) =================================================== diff --git a/matrix-sdk-android-rx/build.gradle b/matrix-sdk-android-rx/build.gradle index 8907ee8b50..734ff0c130 100644 --- a/matrix-sdk-android-rx/build.gradle +++ b/matrix-sdk-android-rx/build.gradle @@ -3,11 +3,11 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' android { - compileSdkVersion 28 + compileSdkVersion 29 defaultConfig { minSdkVersion 16 - targetSdkVersion 28 + targetSdkVersion 29 versionCode 1 versionName "1.0" diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index f980279a8d..c10aaf3545 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -19,12 +19,12 @@ androidExtensions { } android { - compileSdkVersion 28 + compileSdkVersion 29 testOptions.unitTests.includeAndroidResources = true defaultConfig { minSdkVersion 16 - targetSdkVersion 28 + targetSdkVersion 29 versionCode 1 versionName "0.0.1" // Multidex is useful for tests @@ -97,6 +97,7 @@ dependencies { def coroutines_version = "1.3.2" def markwon_version = '3.1.0' def daggerVersion = '2.25.4' + def work_version = '2.3.3' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" @@ -118,7 +119,7 @@ dependencies { implementation "ru.noties.markwon:core:$markwon_version" // Image - implementation 'androidx.exifinterface:exifinterface:1.1.0' + implementation 'androidx.exifinterface:exifinterface:1.3.0-alpha01' implementation 'id.zelory:compressor:3.0.0' // Database @@ -126,7 +127,7 @@ dependencies { kapt 'dk.ilios:realmfieldnameshelper:1.1.1' // Work - implementation "androidx.work:work-runtime-ktx:2.3.3" + implementation "androidx.work:work-runtime-ktx:$work_version" // FP implementation "io.arrow-kt:arrow-core:$arrow_version" 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 386787b882..3cf03fff53 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 @@ -38,6 +38,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.api.session.sync.SyncState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals @@ -265,8 +266,26 @@ class CommonTestHelper(context: Context) { * @param latch * @throws InterruptedException */ - fun await(latch: CountDownLatch) { - assertTrue(latch.await(TestConstants.timeOutMillis, TimeUnit.MILLISECONDS)) + fun await(latch: CountDownLatch, timout: Long? = TestConstants.timeOutMillis) { + assertTrue(latch.await(timout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS)) + } + + fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) { + GlobalScope.launch { + while (true) { + delay(1000) + if (condition()) { + latch.countDown() + return@launch + } + } + } + } + + fun waitWithLatch(timout: Long? = TestConstants.timeOutMillis, block: (CountDownLatch) -> Unit) { + val latch = CountDownLatch(1) + block(latch) + await(latch, timout) } // Transform a method with a MatrixCallback to a synchronous method diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestConstants.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestConstants.kt index 2346898ca7..deaa721e40 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestConstants.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/TestConstants.kt @@ -22,8 +22,8 @@ object TestConstants { const val TESTS_HOME_SERVER_URL = "http://10.0.2.2:8080" - // Time out to use when waiting for server response. 10s - private const val AWAIT_TIME_OUT_MILLIS = 10_000 + // Time out to use when waiting for server response. 20s + private const val AWAIT_TIME_OUT_MILLIS = 20_000 // Time out to use when waiting for server response, when the debugger is connected. 10 minutes private const val AWAIT_TIME_OUT_WITH_DEBUGGER_MILLIS = 10 * 60_000 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 new file mode 100644 index 0000000000..bb6e020d89 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt @@ -0,0 +1,289 @@ +/* + * 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 android.util.Log +import androidx.test.ext.junit.runners.AndroidJUnit4 +import im.vector.matrix.android.InstrumentedTest +import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction +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.VerificationService +import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction +import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState +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.SessionTestParams +import im.vector.matrix.android.common.TestConstants +import im.vector.matrix.android.internal.crypto.GossipingRequestState +import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestState +import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel +import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion +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.EncryptedEventContent +import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertTrue +import junit.framework.TestCase.fail +import org.junit.Assert +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import java.util.concurrent.CountDownLatch + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class KeyShareTests : InstrumentedTest { + + private val mTestHelper = CommonTestHelper(context()) + + @Test + fun test_DoNotSelfShareIfNotTrusted() { + val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + + // Create an encrypted room and add a message + val roomId = mTestHelper.doSync { + aliceSession.createRoom( + CreateRoomParams(RoomDirectoryVisibility.PRIVATE).enableEncryptionWithAlgorithm(true), + it + ) + } + val room = aliceSession.getRoom(roomId) + assertNotNull(room) + Thread.sleep(4_000) + assertTrue(room?.isEncrypted() == true) + val sentEventId = mTestHelper.sendTextMessage(room!!, "My Message", 1).first().eventId + + // Open a new sessionx + + val aliceSession2 = mTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true)) + + val roomSecondSessionPOV = aliceSession2.getRoom(roomId) + + val receivedEvent = roomSecondSessionPOV?.getTimeLineEvent(sentEventId) + assertNotNull(receivedEvent) + assert(receivedEvent!!.isEncrypted()) + + try { + aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") + fail("should fail") + } catch (failure: Throwable) { + } + + val outgoingRequestBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequest() + // Try to request + aliceSession2.cryptoService().requestRoomKeyForEvent(receivedEvent.root) + + val waitLatch = CountDownLatch(1) + val eventMegolmSessionId = receivedEvent.root.content.toModel()?.sessionId + + var outGoingRequestId: String? = null + + mTestHelper.retryPeriodicallyWithLatch(waitLatch) { + aliceSession2.cryptoService().getOutgoingRoomKeyRequest() + .filter { req -> + // filter out request that was known before + !outgoingRequestBefore.any { req.requestId == it.requestId } + } + .let { + val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId } + outGoingRequestId = outgoing?.requestId + outgoing != null + } + } + mTestHelper.await(waitLatch) + + Log.v("TEST", "=======> Outgoing requet Id is $outGoingRequestId") + + val outgoingRequestAfter = aliceSession2.cryptoService().getOutgoingRoomKeyRequest() + + // We should have a new request + Assert.assertTrue(outgoingRequestAfter.size > outgoingRequestBefore.size) + Assert.assertNotNull(outgoingRequestAfter.first { it.sessionId == eventMegolmSessionId }) + + // The first session should see an incoming request + // the request should be refused, because the device is not trusted + mTestHelper.waitWithLatch { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + // DEBUG LOGS + aliceSession.cryptoService().getIncomingRoomKeyRequest().let { + Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)") + Log.v("TEST", "=========================") + it.forEach { keyRequest -> + Log.v("TEST", "[ts${keyRequest.localCreationTimestamp}] requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId} is ${keyRequest.state}") + } + Log.v("TEST", "=========================") + } + + val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequest().firstOrNull { it.requestId == outGoingRequestId } + incoming?.state == GossipingRequestState.REJECTED + } + } + + try { + aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") + fail("should fail") + } catch (failure: Throwable) { + } + + // Mark the device as trusted + aliceSession.cryptoService().setDeviceVerification(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession.myUserId, + aliceSession2.sessionParams.credentials.deviceId ?: "") + + // Re request + aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root) + + mTestHelper.waitWithLatch { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + aliceSession.cryptoService().getIncomingRoomKeyRequest().let { + Log.v("TEST", "Incoming request Session 1") + Log.v("TEST", "=========================") + it.forEach { + Log.v("TEST", "requestId ${it.requestId}, for sessionId ${it.requestBody?.sessionId} is ${it.state}") + } + Log.v("TEST", "=========================") + + it.any { it.requestBody?.sessionId == eventMegolmSessionId && it.state == GossipingRequestState.ACCEPTED } + } + } + } + + Thread.sleep(6_000) + mTestHelper.waitWithLatch { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + aliceSession2.cryptoService().getOutgoingRoomKeyRequest().let { + it.any { it.requestBody?.sessionId == eventMegolmSessionId && it.state == OutgoingGossipingRequestState.CANCELLED } + } + } + } + + try { + aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") + } catch (failure: Throwable) { + fail("should have been able to decrypt") + } + + mTestHelper.signOutAndClose(aliceSession) + mTestHelper.signOutAndClose(aliceSession2) + } + + @Test + fun test_ShareSSSSSecret() { + val aliceSession1 = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + + mTestHelper.doSync { + aliceSession1.cryptoService().crossSigningService() + .initializeCrossSigning(UserPasswordAuth( + user = aliceSession1.myUserId, + password = TestConstants.PASSWORD + ), it) + } + + // Also bootstrap keybackup on first session + val creationInfo = mTestHelper.doSync { + aliceSession1.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it) + } + val version = mTestHelper.doSync { + aliceSession1.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it) + } + // Save it for gossiping + aliceSession1.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version) + + val aliceSession2 = mTestHelper.logIntoAccount(aliceSession1.myUserId, SessionTestParams(true)) + + val aliceVerificationService1 = aliceSession1.cryptoService().verificationService() + val aliceVerificationService2 = aliceSession2.cryptoService().verificationService() + + // force keys download + mTestHelper.doSync> { + aliceSession1.cryptoService().downloadKeys(listOf(aliceSession1.myUserId), true, it) + } + mTestHelper.doSync> { + aliceSession2.cryptoService().downloadKeys(listOf(aliceSession2.myUserId), true, it) + } + + var session1ShortCode: String? = null + var session2ShortCode: String? = null + + aliceVerificationService1.addListener(object : VerificationService.Listener { + override fun transactionUpdated(tx: VerificationTransaction) { + Log.d("#TEST", "AA: tx incoming?:${tx.isIncoming} state ${tx.state}") + if (tx is SasVerificationTransaction) { + if (tx.state == VerificationTxState.OnStarted) { + (tx as IncomingSasVerificationTransaction).performAccept() + } + if (tx.state == VerificationTxState.ShortCodeReady) { + session1ShortCode = tx.getDecimalCodeRepresentation() + tx.userHasVerifiedShortCode() + } + } + } + }) + + aliceVerificationService2.addListener(object : VerificationService.Listener { + override fun transactionUpdated(tx: VerificationTransaction) { + Log.d("#TEST", "BB: tx incoming?:${tx.isIncoming} state ${tx.state}") + if (tx is SasVerificationTransaction) { + if (tx.state == VerificationTxState.ShortCodeReady) { + session2ShortCode = tx.getDecimalCodeRepresentation() + tx.userHasVerifiedShortCode() + } + } + } + }) + + val txId: String = "m.testVerif12" + aliceVerificationService2.beginKeyVerification(VerificationMethod.SAS, aliceSession1.myUserId, aliceSession1.sessionParams.credentials.deviceId + ?: "", txId) + + mTestHelper.waitWithLatch { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + aliceSession1.cryptoService().getDeviceInfo(aliceSession1.myUserId, aliceSession2.sessionParams.credentials.deviceId ?: "")?.isVerified == true + } + } + + assertNotNull(session1ShortCode) + Log.d("#TEST", "session1ShortCode: $session1ShortCode") + assertNotNull(session2ShortCode) + Log.d("#TEST", "session2ShortCode: $session2ShortCode") + assertEquals(session1ShortCode, session2ShortCode) + + // SSK and USK private keys should have been shared + + mTestHelper.waitWithLatch(60_000) { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + Log.d("#TEST", "CAN XS :${aliceSession2.cryptoService().crossSigningService().getMyCrossSigningKeys()}") + aliceSession2.cryptoService().crossSigningService().canCrossSign() + } + } + + // Test that key backup key has been shared to + mTestHelper.waitWithLatch(60_000) { latch -> + val keysBackupService = aliceSession2.cryptoService().keysBackupService() + mTestHelper.retryPeriodicallyWithLatch(latch) { + Log.d("#TEST", "Recovery :${ keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey}") + keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey == creationInfo.recoveryKey + } + } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt index 77ba66d341..3042a3c68f 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt @@ -34,7 +34,6 @@ import im.vector.matrix.android.common.assertDictEquals import im.vector.matrix.android.common.assertListEquals import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP import im.vector.matrix.android.internal.crypto.MegolmSessionData -import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrust import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo @@ -326,46 +325,46 @@ class KeysBackupTest : InstrumentedTest { * - Restore must be successful * - *** There must be no more pending key share requests */ - @Test - fun restoreKeysBackupAndKeyShareRequestTest() { - fail("Check with Valere for this test. I think we do not send key share request") - - val testData = createKeysBackupScenarioWithPassword(null) - - // - Check the SDK sent key share requests - val cryptoStore2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store - val unsentRequest = cryptoStore2 - .getOutgoingRoomKeyRequestByState(setOf(OutgoingRoomKeyRequest.RequestState.UNSENT)) - val sentRequest = cryptoStore2 - .getOutgoingRoomKeyRequestByState(setOf(OutgoingRoomKeyRequest.RequestState.SENT)) - - // Request is either sent or unsent - assertTrue(unsentRequest != null || sentRequest != null) - - // - Restore the e2e backup from the homeserver - val importRoomKeysResult = mTestHelper.doSync { - testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, - testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, - null, - null, - null, - it - ) - } - - checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys) - - // - There must be no more pending key share requests - val unsentRequestAfterRestoration = cryptoStore2 - .getOutgoingRoomKeyRequestByState(setOf(OutgoingRoomKeyRequest.RequestState.UNSENT)) - val sentRequestAfterRestoration = cryptoStore2 - .getOutgoingRoomKeyRequestByState(setOf(OutgoingRoomKeyRequest.RequestState.SENT)) - - // Request is either sent or unsent - assertTrue(unsentRequestAfterRestoration == null && sentRequestAfterRestoration == null) - - testData.cleanUp(mTestHelper) - } +// @Test +// fun restoreKeysBackupAndKeyShareRequestTest() { +// fail("Check with Valere for this test. I think we do not send key share request") +// +// val testData = createKeysBackupScenarioWithPassword(null) +// +// // - Check the SDK sent key share requests +// val cryptoStore2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store +// val unsentRequest = cryptoStore2 +// .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.UNSENT)) +// val sentRequest = cryptoStore2 +// .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.SENT)) +// +// // Request is either sent or unsent +// assertTrue(unsentRequest != null || sentRequest != null) +// +// // - Restore the e2e backup from the homeserver +// val importRoomKeysResult = mTestHelper.doSync { +// testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, +// testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, +// null, +// null, +// null, +// it +// ) +// } +// +// checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys) +// +// // - There must be no more pending key share requests +// val unsentRequestAfterRestoration = cryptoStore2 +// .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.UNSENT)) +// val sentRequestAfterRestoration = cryptoStore2 +// .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.SENT)) +// +// // Request is either sent or unsent +// assertTrue(unsentRequestAfterRestoration == null && sentRequestAfterRestoration == null) +// +// testData.cleanUp(mTestHelper) +// } /** * - Do an e2e backup to the homeserver with a recovery key diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/SASTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/SASTest.kt index eefb040987..1ac70d7f2b 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/SASTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/SASTest.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.internal.crypto.verification +import android.util.Log import androidx.test.ext.junit.runners.AndroidJUnit4 import im.vector.matrix.android.InstrumentedTest import im.vector.matrix.android.api.session.Session @@ -23,6 +24,7 @@ import im.vector.matrix.android.api.session.crypto.verification.CancelCode 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.SasMode +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.VerificationService import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction @@ -355,6 +357,7 @@ class SASTest : InstrumentedTest { val aliceAcceptedLatch = CountDownLatch(1) val aliceListener = object : VerificationService.Listener { override fun transactionUpdated(tx: VerificationTransaction) { + Log.v("TEST", "== aliceTx state ${tx.state} => ${(tx as? OutgoingSasVerificationTransaction)?.uxState}") if ((tx as SASDefaultVerificationTransaction).state === VerificationTxState.OnAccepted) { val at = tx as SASDefaultVerificationTransaction accepted = at.accepted @@ -367,7 +370,9 @@ class SASTest : InstrumentedTest { val bobListener = object : VerificationService.Listener { override fun transactionUpdated(tx: VerificationTransaction) { + Log.v("TEST", "== bobTx state ${tx.state} => ${(tx as? IncomingSasVerificationTransaction)?.uxState}") if ((tx as IncomingSasVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) { + bobVerificationService.removeListener(this) val at = tx as IncomingSasVerificationTransaction at.performAccept() } @@ -515,4 +520,96 @@ class SASTest : InstrumentedTest { assertTrue("bob device should be verified from alice point of view", bobDeviceInfoFromAlicePOV!!.isVerified) cryptoTestData.cleanUp(mTestHelper) } + + @Test + fun test_ConcurrentStart() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession + + val aliceVerificationService = aliceSession.cryptoService().verificationService() + val bobVerificationService = bobSession!!.cryptoService().verificationService() + + val req = aliceVerificationService.requestKeyVerificationInDMs( + listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), + bobSession.myUserId, + cryptoTestData.roomId + ) + + var requestID : String? = null + + mTestHelper.waitWithLatch { + mTestHelper.retryPeriodicallyWithLatch(it) { + val prAlicePOV = aliceVerificationService.getExistingVerificationRequest(bobSession.myUserId)?.firstOrNull() + requestID = prAlicePOV?.transactionId + Log.v("TEST", "== alicePOV is $prAlicePOV") + prAlicePOV?.transactionId != null && prAlicePOV.localId == req.localId + } + } + + Log.v("TEST", "== requestID is $requestID") + + mTestHelper.waitWithLatch { + mTestHelper.retryPeriodicallyWithLatch(it) { + val prBobPOV = bobVerificationService.getExistingVerificationRequest(aliceSession.myUserId)?.firstOrNull() + Log.v("TEST", "== prBobPOV is $prBobPOV") + prBobPOV?.transactionId == requestID + } + } + + bobVerificationService.readyPendingVerification( + listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), + aliceSession.myUserId, + requestID!! + ) + + // wait for alice to get the ready + mTestHelper.waitWithLatch { + mTestHelper.retryPeriodicallyWithLatch(it) { + val prAlicePOV = aliceVerificationService.getExistingVerificationRequest(bobSession.myUserId)?.firstOrNull() + Log.v("TEST", "== prAlicePOV is $prAlicePOV") + prAlicePOV?.transactionId == requestID && prAlicePOV?.isReady != null + } + } + + // Start concurrent! + aliceVerificationService.beginKeyVerificationInDMs( + VerificationMethod.SAS, + requestID!!, + cryptoTestData.roomId, + bobSession.myUserId, + bobSession.sessionParams.credentials.deviceId!!, + null) + + bobVerificationService.beginKeyVerificationInDMs( + VerificationMethod.SAS, + requestID!!, + cryptoTestData.roomId, + aliceSession.myUserId, + aliceSession.sessionParams.credentials.deviceId!!, + null) + + // we should reach SHOW SAS on both + var alicePovTx: SasVerificationTransaction? + var bobPovTx: SasVerificationTransaction? + + mTestHelper.waitWithLatch { + mTestHelper.retryPeriodicallyWithLatch(it) { + alicePovTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, requestID!!) as? SasVerificationTransaction + Log.v("TEST", "== alicePovTx is $alicePovTx") + alicePovTx?.state == VerificationTxState.ShortCodeReady + } + } + // wait for alice to get the ready + mTestHelper.waitWithLatch { + mTestHelper.retryPeriodicallyWithLatch(it) { + bobPovTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, requestID!!) as? SasVerificationTransaction + Log.v("TEST", "== bobPovTx is $bobPovTx") + bobPovTx?.state == VerificationTxState.ShortCodeReady + } + } + + cryptoTestData.cleanUp(mTestHelper) + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/crypto/MXCryptoConfig.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/crypto/MXCryptoConfig.kt index dc08023d99..a8d576bae9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/crypto/MXCryptoConfig.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/crypto/MXCryptoConfig.kt @@ -23,5 +23,14 @@ data class MXCryptoConfig( // Tell whether the encryption of the event content is enabled for the invited members. // SDK clients can disable this by settings it to false. // Note that the encryption for the invited members will be blocked if the history visibility is "joined". - var enableEncryptionForInvitedMembers: Boolean = true + var enableEncryptionForInvitedMembers: Boolean = true, + + /** + * If set to true, the SDK will automatically ignore room key request (gossiping) + * coming from your other untrusted sessions (or blocked). + * If set to false, the request will be forwarded to the application layer; in this + * case the application can decide to prompt the user. + */ + var discardRoomKeyRequestsFromUntrustedDevices : Boolean = true + ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/Try.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/Try.kt new file mode 100644 index 0000000000..3afcac08c1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/Try.kt @@ -0,0 +1,25 @@ +/* + * 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.extensions + +inline fun tryThis(operation: () -> A): A? { + return try { + operation() + } catch (any: Throwable) { + null + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt index e32bb9f21f..b80a17b017 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.api.session.content +import android.net.Uri import android.os.Parcelable import androidx.exifinterface.media.ExifInterface import kotlinx.android.parcel.Parcelize @@ -29,8 +30,7 @@ data class ContentAttachmentData( val width: Long? = 0, val exifOrientation: Int = ExifInterface.ORIENTATION_UNDEFINED, val name: String? = null, - val queryUri: String, - val path: String, + val queryUri: Uri, private val mimeType: String?, val type: Type ) : Parcelable { 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 1360924270..ab8417b542 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 @@ -22,12 +22,14 @@ import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.listeners.ProgressListener import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService -import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener +import im.vector.matrix.android.api.session.crypto.keyshare.GossipingRequestListener import im.vector.matrix.android.api.session.crypto.verification.VerificationService import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult import im.vector.matrix.android.internal.crypto.NewSessionListener +import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult @@ -86,13 +88,15 @@ interface CryptoService { fun getDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? + fun requestRoomKeyForEvent(event: Event) + fun reRequestRoomKeyForEvent(event: Event) fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) - fun addRoomKeysRequestListener(listener: RoomKeysRequestListener) + fun addRoomKeysRequestListener(listener: GossipingRequestListener) - fun removeRoomKeysRequestListener(listener: RoomKeysRequestListener) + fun removeRoomKeysRequestListener(listener: GossipingRequestListener) fun getDevicesList(callback: MatrixCallback) @@ -129,4 +133,8 @@ interface CryptoService { fun addNewSessionListener(newSessionListener: NewSessionListener) fun removeSessionListener(listener: NewSessionListener) + + fun getOutgoingRoomKeyRequest(): List + fun getIncomingRoomKeyRequest(): List + fun getGossipingEventsTrail(): List } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningService.kt index ff4745ef46..4085e1233d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningService.kt @@ -22,6 +22,7 @@ import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustResult import im.vector.matrix.android.internal.crypto.crosssigning.UserTrustResult import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth +import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo interface CrossSigningService { @@ -52,6 +53,8 @@ interface CrossSigningService { fun getMyCrossSigningKeys(): MXCrossSigningInfo? + fun getCrossSigningPrivateKeys(): PrivateKeysInfo? + fun canCrossSign(): Boolean fun trustUser(otherUserId: String, @@ -68,4 +71,7 @@ interface CrossSigningService { fun checkDeviceTrust(otherUserId: String, otherDeviceId: String, locallyTrusted: Boolean?): DeviceTrustResult + + fun onSecretSSKGossip(sskPrivateKey: String) + fun onSecretUSKGossip(uskPrivateKey: String) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningSsssSecretConstants.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningSsssSecretConstants.kt index d46a724463..2c3425bcb4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningSsssSecretConstants.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningSsssSecretConstants.kt @@ -21,3 +21,5 @@ const val MASTER_KEY_SSSS_NAME = "m.cross_signing.master" const val USER_SIGNING_KEY_SSSS_NAME = "m.cross_signing.user_signing" const val SELF_SIGNING_KEY_SSSS_NAME = "m.cross_signing.self_signing" + +const val KEYBACKUP_SECRET_SSSS_NAME = "m.megolm_backup.v1" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keysbackup/KeysBackupService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keysbackup/KeysBackupService.kt index cb06dbf665..e9ed36ba23 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keysbackup/KeysBackupService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keysbackup/KeysBackupService.kt @@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCre import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult +import im.vector.matrix.android.internal.crypto.store.SavedKeyBackupKeyInfo interface KeysBackupService { /** @@ -172,6 +173,8 @@ interface KeysBackupService { password: String, callback: MatrixCallback) + fun onSecretKeyGossip(secret: String) + /** * Restore a backup with a recovery key from a given backup version stored on the homeserver. * @@ -210,4 +213,8 @@ interface KeysBackupService { val isEnabled: Boolean val isStucked: Boolean val state: KeysBackupState + + // For gossiping + fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) + fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo? } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keyshare/RoomKeysRequestListener.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keyshare/GossipingRequestListener.kt similarity index 70% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keyshare/RoomKeysRequestListener.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keyshare/GossipingRequestListener.kt index 5bce27e1b4..1dad685f41 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keyshare/RoomKeysRequestListener.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keyshare/GossipingRequestListener.kt @@ -17,12 +17,13 @@ package im.vector.matrix.android.api.session.crypto.keyshare import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest -import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequestCancellation +import im.vector.matrix.android.internal.crypto.IncomingRequestCancellation +import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest /** * Room keys events listener */ -interface RoomKeysRequestListener { +interface GossipingRequestListener { /** * An room key request has been received. * @@ -30,10 +31,16 @@ interface RoomKeysRequestListener { */ fun onRoomKeyRequest(request: IncomingRoomKeyRequest) + /** + * Returns the secret value to be shared + * @return true if is handled + */ + fun onSecretShareRequest(request: IncomingSecretShareRequest) : Boolean + /** * A room key request cancellation has been received. * * @param request the cancellation request */ - fun onRoomKeyRequestCancellation(request: IncomingRoomKeyRequestCancellation) + fun onRoomKeyRequestCancellation(request: IncomingRequestCancellation) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/EmojiRepresentation.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/EmojiRepresentation.kt index 9ee7c92788..7d18d9bd70 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/EmojiRepresentation.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/EmojiRepresentation.kt @@ -16,7 +16,10 @@ package im.vector.matrix.android.api.session.crypto.verification +import androidx.annotation.DrawableRes import androidx.annotation.StringRes data class EmojiRepresentation(val emoji: String, - @StringRes val nameResId: Int) + @StringRes val nameResId: Int, + @DrawableRes val drawableRes: Int? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/VerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/VerificationService.kt index 5cad8d985c..4482101434 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/VerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/VerificationService.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.api.session.crypto.verification import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.LocalEcho /** @@ -59,6 +60,8 @@ interface VerificationService { roomId: String, localId: String? = LocalEcho.createLocalEchoId()): PendingVerificationRequest + fun cancelVerificationRequest(request: PendingVerificationRequest) + /** * Request a key verification from another user using toDevice events. */ @@ -136,4 +139,6 @@ interface VerificationService { return age in tooInThePast..tooInTheFuture } } + + fun onPotentiallyInterestingEventRoomFailToDecrypt(event: Event) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/VerificationTxState.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/VerificationTxState.kt index aaaf227187..868ec5a3e2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/VerificationTxState.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/VerificationTxState.kt @@ -43,6 +43,7 @@ sealed class VerificationTxState { // Will be used to ask the user if the other user has correctly scanned object QrScannedByOther : VerificationQrTxState() + object WaitingOtherReciprocateConfirm : VerificationQrTxState() // Terminal states abstract class TerminalTxState : VerificationTxState() 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 d131960893..a60d0fd9ac 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 @@ -142,12 +142,12 @@ data class Event( } fun toContentStringWithIndent(): String { - val contentMap = toContent().toMutableMap() + val contentMap = toContent() return JSONObject(contentMap).toString(4) } fun toClearContentStringWithIndent(): String? { - val contentMap = this.mxDecryptionResult?.payload?.toMutableMap() + val contentMap = this.mxDecryptionResult?.payload val adapter = MoshiProvider.providesMoshi().adapter(Map::class.java) return contentMap?.let { JSONObject(adapter.toJson(it)).toString(4) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/KeySigner.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/KeySigner.kt index 2cd7a74f31..242f9a8945 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/KeySigner.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/KeySigner.kt @@ -19,3 +19,7 @@ package im.vector.matrix.android.api.session.securestorage interface KeySigner { fun sign(canonicalJson: String): Map>? } + +class EmptyKeySigner : KeySigner { + override fun sign(canonicalJson: String): Map>? = null +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SharedSecretStorageService.kt index 596d8d3e5d..d32e459dd6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SharedSecretStorageService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SharedSecretStorageService.kt @@ -111,6 +111,8 @@ interface SharedSecretStorageService { fun checkShouldBeAbleToAccessSecrets(secretNames: List, keyId: String?) : IntegrityResult + fun requestSecret(name: String, myOtherDeviceId: String) + data class KeyRef( val keyId: String?, val keySpec: SsssKeySpec? diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SsssKeyCreationInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SsssKeyCreationInfo.kt index 1d5522b8bf..85823d9dbf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SsssKeyCreationInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SsssKeyCreationInfo.kt @@ -19,5 +19,6 @@ package im.vector.matrix.android.api.session.securestorage data class SsssKeyCreationInfo( val keyId: String = "", var content: SecretStorageKeyContent?, - val recoveryKey: String = "" + val recoveryKey: String = "", + val keySpec: SsssKeySpec ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CancelGossipRequestWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CancelGossipRequestWorker.kt new file mode 100644 index 0000000000..54f89dc8b9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CancelGossipRequestWorker.kt @@ -0,0 +1,118 @@ +/* + * 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 + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.failure.shouldBeRetried +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.LocalEcho +import im.vector.matrix.android.api.session.events.model.toContent +import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap +import im.vector.matrix.android.internal.crypto.model.rest.ShareRequestCancellation +import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore +import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask +import im.vector.matrix.android.internal.worker.WorkerParamsFactory +import im.vector.matrix.android.internal.worker.getSessionComponent +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal class CancelGossipRequestWorker(context: Context, + params: WorkerParameters) + : CoroutineWorker(context, params) { + + @JsonClass(generateAdapter = true) + internal data class Params( + val sessionId: String, + val requestId: String, + val recipients: Map> + ) { + companion object { + fun fromRequest(sessionId: String, request: OutgoingGossipingRequest): Params { + return Params( + sessionId = sessionId, + requestId = request.requestId, + recipients = request.recipients + ) + } + } + } + + @Inject lateinit var sendToDeviceTask: SendToDeviceTask + @Inject lateinit var cryptoStore: IMXCryptoStore + @Inject lateinit var eventBus: EventBus + @Inject lateinit var credentials: Credentials + + override suspend fun doWork(): Result { + val errorOutputData = Data.Builder().putBoolean("failed", true).build() + val params = WorkerParamsFactory.fromData(inputData) + ?: return Result.success(errorOutputData) + + val sessionComponent = getSessionComponent(params.sessionId) + ?: return Result.success(errorOutputData).also { + // TODO, can this happen? should I update local echo? + Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}") + } + sessionComponent.inject(this) + + val localId = LocalEcho.createLocalEchoId() + val contentMap = MXUsersDevicesMap() + val toDeviceContent = ShareRequestCancellation( + requestingDeviceId = credentials.deviceId, + requestId = params.requestId + ) + cryptoStore.saveGossipingEvent(Event( + type = EventType.ROOM_KEY_REQUEST, + content = toDeviceContent.toContent(), + senderId = credentials.userId + ).also { + it.ageLocalTs = System.currentTimeMillis() + }) + + params.recipients.forEach { userToDeviceMap -> + userToDeviceMap.value.forEach { deviceId -> + contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent) + } + } + + try { + cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.CANCELLING) + sendToDeviceTask.execute( + SendToDeviceTask.Params( + eventType = EventType.ROOM_KEY_REQUEST, + contentMap = contentMap, + transactionId = localId + ) + ) + cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.CANCELLED) + return Result.success() + } catch (exception: Throwable) { + return if (exception.shouldBeRetried()) { + Result.retry() + } else { + cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.FAILED_TO_CANCEL) + Result.success(errorOutputData) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt index 28375a091b..50ffb3082a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt @@ -66,6 +66,7 @@ import im.vector.matrix.android.internal.crypto.tasks.DefaultDownloadKeysForUser import im.vector.matrix.android.internal.crypto.tasks.DefaultEncryptEventTask import im.vector.matrix.android.internal.crypto.tasks.DefaultGetDeviceInfoTask import im.vector.matrix.android.internal.crypto.tasks.DefaultGetDevicesTask +import im.vector.matrix.android.internal.crypto.tasks.DefaultInitializeCrossSigningTask import im.vector.matrix.android.internal.crypto.tasks.DefaultSendToDeviceTask import im.vector.matrix.android.internal.crypto.tasks.DefaultSendVerificationMessageTask import im.vector.matrix.android.internal.crypto.tasks.DefaultSetDeviceNameTask @@ -78,6 +79,7 @@ import im.vector.matrix.android.internal.crypto.tasks.DownloadKeysForUsersTask import im.vector.matrix.android.internal.crypto.tasks.EncryptEventTask import im.vector.matrix.android.internal.crypto.tasks.GetDeviceInfoTask import im.vector.matrix.android.internal.crypto.tasks.GetDevicesTask +import im.vector.matrix.android.internal.crypto.tasks.InitializeCrossSigningTask import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask import im.vector.matrix.android.internal.crypto.tasks.SendVerificationMessageTask import im.vector.matrix.android.internal.crypto.tasks.SetDeviceNameTask @@ -245,4 +247,7 @@ internal abstract class CryptoModule { @Binds abstract fun bindComputeShieldTrustTask(task: DefaultComputeTrustTask): ComputeTrustTask + + @Binds + abstract fun bindInitializeCrossSigningTask(task: DefaultInitializeCrossSigningTask): InitializeCrossSigningTask } 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 bf923e9c58..ee96e45e85 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 @@ -33,7 +33,10 @@ import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.listeners.ProgressListener import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.crypto.MXCryptoError -import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener +import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME +import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME +import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME +import im.vector.matrix.android.api.session.crypto.keyshare.GossipingRequestListener import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType @@ -55,7 +58,9 @@ 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.EncryptedEventContent import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent +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 import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse @@ -80,6 +85,7 @@ import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembers import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper import im.vector.matrix.android.internal.session.sync.model.SyncResponse import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.TaskThread import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.JsonCanonicalizer import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers @@ -115,6 +121,7 @@ internal class DefaultCryptoService @Inject constructor( private val myDeviceInfoHolder: Lazy, // the crypto store private val cryptoStore: IMXCryptoStore, + // Olm device private val olmDevice: MXOlmDevice, // Set of parameters used to configure/customize the end-to-end crypto. @@ -134,9 +141,9 @@ internal class DefaultCryptoService @Inject constructor( private val crossSigningService: DefaultCrossSigningService, // - private val incomingRoomKeyRequestManager: IncomingRoomKeyRequestManager, + private val incomingGossipingRequestManager: IncomingGossipingRequestManager, // - private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager, + private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, // Actions private val setDeviceVerificationAction: SetDeviceVerificationAction, private val megolmSessionDataImporter: MegolmSessionDataImporter, @@ -188,6 +195,7 @@ internal class DefaultCryptoService @Inject constructor( override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback) { setDeviceNameTask .configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) { + this.executionThread = TaskThread.CRYPTO this.callback = object : MatrixCallback { override fun onSuccess(data: Unit) { // bg refresh of crypto device @@ -206,6 +214,7 @@ internal class DefaultCryptoService @Inject constructor( override fun deleteDevice(deviceId: String, callback: MatrixCallback) { deleteDeviceTask .configureWith(DeleteDeviceTask.Params(deviceId)) { + this.executionThread = TaskThread.CRYPTO this.callback = callback } .executeBy(taskExecutor) @@ -214,6 +223,7 @@ internal class DefaultCryptoService @Inject constructor( override fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback) { deleteDeviceWithUserPasswordTask .configureWith(DeleteDeviceWithUserPasswordTask.Params(deviceId, authSession, password)) { + this.executionThread = TaskThread.CRYPTO this.callback = callback } .executeBy(taskExecutor) @@ -230,6 +240,7 @@ internal class DefaultCryptoService @Inject constructor( override fun getDevicesList(callback: MatrixCallback) { getDevicesTask .configureWith { + // this.executionThread = TaskThread.CRYPTO this.callback = callback } .executeBy(taskExecutor) @@ -238,6 +249,7 @@ internal class DefaultCryptoService @Inject constructor( override fun getDeviceInfo(deviceId: String, callback: MatrixCallback) { getDeviceInfoTask .configureWith(GetDeviceInfoTask.Params(deviceId)) { + this.executionThread = TaskThread.CRYPTO this.callback = callback } .executeBy(taskExecutor) @@ -300,14 +312,13 @@ internal class DefaultCryptoService @Inject constructor( runCatching { uploadDeviceKeys() oneTimeKeysUploader.maybeUploadOneTimeKeys() - outgoingRoomKeyRequestManager.start() keysBackupService.checkAndStartKeysBackup() if (isInitialSync) { // refresh the devices list for each known room members deviceListManager.invalidateAllDeviceLists() deviceListManager.refreshOutdatedDeviceLists() } else { - incomingRoomKeyRequestManager.processReceivedRoomKeyRequests() + incomingGossipingRequestManager.processReceivedGossipingRequests() } }.fold( { @@ -328,8 +339,6 @@ internal class DefaultCryptoService @Inject constructor( fun close() = runBlocking(coroutineDispatchers.crypto) { cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module")) - outgoingRoomKeyRequestManager.stop() - olmDevice.release() cryptoStore.close() } @@ -368,7 +377,7 @@ internal class DefaultCryptoService @Inject constructor( // Make sure we process to-device messages before generating new one-time-keys #2782 deviceListManager.refreshOutdatedDeviceLists() oneTimeKeysUploader.maybeUploadOneTimeKeys() - incomingRoomKeyRequestManager.processReceivedRoomKeyRequests() + incomingGossipingRequestManager.processReceivedGossipingRequests() } } } @@ -627,7 +636,7 @@ internal class DefaultCryptoService @Inject constructor( */ @Throws(MXCryptoError::class) override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { - return internalDecryptEvent(event, timeline) + return internalDecryptEvent(event, timeline) } /** @@ -688,13 +697,24 @@ internal class DefaultCryptoService @Inject constructor( * @param event the event */ fun onToDeviceEvent(event: Event) { + // event have already been decrypted cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { when (event.getClearType()) { EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> { + cryptoStore.saveGossipingEvent(event) + // Keys are imported directly, not waiting for end of sync onRoomKeyEvent(event) } + EventType.REQUEST_SECRET, EventType.ROOM_KEY_REQUEST -> { - incomingRoomKeyRequestManager.onRoomKeyRequestEvent(event) + // save audit trail + cryptoStore.saveGossipingEvent(event) + // Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete) + incomingGossipingRequestManager.onGossipingRequestEvent(event) + } + EventType.SEND_SECRET -> { + cryptoStore.saveGossipingEvent(event) + onSecretSendReceived(event) } else -> { // ignore @@ -710,18 +730,70 @@ internal class DefaultCryptoService @Inject constructor( */ private fun onRoomKeyEvent(event: Event) { val roomKeyContent = event.getClearContent().toModel() ?: return + Timber.v("## GOSSIP onRoomKeyEvent() : type<${event.type}> , sessionId<${roomKeyContent.sessionId}>") if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) { - Timber.e("## onRoomKeyEvent() : missing fields") + Timber.e("## GOSSIP onRoomKeyEvent() : missing fields") return } val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(roomKeyContent.roomId, roomKeyContent.algorithm) if (alg == null) { - Timber.e("## onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}") + Timber.e("## GOSSIP onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}") return } alg.onRoomKeyEvent(event, keysBackupService) } + private fun onSecretSendReceived(event: Event) { + Timber.i("## GOSSIP onSecretSend() : onSecretSendReceived ${event.content?.get("sender_key")}") + if (!event.isEncrypted()) { + // secret send messages must be encrypted + Timber.e("## GOSSIP onSecretSend() :Received unencrypted secret send event") + return + } + + // Was that sent by us? + if (event.senderId != credentials.userId) { + Timber.e("## GOSSIP onSecretSend() : Ignore secret from other user ${event.senderId}") + return + } + + val secretContent = event.getClearContent().toModel() ?: return + + val existingRequest = cryptoStore + .getOutgoingSecretKeyRequests().firstOrNull { it.requestId == secretContent.requestId } + + if (existingRequest == null) { + Timber.i("## GOSSIP onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}") + return + } + + if (!handleSDKLevelGossip(existingRequest.secretName, secretContent.secretValue)) { + // TODO Ask to application layer? + Timber.v("## onSecretSend() : secret not handled by SDK") + } + } + + /** + * Returns true if handled by SDK, otherwise should be sent to application layer + */ + private fun handleSDKLevelGossip(secretName: String?, secretValue: String): Boolean { + return when (secretName) { + SELF_SIGNING_KEY_SSSS_NAME -> { + crossSigningService.onSecretSSKGossip(secretValue) + true + } + USER_SIGNING_KEY_SSSS_NAME -> { + crossSigningService.onSecretUSKGossip(secretValue) + true + } + KEYBACKUP_SECRET_SSSS_NAME -> { + keysBackupService.onSecretKeyGossip(secretValue) + true + } + else -> false + } + } + /** * Handle an m.room.encryption event. * @@ -732,10 +804,11 @@ internal class DefaultCryptoService @Inject constructor( val params = LoadRoomMembersTask.Params(roomId) try { loadRoomMembersTask.execute(params) + } catch (throwable: Throwable) { + Timber.e(throwable, "## onRoomEncryptionEvent ERROR FAILED TO SETUP CRYPTO ") + } finally { val userIds = getRoomUserIds(roomId) setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds) - } catch (throwable: Throwable) { - Timber.e(throwable) } } } @@ -997,14 +1070,14 @@ internal class DefaultCryptoService @Inject constructor( setRoomBlacklistUnverifiedDevices(roomId, false) } - // TODO Check if this method is still necessary +// TODO Check if this method is still necessary /** * Cancel any earlier room key request * * @param requestBody requestBody */ override fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) { - outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody) + outgoingGossipingRequestManager.cancelRoomKeyRequest(requestBody) } /** @@ -1013,38 +1086,54 @@ internal class DefaultCryptoService @Inject constructor( * @param event the event to decrypt again. */ override fun reRequestRoomKeyForEvent(event: Event) { - val wireContent = event.content - if (wireContent == null) { + val wireContent = event.content.toModel() ?: return Unit.also { Timber.e("## reRequestRoomKeyForEvent Failed to re-request key, null content") - return } val requestBody = RoomKeyRequestBody( - algorithm = wireContent["algorithm"]?.toString(), + algorithm = wireContent.algorithm, roomId = event.roomId, - senderKey = wireContent["sender_key"]?.toString(), - sessionId = wireContent["session_id"]?.toString() + senderKey = wireContent.senderKey, + sessionId = wireContent.sessionId ) - outgoingRoomKeyRequestManager.resendRoomKeyRequest(requestBody) + outgoingGossipingRequestManager.resendRoomKeyRequest(requestBody) + } + + override fun requestRoomKeyForEvent(event: Event) { + val wireContent = event.content.toModel() ?: return Unit.also { + Timber.e("## requestRoomKeyForEvent Failed to request key, null content eventId: ${event.eventId}") + } + + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + if (!isStarted()) { + Timber.v("## requestRoomKeyForEvent() : wait after e2e init") + internalStart(false) + } + roomDecryptorProvider + .getOrCreateRoomDecryptor(event.roomId, wireContent.algorithm) + ?.requestKeysForEvent(event) ?: run { + Timber.v("## requestRoomKeyForEvent() : No room decryptor for roomId:${event.roomId} algorithm:${wireContent.algorithm}") + } + } } /** - * Add a RoomKeysRequestListener listener. + * Add a GossipingRequestListener listener. * * @param listener listener */ - override fun addRoomKeysRequestListener(listener: RoomKeysRequestListener) { - incomingRoomKeyRequestManager.addRoomKeysRequestListener(listener) + override fun addRoomKeysRequestListener(listener: GossipingRequestListener) { + incomingGossipingRequestManager.addRoomKeysRequestListener(listener) } /** - * Add a RoomKeysRequestListener listener. + * Add a GossipingRequestListener listener. * * @param listener listener */ - override fun removeRoomKeysRequestListener(listener: RoomKeysRequestListener) { - incomingRoomKeyRequestManager.removeRoomKeysRequestListener(listener) + override fun removeRoomKeysRequestListener(listener: GossipingRequestListener) { + incomingGossipingRequestManager.removeRoomKeysRequestListener(listener) } /** @@ -1084,11 +1173,23 @@ internal class DefaultCryptoService @Inject constructor( override fun removeSessionListener(listener: NewSessionListener) { roomDecryptorProvider.removeSessionListener(listener) } - /* ========================================================================================== - * DEBUG INFO - * ========================================================================================== */ +/* ========================================================================================== + * DEBUG INFO + * ========================================================================================== */ override fun toString(): String { return "DefaultCryptoService of " + credentials.userId + " (" + credentials.deviceId + ")" } + + override fun getOutgoingRoomKeyRequest(): List { + return cryptoStore.getOutgoingRoomKeyRequests() + } + + override fun getIncomingRoomKeyRequest(): List { + return cryptoStore.getIncomingRoomKeyRequests() + } + + override fun getGossipingEventsTrail(): List { + return cryptoStore.getGossipingEventsTrail() + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt index 04b301ba9e..37a5ee18e1 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt @@ -361,13 +361,13 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM // Handle cross signing keys update val masterKey = response.masterKeys?.get(userId)?.toCryptoModel().also { - Timber.d("## CrossSigning : Got keys for $userId : MSK ${it?.unpaddedBase64PublicKey}") + Timber.v("## CrossSigning : Got keys for $userId : MSK ${it?.unpaddedBase64PublicKey}") } val selfSigningKey = response.selfSigningKeys?.get(userId)?.toCryptoModel()?.also { - Timber.d("## CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}") + Timber.v("## CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}") } val userSigningKey = response.userSigningKeys?.get(userId)?.toCryptoModel()?.also { - Timber.d("## CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}") + Timber.v("## CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}") } cryptoStore.storeUserCrossSigningKeys( userId, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/GossipingRequestState.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/GossipingRequestState.kt new file mode 100644 index 0000000000..f2e45ef109 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/GossipingRequestState.kt @@ -0,0 +1,45 @@ +/* + * 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 + +enum class GossipRequestType { + KEY, + SECRET +} + +enum class GossipingRequestState { + NONE, + PENDING, + REJECTED, + ACCEPTING, + ACCEPTED, + FAILED_TO_ACCEPTED, + // USER_REJECTED, + UNABLE_TO_PROCESS, + CANCELLED_BY_REQUESTER, + RE_REQUESTED +} + +enum class OutgoingGossipingRequestState { + UNSENT, + SENDING, + SENT, + CANCELLING, + CANCELLED, + FAILED_TO_SEND, + FAILED_TO_CANCEL +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/GossipingWorkManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/GossipingWorkManager.kt new file mode 100644 index 0000000000..b7c782c5b4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/GossipingWorkManager.kt @@ -0,0 +1,57 @@ +/* + * 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 + +import androidx.work.BackoffPolicy +import androidx.work.Data +import androidx.work.ExistingWorkPolicy +import androidx.work.ListenableWorker +import androidx.work.OneTimeWorkRequest +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.internal.di.WorkManagerProvider +import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.util.CancelableWork +import im.vector.matrix.android.internal.worker.startChain +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +@SessionScope +internal class GossipingWorkManager @Inject constructor( + private val workManagerProvider: WorkManagerProvider +) { + + inline fun createWork(data: Data, startChain: Boolean): OneTimeWorkRequest { + return workManagerProvider.matrixOneTimeWorkRequestBuilder() + .setConstraints(WorkManagerProvider.workConstraints) + .startChain(startChain) + .setInputData(data) + .setBackoffCriteria(BackoffPolicy.LINEAR, 10_000L, TimeUnit.MILLISECONDS) + .build() + } + + // Prevent sending queue to stay broken after app restart + // The unique queue id will stay the same as long as this object is instanciated + val queueSuffixApp = System.currentTimeMillis() + + fun postWork(workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND): Cancelable { + workManagerProvider.workManager + .beginUniqueWork(this::class.java.name + "_$queueSuffixApp", policy, workRequest) + .enqueue() + + return CancelableWork(workManagerProvider.workManager, workRequest.id) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingGossipingRequestManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingGossipingRequestManager.kt new file mode 100644 index 0000000000..da596960dd --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingGossipingRequestManager.kt @@ -0,0 +1,401 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto + +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.crypto.MXCryptoConfig +import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME +import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME +import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME +import im.vector.matrix.android.api.session.crypto.keyshare.GossipingRequestListener +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding +import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey +import im.vector.matrix.android.internal.crypto.model.rest.GossipingDefaultContent +import im.vector.matrix.android.internal.crypto.model.rest.GossipingToDeviceObject +import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore +import im.vector.matrix.android.internal.di.SessionId +import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.worker.WorkerParamsFactory +import timber.log.Timber +import javax.inject.Inject + +@SessionScope +internal class IncomingGossipingRequestManager @Inject constructor( + @SessionId private val sessionId: String, + private val credentials: Credentials, + private val cryptoStore: IMXCryptoStore, + private val cryptoConfig: MXCryptoConfig, + private val gossipingWorkManager: GossipingWorkManager, + private val roomDecryptorProvider: RoomDecryptorProvider) { + + // list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations + // we received in the current sync. + private val receivedGossipingRequests = ArrayList() + private val receivedRequestCancellations = ArrayList() + + // the listeners + private val gossipingRequestListeners: MutableSet = HashSet() + + init { + receivedGossipingRequests.addAll(cryptoStore.getPendingIncomingGossipingRequests()) + } + + // Recently verified devices (map of deviceId and timestamp) + private val recentlyVerifiedDevices = HashMap() + + /** + * Called when a session has been verified. + * This information can be used by the manager to decide whether or not to fullfil gossiping requests + */ + fun onVerificationCompleteForDevice(deviceId: String) { + // For now we just keep an in memory cache + synchronized(recentlyVerifiedDevices) { + recentlyVerifiedDevices[deviceId] = System.currentTimeMillis() + } + } + + private fun hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId: String): Boolean { + val verifTimestamp: Long? + synchronized(recentlyVerifiedDevices) { + verifTimestamp = recentlyVerifiedDevices[deviceId] + } + if (verifTimestamp == null) return false + + val age = System.currentTimeMillis() - verifTimestamp + + return age < FIVE_MINUTES_IN_MILLIS + } + + /** + * Called when we get an m.room_key_request event + * It must be called on CryptoThread + * + * @param event the announcement event. + */ + fun onGossipingRequestEvent(event: Event) { + Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} from user ${event.senderId}") + val roomKeyShare = event.getClearContent().toModel() + val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it } + when (roomKeyShare?.action) { + GossipingToDeviceObject.ACTION_SHARE_REQUEST -> { + if (event.getClearType() == EventType.REQUEST_SECRET) { + IncomingSecretShareRequest.fromEvent(event)?.let { + if (event.senderId == credentials.userId && it.deviceId == credentials.deviceId) { + // ignore, it was sent by me as * + Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo") + } else { + // save in DB + cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs) + receivedGossipingRequests.add(it) + } + } + } else if (event.getClearType() == EventType.ROOM_KEY_REQUEST) { + IncomingRoomKeyRequest.fromEvent(event)?.let { + if (event.senderId == credentials.userId && it.deviceId == credentials.deviceId) { + // ignore, it was sent by me as * + Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo") + } else { + cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs) + receivedGossipingRequests.add(it) + } + } + } + } + GossipingToDeviceObject.ACTION_SHARE_CANCELLATION -> { + IncomingRequestCancellation.fromEvent(event)?.let { + receivedRequestCancellations.add(it) + } + } + else -> { + Timber.e("## GOSSIP onGossipingRequestEvent() : unsupported action ${roomKeyShare?.action}") + } + } + } + + /** + * Process any m.room_key_request or m.secret.request events which were queued up during the + * current sync. + * It must be called on CryptoThread + */ + fun processReceivedGossipingRequests() { + val roomKeyRequestsToProcess = receivedGossipingRequests.toList() + receivedGossipingRequests.clear() + for (request in roomKeyRequestsToProcess) { + if (request is IncomingRoomKeyRequest) { + processIncomingRoomKeyRequest(request) + } else if (request is IncomingSecretShareRequest) { + processIncomingSecretShareRequest(request) + } + } + + var receivedRequestCancellations: List? = null + + synchronized(this.receivedRequestCancellations) { + if (this.receivedRequestCancellations.isNotEmpty()) { + receivedRequestCancellations = this.receivedRequestCancellations.toList() + this.receivedRequestCancellations.clear() + } + } + + receivedRequestCancellations?.forEach { request -> + Timber.v("## GOSSIP processReceivedGossipingRequests() : m.room_key_request cancellation $request") + // we should probably only notify the app of cancellations we told it + // about, but we don't currently have a record of that, so we just pass + // everything through. + if (request.userId == credentials.userId && request.deviceId == credentials.deviceId) { + // ignore remote echo + return@forEach + } + val matchingIncoming = cryptoStore.getIncomingRoomKeyRequest(request.userId ?: "", request.deviceId ?: "", request.requestId ?: "") + if (matchingIncoming == null) { + // ignore that? + return@forEach + } else { + // If it was accepted from this device, keep the information, do not mark as cancelled + if (matchingIncoming.state != GossipingRequestState.ACCEPTED) { + onRoomKeyRequestCancellation(request) + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.CANCELLED_BY_REQUESTER) + } + } + } + } + + private fun processIncomingRoomKeyRequest(request: IncomingRoomKeyRequest) { + val userId = request.userId + val deviceId = request.deviceId + val body = request.requestBody + val roomId = body!!.roomId + val alg = body.algorithm + + Timber.v("## GOSSIP processIncomingRoomKeyRequest from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}") + if (userId == null || credentials.userId != userId) { + // TODO: determine if we sent this device the keys already: in + Timber.w("## GOSSIP processReceivedGossipingRequests() : Ignoring room key request from other user for now") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + return + } + // TODO: should we queue up requests we don't yet have keys for, in case they turn up later? + // if we don't have a decryptor for this room/alg, we don't have + // the keys for the requested events, and can drop the requests. + val decryptor = roomDecryptorProvider.getRoomDecryptor(roomId, alg) + if (null == decryptor) { + Timber.w("## GOSSIP processReceivedGossipingRequests() : room key request for unknown $alg in room $roomId") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + return + } + if (!decryptor.hasKeysForKeyRequest(request)) { + Timber.w("## GOSSIP processReceivedGossipingRequests() : room key request for unknown session ${body.sessionId!!}") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + return + } + + if (credentials.deviceId == deviceId && credentials.userId == userId) { + Timber.v("## GOSSIP processReceivedGossipingRequests() : oneself device - ignored") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + return + } + request.share = Runnable { + decryptor.shareKeysWithDevice(request) + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED) + } + request.ignore = Runnable { + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + } + // if the device is verified already, share the keys + val device = cryptoStore.getUserDevice(userId, deviceId!!) + if (device != null) { + if (device.isVerified) { + Timber.v("## GOSSIP processReceivedGossipingRequests() : device is already verified: sharing keys") + request.share?.run() + return + } + + if (device.isBlocked) { + Timber.v("## GOSSIP processReceivedGossipingRequests() : device is blocked -> ignored") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + return + } + } + + // As per config we automatically discard untrusted devices request + if (cryptoConfig.discardRoomKeyRequestsFromUntrustedDevices) { + Timber.v("## processReceivedGossipingRequests() : discardRoomKeyRequestsFromUntrustedDevices") + // At this point the device is unknown, we don't want to bother user with that + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + return + } + + // Pass to application layer to decide what to do + onRoomKeyRequest(request) + } + + private fun processIncomingSecretShareRequest(request: IncomingSecretShareRequest) { + val secretName = request.secretName ?: return Unit.also { + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + Timber.v("## GOSSIP processIncomingSecretShareRequest() : Missing secret name") + } + + val userId = request.userId + if (userId == null || credentials.userId != userId) { + Timber.e("## GOSSIP processIncomingSecretShareRequest() : Ignoring secret share request from other users") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + return + } + + val deviceId = request.deviceId + ?: return Unit.also { + Timber.e("## GOSSIP processIncomingSecretShareRequest() : Malformed request, no ") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + } + + val device = cryptoStore.getUserDevice(userId, deviceId) + ?: return Unit.also { + Timber.e("## GOSSIP processIncomingSecretShareRequest() : Received secret share request from unknown device ${request.deviceId}") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + } + + if (!device.isVerified || device.isBlocked) { + Timber.v("## GOSSIP processIncomingSecretShareRequest() : Ignoring secret share request from untrusted/blocked session $device") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + return + } + + val isDeviceLocallyVerified = cryptoStore.getUserDevice(userId, deviceId)?.trustLevel?.isLocallyVerified() + + // Should SDK always Silently reject any request for the master key? + when (secretName) { + SELF_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.selfSigned + USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user + KEYBACKUP_SECRET_SSSS_NAME -> cryptoStore.getKeyBackupRecoveryKeyInfo()?.recoveryKey + ?.let { + extractCurveKeyFromRecoveryKey(it)?.toBase64NoPadding() + } + else -> null + }?.let { secretValue -> + Timber.i("## GOSSIP processIncomingSecretShareRequest() : Sharing secret $secretName with $device locally trusted") + if (isDeviceLocallyVerified == true && hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId)) { + val params = SendGossipWorker.Params( + sessionId = sessionId, + secretValue = secretValue, + request = request + ) + + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTING) + val workRequest = gossipingWorkManager.createWork(WorkerParamsFactory.toData(params), true) + gossipingWorkManager.postWork(workRequest) + } else { + Timber.v("## GOSSIP processIncomingSecretShareRequest() : Can't share secret $secretName with $device, verification too old") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + } + return + } + + Timber.v("## GOSSIP processIncomingSecretShareRequest() : $secretName unknown at SDK level, asking to app layer") + + request.ignore = Runnable { + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + } + + request.share = { secretValue -> + + val params = SendGossipWorker.Params( + sessionId = userId, + secretValue = secretValue, + request = request + ) + + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTING) + val workRequest = gossipingWorkManager.createWork(WorkerParamsFactory.toData(params), true) + gossipingWorkManager.postWork(workRequest) + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED) + } + + onShareRequest(request) + } + + /** + * Dispatch onRoomKeyRequest + * + * @param request the request + */ + private fun onRoomKeyRequest(request: IncomingRoomKeyRequest) { + synchronized(gossipingRequestListeners) { + for (listener in gossipingRequestListeners) { + try { + listener.onRoomKeyRequest(request) + } catch (e: Exception) { + Timber.e(e, "## onRoomKeyRequest() failed") + } + } + } + } + + /** + * Ask for a value to the listeners, and take the first one + */ + private fun onShareRequest(request: IncomingSecretShareRequest) { + synchronized(gossipingRequestListeners) { + for (listener in gossipingRequestListeners) { + try { + if (listener.onSecretShareRequest(request)) { + return + } + } catch (e: Exception) { + Timber.e(e, "## GOSSIP onRoomKeyRequest() failed") + } + } + } + // Not handled, ignore + request.ignore?.run() + } + + /** + * A room key request cancellation has been received. + * + * @param request the cancellation request + */ + private fun onRoomKeyRequestCancellation(request: IncomingRequestCancellation) { + synchronized(gossipingRequestListeners) { + for (listener in gossipingRequestListeners) { + try { + listener.onRoomKeyRequestCancellation(request) + } catch (e: Exception) { + Timber.e(e, "## GOSSIP onRoomKeyRequestCancellation() failed") + } + } + } + } + + fun addRoomKeysRequestListener(listener: GossipingRequestListener) { + synchronized(gossipingRequestListeners) { + gossipingRequestListeners.add(listener) + } + } + + fun removeRoomKeysRequestListener(listener: GossipingRequestListener) { + synchronized(gossipingRequestListeners) { + gossipingRequestListeners.remove(listener) + } + } + + companion object { + private const val FIVE_MINUTES_IN_MILLIS = 5 * 60 * 1000 + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestCancellation.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRequestCancellation.kt similarity index 67% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestCancellation.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRequestCancellation.kt index 6779936f3a..98e1e95423 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestCancellation.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRequestCancellation.kt @@ -18,12 +18,12 @@ package im.vector.matrix.android.internal.crypto import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyShareCancellation +import im.vector.matrix.android.internal.crypto.model.rest.ShareRequestCancellation /** - * IncomingRoomKeyRequestCancellation describes the incoming room key cancellation. + * IncomingRequestCancellation describes the incoming room key cancellation. */ -data class IncomingRoomKeyRequestCancellation( +data class IncomingRequestCancellation( /** * The user id */ @@ -37,22 +37,24 @@ data class IncomingRoomKeyRequestCancellation( /** * The request id */ - override val requestId: String? = null -) : IncomingRoomKeyRequestCommon { + override val requestId: String? = null, + override val localCreationTimestamp: Long? +) : IncomingShareRequestCommon { companion object { /** * Factory * * @param event the event */ - fun fromEvent(event: Event): IncomingRoomKeyRequestCancellation? { + fun fromEvent(event: Event): IncomingRequestCancellation? { return event.getClearContent() - .toModel() + .toModel() ?.let { - IncomingRoomKeyRequestCancellation( + IncomingRequestCancellation( userId = event.senderId, deviceId = it.requestingDeviceId, - requestId = it.requestId + requestId = it.requestId, + localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis() ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequest.kt index 39b4678a27..13f3d38677 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequest.kt @@ -46,6 +46,8 @@ data class IncomingRoomKeyRequest( */ val requestBody: RoomKeyRequestBody? = null, + val state: GossipingRequestState = GossipingRequestState.NONE, + /** * The runnable to call to accept to share the keys */ @@ -56,8 +58,9 @@ data class IncomingRoomKeyRequest( * The runnable to call to ignore the key share request. */ @Transient - var ignore: Runnable? = null -) : IncomingRoomKeyRequestCommon { + var ignore: Runnable? = null, + override val localCreationTimestamp: Long? +) : IncomingShareRequestCommon { companion object { /** * Factory @@ -72,7 +75,8 @@ data class IncomingRoomKeyRequest( userId = event.senderId, deviceId = it.requestingDeviceId, requestId = it.requestId, - requestBody = it.body ?: RoomKeyRequestBody() + requestBody = it.body ?: RoomKeyRequestBody(), + localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis() ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt deleted file mode 100644 index 92a117d64b..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.matrix.android.internal.crypto - -import im.vector.matrix.android.api.auth.data.Credentials -import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener -import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyShare -import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore -import im.vector.matrix.android.internal.session.SessionScope -import timber.log.Timber -import javax.inject.Inject - -@SessionScope -internal class IncomingRoomKeyRequestManager @Inject constructor( - private val credentials: Credentials, - private val cryptoStore: IMXCryptoStore, - private val roomDecryptorProvider: RoomDecryptorProvider) { - - // list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations - // we received in the current sync. - private val receivedRoomKeyRequests = ArrayList() - private val receivedRoomKeyRequestCancellations = ArrayList() - - // the listeners - private val roomKeysRequestListeners: MutableSet = HashSet() - - init { - receivedRoomKeyRequests.addAll(cryptoStore.getPendingIncomingRoomKeyRequests()) - } - - /** - * Called when we get an m.room_key_request event - * It must be called on CryptoThread - * - * @param event the announcement event. - */ - fun onRoomKeyRequestEvent(event: Event) { - when (val roomKeyShareAction = event.getClearContent()?.get("action") as? String) { - RoomKeyShare.ACTION_SHARE_REQUEST -> IncomingRoomKeyRequest.fromEvent(event)?.let { receivedRoomKeyRequests.add(it) } - RoomKeyShare.ACTION_SHARE_CANCELLATION -> IncomingRoomKeyRequestCancellation.fromEvent(event)?.let { receivedRoomKeyRequestCancellations.add(it) } - else -> Timber.e("## onRoomKeyRequestEvent() : unsupported action $roomKeyShareAction") - } - } - - /** - * Process any m.room_key_request events which were queued up during the - * current sync. - * It must be called on CryptoThread - */ - fun processReceivedRoomKeyRequests() { - val roomKeyRequestsToProcess = receivedRoomKeyRequests.toList() - receivedRoomKeyRequests.clear() - for (request in roomKeyRequestsToProcess) { - val userId = request.userId - val deviceId = request.deviceId - val body = request.requestBody - val roomId = body!!.roomId - val alg = body.algorithm - - Timber.v("m.room_key_request from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}") - if (userId == null || credentials.userId != userId) { - // TODO: determine if we sent this device the keys already: in - Timber.w("## processReceivedRoomKeyRequests() : Ignoring room key request from other user for now") - return - } - // TODO: should we queue up requests we don't yet have keys for, in case they turn up later? - // if we don't have a decryptor for this room/alg, we don't have - // the keys for the requested events, and can drop the requests. - val decryptor = roomDecryptorProvider.getRoomDecryptor(roomId, alg) - if (null == decryptor) { - Timber.w("## processReceivedRoomKeyRequests() : room key request for unknown $alg in room $roomId") - continue - } - if (!decryptor.hasKeysForKeyRequest(request)) { - Timber.w("## processReceivedRoomKeyRequests() : room key request for unknown session ${body.sessionId!!}") - cryptoStore.deleteIncomingRoomKeyRequest(request) - continue - } - - if (credentials.deviceId == deviceId && credentials.userId == userId) { - Timber.v("## processReceivedRoomKeyRequests() : oneself device - ignored") - cryptoStore.deleteIncomingRoomKeyRequest(request) - continue - } - request.share = Runnable { - decryptor.shareKeysWithDevice(request) - cryptoStore.deleteIncomingRoomKeyRequest(request) - } - request.ignore = Runnable { - cryptoStore.deleteIncomingRoomKeyRequest(request) - } - // if the device is verified already, share the keys - val device = cryptoStore.getUserDevice(userId, deviceId!!) - if (device != null) { - if (device.isVerified) { - Timber.v("## processReceivedRoomKeyRequests() : device is already verified: sharing keys") - cryptoStore.deleteIncomingRoomKeyRequest(request) - request.share?.run() - continue - } - - if (device.isBlocked) { - Timber.v("## processReceivedRoomKeyRequests() : device is blocked -> ignored") - cryptoStore.deleteIncomingRoomKeyRequest(request) - continue - } - } - - // If cross signing is available on account we automatically discard untrust devices request - if (cryptoStore.getMyCrossSigningInfo() != null) { - // At this point the device is unknown, we don't want to bother user with that - cryptoStore.deleteIncomingRoomKeyRequest(request) - continue - } - - cryptoStore.storeIncomingRoomKeyRequest(request) - onRoomKeyRequest(request) - } - - var receivedRoomKeyRequestCancellations: List? = null - - synchronized(this.receivedRoomKeyRequestCancellations) { - if (this.receivedRoomKeyRequestCancellations.isNotEmpty()) { - receivedRoomKeyRequestCancellations = this.receivedRoomKeyRequestCancellations.toList() - this.receivedRoomKeyRequestCancellations.clear() - } - } - - if (null != receivedRoomKeyRequestCancellations) { - for (request in receivedRoomKeyRequestCancellations!!) { - Timber.v("## ## processReceivedRoomKeyRequests() : m.room_key_request cancellation for " + request.userId - + ":" + request.deviceId + " id " + request.requestId) - - // we should probably only notify the app of cancellations we told it - // about, but we don't currently have a record of that, so we just pass - // everything through. - onRoomKeyRequestCancellation(request) - cryptoStore.deleteIncomingRoomKeyRequest(request) - } - } - } - - /** - * Dispatch onRoomKeyRequest - * - * @param request the request - */ - private fun onRoomKeyRequest(request: IncomingRoomKeyRequest) { - synchronized(roomKeysRequestListeners) { - for (listener in roomKeysRequestListeners) { - try { - listener.onRoomKeyRequest(request) - } catch (e: Exception) { - Timber.e(e, "## onRoomKeyRequest() failed") - } - } - } - } - - /** - * A room key request cancellation has been received. - * - * @param request the cancellation request - */ - private fun onRoomKeyRequestCancellation(request: IncomingRoomKeyRequestCancellation) { - synchronized(roomKeysRequestListeners) { - for (listener in roomKeysRequestListeners) { - try { - listener.onRoomKeyRequestCancellation(request) - } catch (e: Exception) { - Timber.e(e, "## onRoomKeyRequestCancellation() failed") - } - } - } - } - - fun addRoomKeysRequestListener(listener: RoomKeysRequestListener) { - synchronized(roomKeysRequestListeners) { - roomKeysRequestListeners.add(listener) - } - } - - fun removeRoomKeysRequestListener(listener: RoomKeysRequestListener) { - synchronized(roomKeysRequestListeners) { - roomKeysRequestListeners.remove(listener) - } - } -} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingSecretShareRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingSecretShareRequest.kt new file mode 100755 index 0000000000..2fcd3e22d5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingSecretShareRequest.kt @@ -0,0 +1,82 @@ +/* + * 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 + +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.internal.crypto.model.rest.SecretShareRequest + +/** + * IncomingRoomKeyRequest class defines the incoming room keys request. + */ +data class IncomingSecretShareRequest( + /** + * The user id + */ + override val userId: String? = null, + + /** + * The device id + */ + override val deviceId: String? = null, + + /** + * The request id + */ + override val requestId: String? = null, + + /** + * The request body + */ + val secretName: String? = null, + + /** + * The runnable to call to accept to share the keys + */ + @Transient + var share: ((String) -> Unit)? = null, + + /** + * The runnable to call to ignore the key share request. + */ + @Transient + var ignore: Runnable? = null, + + override val localCreationTimestamp: Long? + +) : IncomingShareRequestCommon { + companion object { + /** + * Factory + * + * @param event the event + */ + fun fromEvent(event: Event): IncomingSecretShareRequest? { + return event.getClearContent() + .toModel() + ?.let { + IncomingSecretShareRequest( + userId = event.senderId, + deviceId = it.requestingDeviceId, + requestId = it.requestId, + secretName = it.secretName, + localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis() + ) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestCommon.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingShareRequestCommon.kt similarity index 91% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestCommon.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingShareRequestCommon.kt index a7b1c6b117..f39a0d80d1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestCommon.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingShareRequestCommon.kt @@ -16,7 +16,7 @@ package im.vector.matrix.android.internal.crypto -interface IncomingRoomKeyRequestCommon { +interface IncomingShareRequestCommon { /** * The user id */ @@ -31,4 +31,6 @@ interface IncomingRoomKeyRequestCommon { * The request id */ val requestId: String? + + val localCreationTimestamp: Long? } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt index 47ec85ec8c..86f0768a7d 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt @@ -59,9 +59,6 @@ internal class MXOlmDevice @Inject constructor( var deviceEd25519Key: String? = null private set - // The OLM lib account instance. - private var olmAccount: OlmAccount? = null - // The OLM lib utility instance. private var olmUtility: OlmUtility? = null @@ -86,19 +83,10 @@ internal class MXOlmDevice @Inject constructor( init { // Retrieve the account from the store - olmAccount = store.getAccount() - - if (null == olmAccount) { - Timber.v("MXOlmDevice : create a new olm account") - // Else, create it - try { - olmAccount = OlmAccount() - store.storeAccount(olmAccount!!) - } catch (e: Exception) { - Timber.e(e, "MXOlmDevice : cannot initialize olmAccount") - } - } else { - Timber.v("MXOlmDevice : use an existing account") + try { + store.getOrCreateOlmAccount() + } catch (e: Exception) { + Timber.e(e, "MXOlmDevice : cannot initialize olmAccount") } try { @@ -109,13 +97,13 @@ internal class MXOlmDevice @Inject constructor( } try { - deviceCurve25519Key = olmAccount!!.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY] + deviceCurve25519Key = store.getOlmAccount().identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY] } catch (e: Exception) { Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_IDENTITY_KEY} with error") } try { - deviceEd25519Key = olmAccount!!.identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY] + deviceEd25519Key = store.getOlmAccount().identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY] } catch (e: Exception) { Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_FINGER_PRINT_KEY} with error") } @@ -126,7 +114,7 @@ internal class MXOlmDevice @Inject constructor( */ fun getOneTimeKeys(): Map>? { try { - return olmAccount!!.oneTimeKeys() + return store.getOlmAccount().oneTimeKeys() } catch (e: Exception) { Timber.e(e, "## getOneTimeKeys() : failed") } @@ -138,14 +126,13 @@ internal class MXOlmDevice @Inject constructor( * @return The maximum number of one-time keys the olm account can store. */ fun getMaxNumberOfOneTimeKeys(): Long { - return olmAccount?.maxOneTimeKeys() ?: -1 + return store.getOlmAccount().maxOneTimeKeys() } /** * Release the instance */ fun release() { - olmAccount?.releaseAccount() olmUtility?.releaseUtility() } @@ -157,7 +144,7 @@ internal class MXOlmDevice @Inject constructor( */ fun signMessage(message: String): String? { try { - return olmAccount!!.signMessage(message) + return store.getOlmAccount().signMessage(message) } catch (e: Exception) { Timber.e(e, "## signMessage() : failed") } @@ -170,8 +157,8 @@ internal class MXOlmDevice @Inject constructor( */ fun markKeysAsPublished() { try { - olmAccount!!.markOneTimeKeysAsPublished() - store.storeAccount(olmAccount!!) + store.getOlmAccount().markOneTimeKeysAsPublished() + store.saveOlmAccount() } catch (e: Exception) { Timber.e(e, "## markKeysAsPublished() : failed") } @@ -184,8 +171,8 @@ internal class MXOlmDevice @Inject constructor( */ fun generateOneTimeKeys(numKeys: Int) { try { - olmAccount!!.generateOneTimeKeys(numKeys) - store.storeAccount(olmAccount!!) + store.getOlmAccount().generateOneTimeKeys(numKeys) + store.saveOlmAccount() } catch (e: Exception) { Timber.e(e, "## generateOneTimeKeys() : failed") } @@ -205,7 +192,7 @@ internal class MXOlmDevice @Inject constructor( try { olmSession = OlmSession() - olmSession.initOutboundSession(olmAccount!!, theirIdentityKey, theirOneTimeKey) + olmSession.initOutboundSession(store.getOlmAccount(), theirIdentityKey, theirOneTimeKey) val olmSessionWrapper = OlmSessionWrapper(olmSession, 0) @@ -245,7 +232,7 @@ internal class MXOlmDevice @Inject constructor( try { try { olmSession = OlmSession() - olmSession.initInboundSessionFrom(olmAccount!!, theirDeviceIdentityKey, ciphertext) + olmSession.initInboundSessionFrom(store.getOlmAccount(), theirDeviceIdentityKey, ciphertext) } catch (e: Exception) { Timber.e(e, "## createInboundSession() : the session creation failed") return null @@ -254,8 +241,8 @@ internal class MXOlmDevice @Inject constructor( Timber.v("## createInboundSession() : sessionId: ${olmSession.sessionIdentifier()}") try { - olmAccount!!.removeOneTimeKeys(olmSession) - store.storeAccount(olmAccount!!) + store.getOlmAccount().removeOneTimeKeys(olmSession) + store.saveOlmAccount() } catch (e: Exception) { Timber.e(e, "## createInboundSession() : removeOneTimeKeys failed") } @@ -654,7 +641,7 @@ internal class MXOlmDevice @Inject constructor( throw MXCryptoError.OlmError(e) } - if (null != timeline) { + if (timeline?.isNotBlank() == true) { val timelineSet = inboundGroupSessionMessageIndexes.getOrPut(timeline) { mutableSetOf() } val messageIndexKey = senderKey + "|" + sessionId + "|" + decryptResult.mIndex @@ -770,7 +757,7 @@ internal class MXOlmDevice @Inject constructor( return session } } else { - Timber.w("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId") + Timber.v("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId") throw MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingGossipingRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingGossipingRequest.kt new file mode 100644 index 0000000000..2fb0c7094b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingGossipingRequest.kt @@ -0,0 +1,25 @@ +/* + * 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 + +interface OutgoingGossipingRequest { + var recipients: Map> + var requestId: String + var state: OutgoingGossipingRequestState + // transaction id for the cancellation, if any + // var cancellationTxnId: String? +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingGossipingRequestManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingGossipingRequestManager.kt new file mode 100755 index 0000000000..c06f10b106 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingGossipingRequestManager.kt @@ -0,0 +1,162 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto + +import im.vector.matrix.android.api.session.events.model.LocalEcho +import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody +import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore +import im.vector.matrix.android.internal.di.SessionId +import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers +import im.vector.matrix.android.internal.worker.WorkerParamsFactory +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@SessionScope +internal class OutgoingGossipingRequestManager @Inject constructor( + @SessionId private val sessionId: String, + private val cryptoStore: IMXCryptoStore, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val cryptoCoroutineScope: CoroutineScope, + private val gossipingWorkManager: GossipingWorkManager) { + + /** + * Send off a room key request, if we haven't already done so. + * + * + * The `requestBody` is compared (with a deep-equality check) against + * previous queued or sent requests and if it matches, no change is made. + * Otherwise, a request is added to the pending list, and a job is started + * in the background to send it. + * + * @param requestBody requestBody + * @param recipients recipients + */ + fun sendRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map>) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients)?.let { + // Don't resend if it's already done, you need to cancel first (reRequest) + if (it.state == OutgoingGossipingRequestState.SENDING || it.state == OutgoingGossipingRequestState.SENT) { + Timber.v("## GOSSIP sendOutgoingRoomKeyRequest() : we already request for that session: $it") + return@launch + } + + sendOutgoingGossipingRequest(it) + } + } + } + + fun sendSecretShareRequest(secretName: String, recipients: Map>) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + // A bit dirty, but for better stability give other party some time to mark + // devices trusted :/ + delay(1500) + cryptoStore.getOrAddOutgoingSecretShareRequest(secretName, recipients)?.let { + // TODO check if there is already one that is being sent? + if (it.state == OutgoingGossipingRequestState.SENDING || it.state == OutgoingGossipingRequestState.SENT) { + Timber.v("## GOSSIP sendSecretShareRequest() : we already request for that session: $it") + return@launch + } + + sendOutgoingGossipingRequest(it) + } + } + } + + /** + * Cancel room key requests, if any match the given details + * + * @param requestBody requestBody + */ + fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + cancelRoomKeyRequest(requestBody, false) + } + } + + /** + * Cancel room key requests, if any match the given details, and resend + * + * @param requestBody requestBody + */ + fun resendRoomKeyRequest(requestBody: RoomKeyRequestBody) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + cancelRoomKeyRequest(requestBody, true) + } + } + + /** + * Cancel room key requests, if any match the given details, and resend + * + * @param requestBody requestBody + * @param andResend true to resend the key request + */ + private fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody, andResend: Boolean) { + val req = cryptoStore.getOutgoingRoomKeyRequest(requestBody) + ?: // no request was made for this key + return Unit.also { + Timber.v("## GOSSIP cancelRoomKeyRequest() Unknown request") + } + + sendOutgoingRoomKeyRequestCancellation(req, andResend) + } + + /** + * Send the outgoing key request. + * + * @param request the request + */ + private fun sendOutgoingGossipingRequest(request: OutgoingGossipingRequest) { + Timber.v("## GOSSIP sendOutgoingRoomKeyRequest() : Requesting keys $request") + + val params = SendGossipRequestWorker.Params( + sessionId = sessionId, + keyShareRequest = request as? OutgoingRoomKeyRequest, + secretShareRequest = request as? OutgoingSecretRequest + ) + cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.SENDING) + val workRequest = gossipingWorkManager.createWork(WorkerParamsFactory.toData(params), true) + gossipingWorkManager.postWork(workRequest) + } + + /** + * Given a OutgoingRoomKeyRequest, cancel it and delete the request record + * + * @param request the request + */ + private fun sendOutgoingRoomKeyRequestCancellation(request: OutgoingRoomKeyRequest, resend: Boolean = false) { + Timber.v("$request") + val params = CancelGossipRequestWorker.Params.fromRequest(sessionId, request) + cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.CANCELLING) + + val workRequest = gossipingWorkManager.createWork(WorkerParamsFactory.toData(params), true) + gossipingWorkManager.postWork(workRequest) + + if (resend) { + val reSendParams = SendGossipRequestWorker.Params( + sessionId = sessionId, + keyShareRequest = request.copy(requestId = LocalEcho.createLocalEchoId()) + ) + val reSendWorkRequest = gossipingWorkManager.createWork(WorkerParamsFactory.toData(reSendParams), true) + gossipingWorkManager.postWork(reSendWorkRequest) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequest.kt index a66b0242ec..b01c9d9b3f 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequest.kt @@ -17,22 +17,26 @@ package im.vector.matrix.android.internal.crypto +import com.squareup.moshi.JsonClass import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody /** * Represents an outgoing room key request */ -class OutgoingRoomKeyRequest( +@JsonClass(generateAdapter = true) +data class OutgoingRoomKeyRequest( // RequestBody - var requestBody: RoomKeyRequestBody?, // list of recipients for the request - var recipients: List>, // Unique id for this request. Used for both + var requestBody: RoomKeyRequestBody?, + // list of recipients for the request + override var recipients: Map>, + // Unique id for this request. Used for both // an id within the request for later pairing with a cancellation, and for // the transaction id when sending the to_device messages to our local - var requestId: String, // current state of this request - var state: RequestState) { - - // transaction id for the cancellation, if any - var cancellationTxnId: String? = null + override var requestId: String, // current state of this request + override var state: OutgoingGossipingRequestState + // transaction id for the cancellation, if any + // override var cancellationTxnId: String? = null +) : OutgoingGossipingRequest { /** * Used only for log. @@ -53,66 +57,4 @@ class OutgoingRoomKeyRequest( get() = if (null != requestBody) { requestBody!!.sessionId } else null - - /** - * possible states for a room key request - * - * - * The state machine looks like: - *
-     *
-     *      |
-     *      V
-     *    UNSENT  -----------------------------+
-     *      |                                  |
-     *      | (send successful)                | (cancellation requested)
-     *      V                                  |
-     *     SENT                                |
-     *      |--------------------------------  |  --------------+
-     *      |                                  |                |
-     *      |                                  |                | (cancellation requested with intent
-     *      |                                  |                | to resend a new request)
-     *      | (cancellation requested)         |                |
-     *      V                                  |                V
-     *  CANCELLATION_PENDING                   | CANCELLATION_PENDING_AND_WILL_RESEND
-     *      |                                  |                |
-     *      | (cancellation sent)              |                | (cancellation sent. Create new request
-     *      |                                  |                |  in the UNSENT state)
-     *      V                                  |                |
-     *  (deleted)  <---------------------------+----------------+
-     *  
- */ - - enum class RequestState { - /** - * request not yet sent - */ - UNSENT, - /** - * request sent, awaiting reply - */ - SENT, - /** - * reply received, cancellation not yet sent - */ - CANCELLATION_PENDING, - /** - * Cancellation not yet sent, once sent, a new request will be done - */ - CANCELLATION_PENDING_AND_WILL_RESEND, - /** - * sending failed - */ - FAILED; - - companion object { - fun from(state: Int) = when (state) { - 0 -> UNSENT - 1 -> SENT - 2 -> CANCELLATION_PENDING - 3 -> CANCELLATION_PENDING_AND_WILL_RESEND - else /*4*/ -> FAILED - } - } - } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt deleted file mode 100755 index b59c93ba83..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt +++ /dev/null @@ -1,320 +0,0 @@ -/* - * Copyright 2016 OpenMarket Ltd - * Copyright 2018 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.matrix.android.internal.crypto - -import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap -import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody -import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyShareCancellation -import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyShareRequest -import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore -import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask -import im.vector.matrix.android.internal.session.SessionScope -import im.vector.matrix.android.internal.task.TaskExecutor -import im.vector.matrix.android.internal.task.TaskThread -import im.vector.matrix.android.internal.task.configureWith -import im.vector.matrix.android.internal.util.createBackgroundHandler -import timber.log.Timber -import java.util.concurrent.atomic.AtomicBoolean -import javax.inject.Inject - -@SessionScope -internal class OutgoingRoomKeyRequestManager @Inject constructor( - private val cryptoStore: IMXCryptoStore, - private val sendToDeviceTask: SendToDeviceTask, - private val taskExecutor: TaskExecutor) { - - // running - private var isClientRunning: Boolean = false - - // transaction counter - private var txnCtr: Int = 0 - - // sanity check to ensure that we don't end up with two concurrent runs - // of sendOutgoingRoomKeyRequestsTimer - private val sendOutgoingRoomKeyRequestsRunning = AtomicBoolean(false) - - /** - * Called when the client is started. Sets background processes running. - */ - fun start() { - isClientRunning = true - startTimer() - } - - /** - * Called when the client is stopped. Stops any running background processes. - */ - fun stop() { - isClientRunning = false - stopTimer() - } - - /** - * Make up a new transaction id - * - * @return {string} a new, unique, transaction id - */ - private fun makeTxnId(): String { - return "m" + System.currentTimeMillis() + "." + txnCtr++ - } - - /** - * Send off a room key request, if we haven't already done so. - * - * - * The `requestBody` is compared (with a deep-equality check) against - * previous queued or sent requests and if it matches, no change is made. - * Otherwise, a request is added to the pending list, and a job is started - * in the background to send it. - * - * @param requestBody requestBody - * @param recipients recipients - */ - fun sendRoomKeyRequest(requestBody: RoomKeyRequestBody?, recipients: List>) { - val req = cryptoStore.getOrAddOutgoingRoomKeyRequest( - OutgoingRoomKeyRequest(requestBody, recipients, makeTxnId(), OutgoingRoomKeyRequest.RequestState.UNSENT)) - - if (req?.state == OutgoingRoomKeyRequest.RequestState.UNSENT) { - startTimer() - } - } - - /** - * Cancel room key requests, if any match the given details - * - * @param requestBody requestBody - */ - fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) { - BACKGROUND_HANDLER.post { - cancelRoomKeyRequest(requestBody, false) - } - } - - /** - * Cancel room key requests, if any match the given details, and resend - * - * @param requestBody requestBody - */ - fun resendRoomKeyRequest(requestBody: RoomKeyRequestBody) { - BACKGROUND_HANDLER.post { - cancelRoomKeyRequest(requestBody, true) - } - } - - /** - * Cancel room key requests, if any match the given details, and resend - * - * @param requestBody requestBody - * @param andResend true to resend the key request - */ - private fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody, andResend: Boolean) { - val req = cryptoStore.getOutgoingRoomKeyRequest(requestBody) - ?: // no request was made for this key - return - - Timber.v("cancelRoomKeyRequest: requestId: " + req.requestId + " state: " + req.state + " andResend: " + andResend) - - when (req.state) { - OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING, - OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> { - // nothing to do here - } - OutgoingRoomKeyRequest.RequestState.UNSENT, - OutgoingRoomKeyRequest.RequestState.FAILED -> { - Timber.v("## cancelRoomKeyRequest() : deleting unnecessary room key request for $requestBody") - cryptoStore.deleteOutgoingRoomKeyRequest(req.requestId) - } - OutgoingRoomKeyRequest.RequestState.SENT -> { - if (andResend) { - req.state = OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND - } else { - req.state = OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING - } - req.cancellationTxnId = makeTxnId() - cryptoStore.updateOutgoingRoomKeyRequest(req) - sendOutgoingRoomKeyRequestCancellation(req) - } - } - } - - /** - * Start the background timer to send queued requests, if the timer isn't already running. - */ - private fun startTimer() { - if (sendOutgoingRoomKeyRequestsRunning.get()) { - return - } - BACKGROUND_HANDLER.postDelayed(Runnable { - if (sendOutgoingRoomKeyRequestsRunning.get()) { - Timber.v("## startTimer() : RoomKeyRequestSend already in progress!") - return@Runnable - } - - sendOutgoingRoomKeyRequestsRunning.set(true) - sendOutgoingRoomKeyRequests() - }, SEND_KEY_REQUESTS_DELAY_MS.toLong()) - } - - private fun stopTimer() { - BACKGROUND_HANDLER.removeCallbacksAndMessages(null) - } - - // look for and send any queued requests. Runs itself recursively until - // there are no more requests, or there is an error (in which case, the - // timer will be restarted before the promise resolves). - private fun sendOutgoingRoomKeyRequests() { - if (!isClientRunning) { - sendOutgoingRoomKeyRequestsRunning.set(false) - return - } - - Timber.v("## sendOutgoingRoomKeyRequests() : Looking for queued outgoing room key requests") - val outgoingRoomKeyRequest = cryptoStore.getOutgoingRoomKeyRequestByState( - setOf(OutgoingRoomKeyRequest.RequestState.UNSENT, - OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING, - OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND)) - - if (null == outgoingRoomKeyRequest) { - Timber.v("## sendOutgoingRoomKeyRequests() : No more outgoing room key requests") - sendOutgoingRoomKeyRequestsRunning.set(false) - return - } - - if (OutgoingRoomKeyRequest.RequestState.UNSENT === outgoingRoomKeyRequest.state) { - sendOutgoingRoomKeyRequest(outgoingRoomKeyRequest) - } else { - sendOutgoingRoomKeyRequestCancellation(outgoingRoomKeyRequest) - } - } - - /** - * Send the outgoing key request. - * - * @param request the request - */ - private fun sendOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest) { - Timber.v("## sendOutgoingRoomKeyRequest() : Requesting keys " + request.requestBody - + " from " + request.recipients + " id " + request.requestId) - - val requestMessage = RoomKeyShareRequest( - requestingDeviceId = cryptoStore.getDeviceId(), - requestId = request.requestId, - body = request.requestBody - ) - - sendMessageToDevices(requestMessage, request.recipients, request.requestId, object : MatrixCallback { - private fun onDone(state: OutgoingRoomKeyRequest.RequestState) { - if (request.state !== OutgoingRoomKeyRequest.RequestState.UNSENT) { - Timber.v("## sendOutgoingRoomKeyRequest() : Cannot update room key request from UNSENT as it was already updated to ${request.state}") - } else { - request.state = state - cryptoStore.updateOutgoingRoomKeyRequest(request) - } - - sendOutgoingRoomKeyRequestsRunning.set(false) - startTimer() - } - - override fun onSuccess(data: Unit) { - Timber.v("## sendOutgoingRoomKeyRequest succeed") - onDone(OutgoingRoomKeyRequest.RequestState.SENT) - } - - override fun onFailure(failure: Throwable) { - Timber.e("## sendOutgoingRoomKeyRequest failed") - onDone(OutgoingRoomKeyRequest.RequestState.FAILED) - } - }) - } - - /** - * Given a OutgoingRoomKeyRequest, cancel it and delete the request record - * - * @param request the request - */ - private fun sendOutgoingRoomKeyRequestCancellation(request: OutgoingRoomKeyRequest) { - Timber.v("## sendOutgoingRoomKeyRequestCancellation() : Sending cancellation for key request for " + request.requestBody - + " to " + request.recipients - + " cancellation id " + request.cancellationTxnId) - - val roomKeyShareCancellation = RoomKeyShareCancellation( - requestingDeviceId = cryptoStore.getDeviceId(), - requestId = request.cancellationTxnId - ) - - sendMessageToDevices(roomKeyShareCancellation, request.recipients, request.cancellationTxnId, object : MatrixCallback { - private fun onDone() { - cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId) - sendOutgoingRoomKeyRequestsRunning.set(false) - startTimer() - } - - override fun onSuccess(data: Unit) { - Timber.v("## sendOutgoingRoomKeyRequestCancellation() : done") - val resend = request.state === OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND - - onDone() - - // Resend the request with a new ID - if (resend) { - sendRoomKeyRequest(request.requestBody, request.recipients) - } - } - - override fun onFailure(failure: Throwable) { - Timber.e("## sendOutgoingRoomKeyRequestCancellation failed") - onDone() - } - }) - } - - /** - * Send a SendToDeviceObject to a list of recipients - * - * @param message the message - * @param recipients the recipients. - * @param transactionId the transaction id - * @param callback the asynchronous callback. - */ - private fun sendMessageToDevices(message: Any, - recipients: List>, - transactionId: String?, - callback: MatrixCallback) { - val contentMap = MXUsersDevicesMap() - - for (recipient in recipients) { - // TODO Change this two hard coded key to something better - contentMap.setObject(recipient["userId"], recipient["deviceId"], message) - } - sendToDeviceTask - .configureWith(SendToDeviceTask.Params(EventType.ROOM_KEY_REQUEST, contentMap, transactionId)) { - this.callback = callback - this.callbackThread = TaskThread.CALLER - this.executionThread = TaskThread.CALLER - } - .executeBy(taskExecutor) - } - - companion object { - private const val SEND_KEY_REQUESTS_DELAY_MS = 500 - - private val BACKGROUND_HANDLER = createBackgroundHandler("OutgoingRoomKeyRequest") - } -} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingSecretRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingSecretRequest.kt new file mode 100755 index 0000000000..1497796743 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingSecretRequest.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 + +import com.squareup.moshi.JsonClass + +/** + * Represents an outgoing room key request + */ +@JsonClass(generateAdapter = true) +class OutgoingSecretRequest( + // Secret Name + val secretName: String?, + // list of recipients for the request + override var recipients: Map>, + // Unique id for this request. Used for both + // an id within the request for later pairing with a cancellation, and for + // the transaction id when sending the to_device messages to our local + override var requestId: String, + // current state of this request + override var state: OutgoingGossipingRequestState) : OutgoingGossipingRequest { + + // transaction id for the cancellation, if any +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/SendGossipRequestWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/SendGossipRequestWorker.kt new file mode 100644 index 0000000000..fb9c45da45 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/SendGossipRequestWorker.kt @@ -0,0 +1,148 @@ +/* + * 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 + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.failure.shouldBeRetried +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.LocalEcho +import im.vector.matrix.android.api.session.events.model.toContent +import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap +import im.vector.matrix.android.internal.crypto.model.rest.GossipingToDeviceObject +import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyShareRequest +import im.vector.matrix.android.internal.crypto.model.rest.SecretShareRequest +import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore +import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask +import im.vector.matrix.android.internal.worker.WorkerParamsFactory +import im.vector.matrix.android.internal.worker.getSessionComponent +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal class SendGossipRequestWorker(context: Context, + params: WorkerParameters) + : CoroutineWorker(context, params) { + + @JsonClass(generateAdapter = true) + internal data class Params( + val sessionId: String, + val keyShareRequest: OutgoingRoomKeyRequest? = null, + val secretShareRequest: OutgoingSecretRequest? = null + ) + + @Inject lateinit var sendToDeviceTask: SendToDeviceTask + @Inject lateinit var cryptoStore: IMXCryptoStore + @Inject lateinit var eventBus: EventBus + @Inject lateinit var credentials: Credentials + + override suspend fun doWork(): Result { + val errorOutputData = Data.Builder().putBoolean("failed", true).build() + val params = WorkerParamsFactory.fromData(inputData) + ?: return Result.success(errorOutputData) + + val sessionComponent = getSessionComponent(params.sessionId) + ?: return Result.success(errorOutputData).also { + // TODO, can this happen? should I update local echo? + Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}") + } + sessionComponent.inject(this) + + val localId = LocalEcho.createLocalEchoId() + val contentMap = MXUsersDevicesMap() + val eventType: String + val requestId: String + when { + params.keyShareRequest != null -> { + eventType = EventType.ROOM_KEY_REQUEST + requestId = params.keyShareRequest.requestId + val toDeviceContent = RoomKeyShareRequest( + requestingDeviceId = credentials.deviceId, + requestId = params.keyShareRequest.requestId, + action = GossipingToDeviceObject.ACTION_SHARE_REQUEST, + body = params.keyShareRequest.requestBody + ) + cryptoStore.saveGossipingEvent(Event( + type = eventType, + content = toDeviceContent.toContent(), + senderId = credentials.userId + ).also { + it.ageLocalTs = System.currentTimeMillis() + }) + + params.keyShareRequest.recipients.forEach { userToDeviceMap -> + userToDeviceMap.value.forEach { deviceId -> + contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent) + } + } + } + params.secretShareRequest != null -> { + eventType = EventType.REQUEST_SECRET + requestId = params.secretShareRequest.requestId + val toDeviceContent = SecretShareRequest( + requestingDeviceId = credentials.deviceId, + requestId = params.secretShareRequest.requestId, + action = GossipingToDeviceObject.ACTION_SHARE_REQUEST, + secretName = params.secretShareRequest.secretName + ) + + cryptoStore.saveGossipingEvent(Event( + type = eventType, + content = toDeviceContent.toContent(), + senderId = credentials.userId + ).also { + it.ageLocalTs = System.currentTimeMillis() + }) + + params.secretShareRequest.recipients.forEach { userToDeviceMap -> + userToDeviceMap.value.forEach { deviceId -> + contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent) + } + } + } + else -> { + return Result.success(errorOutputData).also { + Timber.e("Unknown empty gossiping request: $params") + } + } + } + try { + cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.SENDING) + sendToDeviceTask.execute( + SendToDeviceTask.Params( + eventType = eventType, + contentMap = contentMap, + transactionId = localId + ) + ) + cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.SENT) + return Result.success() + } catch (exception: Throwable) { + return if (exception.shouldBeRetried()) { + Result.retry() + } else { + cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.FAILED_TO_SEND) + Result.success(errorOutputData) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/SendGossipWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/SendGossipWorker.kt new file mode 100644 index 0000000000..8a273da338 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/SendGossipWorker.kt @@ -0,0 +1,141 @@ +/* + * 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 + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.failure.shouldBeRetried +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.LocalEcho +import im.vector.matrix.android.api.session.events.model.toContent +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.model.MXUsersDevicesMap +import im.vector.matrix.android.internal.crypto.model.event.SecretSendEventContent +import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore +import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask +import im.vector.matrix.android.internal.worker.WorkerParamsFactory +import im.vector.matrix.android.internal.worker.getSessionComponent +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal class SendGossipWorker(context: Context, + params: WorkerParameters) + : CoroutineWorker(context, params) { + + @JsonClass(generateAdapter = true) + internal data class Params( + val sessionId: String, + val secretValue: String, + val request: IncomingSecretShareRequest + ) + + @Inject lateinit var sendToDeviceTask: SendToDeviceTask + @Inject lateinit var cryptoStore: IMXCryptoStore + @Inject lateinit var eventBus: EventBus + @Inject lateinit var credentials: Credentials + @Inject lateinit var messageEncrypter: MessageEncrypter + @Inject lateinit var ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction + + override suspend fun doWork(): Result { + val errorOutputData = Data.Builder().putBoolean("failed", true).build() + val params = WorkerParamsFactory.fromData(inputData) + ?: return Result.success(errorOutputData) + + val sessionComponent = getSessionComponent(params.sessionId) + ?: return Result.success(errorOutputData).also { + // TODO, can this happen? should I update local echo? + Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}") + } + sessionComponent.inject(this) + + val localId = LocalEcho.createLocalEchoId() + val eventType: String = EventType.SEND_SECRET + + val toDeviceContent = SecretSendEventContent( + requestId = params.request.requestId ?: "", + secretValue = params.secretValue + ) + + val requestingUserId = params.request.userId ?: "" + val requestingDeviceId = params.request.deviceId ?: "" + val deviceInfo = cryptoStore.getUserDevice(requestingUserId, requestingDeviceId) + ?: return Result.success(errorOutputData).also { + cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED) + Timber.e("Unknown deviceInfo, cannot send message, sessionId: ${params.request.deviceId}") + } + + val sendToDeviceMap = MXUsersDevicesMap() + + val devicesByUser = mapOf(requestingUserId to listOf(deviceInfo)) + val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser) + val olmSessionResult = usersDeviceMap.getObject(requestingUserId, requestingDeviceId) + if (olmSessionResult?.sessionId == null) { + // no session with this device, probably because there + // were no one-time keys. + return Result.success(errorOutputData).also { + cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED) + Timber.e("no session with this device, probably because there were no one-time keys.") + } + } + + val payloadJson = mapOf( + "type" to EventType.SEND_SECRET, + "content" to toDeviceContent.toContent() + ) + + try { + val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) + sendToDeviceMap.setObject(requestingUserId, requestingDeviceId, encodedPayload) + } catch (failure: Throwable) { + Timber.e("## Fail to encrypt gossip + ${failure.localizedMessage}") + } + + cryptoStore.saveGossipingEvent(Event( + type = eventType, + content = toDeviceContent.toContent(), + senderId = credentials.userId + ).also { + it.ageLocalTs = System.currentTimeMillis() + }) + + try { + sendToDeviceTask.execute( + SendToDeviceTask.Params( + eventType = EventType.ENCRYPTED, + contentMap = sendToDeviceMap, + transactionId = localId + ) + ) + cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.ACCEPTED) + return Result.success() + } catch (exception: Throwable) { + return if (exception.shouldBeRetried()) { + Result.retry() + } else { + cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED) + Result.success(errorOutputData) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/MegolmSessionDataImporter.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/MegolmSessionDataImporter.kt index 6f41116b90..cac4659ae5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/MegolmSessionDataImporter.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/MegolmSessionDataImporter.kt @@ -20,7 +20,7 @@ import androidx.annotation.WorkerThread import im.vector.matrix.android.api.listeners.ProgressListener import im.vector.matrix.android.internal.crypto.MXOlmDevice import im.vector.matrix.android.internal.crypto.MegolmSessionData -import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequestManager +import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager import im.vector.matrix.android.internal.crypto.RoomDecryptorProvider import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody @@ -30,7 +30,7 @@ import javax.inject.Inject internal class MegolmSessionDataImporter @Inject constructor(private val olmDevice: MXOlmDevice, private val roomDecryptorProvider: RoomDecryptorProvider, - private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager, + private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, private val cryptoStore: IMXCryptoStore) { /** @@ -73,7 +73,7 @@ internal class MegolmSessionDataImporter @Inject constructor(private val olmDevi sessionId = megolmSessionData.sessionId ) - outgoingRoomKeyRequestManager.cancelRoomKeyRequest(roomKeyRequestBody) + outgoingGossipingRequestManager.cancelRoomKeyRequest(roomKeyRequestBody) // Have another go at decrypting events sent with this session decrypting.onNewSession(megolmSessionData.senderKey!!, sessionId!!) 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 8a66029026..e9176ad6d9 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 @@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.crypto.algorithms import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest +import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult import im.vector.matrix.android.internal.crypto.keysbackup.DefaultKeysBackupService @@ -65,4 +66,8 @@ internal interface IMXDecrypting { * @param request keyRequest */ fun shareKeysWithDevice(request: IncomingRoomKeyRequest) {} + + fun shareSecretWithDevice(request: IncomingSecretShareRequest, secretValue : String) {} + + fun requestKeysForEvent(event: Event) } 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 c8a1d628d7..1d7a2765fa 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 @@ -26,7 +26,7 @@ import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult import im.vector.matrix.android.internal.crypto.MXOlmDevice import im.vector.matrix.android.internal.crypto.NewSessionListener -import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequestManager +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 @@ -46,7 +46,7 @@ import timber.log.Timber internal class MXMegolmDecryption(private val userId: String, private val olmDevice: MXOlmDevice, private val deviceListManager: DeviceListManager, - private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager, + private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, private val messageEncrypter: MessageEncrypter, private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, private val cryptoStore: IMXCryptoStore, @@ -144,23 +144,23 @@ internal class MXMegolmDecryption(private val userId: String, * * @param event the event */ - private fun requestKeysForEvent(event: Event) { - val sender = event.senderId!! - val encryptedEventContent = event.content.toModel()!! + override fun requestKeysForEvent(event: Event) { + val sender = event.senderId ?: return + val encryptedEventContent = event.content.toModel() + val senderDevice = encryptedEventContent?.deviceId ?: return - val recipients = ArrayList>() - - val selfMap = HashMap() - // TODO Replace this hard coded keys (see OutgoingRoomKeyRequestManager) - selfMap["userId"] = userId - selfMap["deviceId"] = "*" - recipients.add(selfMap) - - if (sender != userId) { - val senderMap = HashMap() - senderMap["userId"] = sender - senderMap["deviceId"] = encryptedEventContent.deviceId!! - recipients.add(senderMap) + val recipients = if (event.senderId == userId) { + mapOf( + userId to listOf("*") + ) + } else { + // for the case where you share the key with a device that has a broken olm session + // The other user might Re-shares a megolm session key with devices if the key has already been + // sent to them. + mapOf( + userId to listOf("*"), + sender to listOf(senderDevice) + ) } val requestBody = RoomKeyRequestBody( @@ -170,7 +170,7 @@ internal class MXMegolmDecryption(private val userId: String, sessionId = encryptedEventContent.sessionId ) - outgoingRoomKeyRequestManager.sendRoomKeyRequest(requestBody, recipients) + outgoingGossipingRequestManager.sendRoomKeyRequest(requestBody, recipients) } /** @@ -271,7 +271,7 @@ internal class MXMegolmDecryption(private val userId: String, senderKey = senderKey ) - outgoingRoomKeyRequestManager.cancelRoomKeyRequest(content) + outgoingGossipingRequestManager.cancelRoomKeyRequest(content) onNewSession(senderKey, roomKeyContent.sessionId) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt index 7cddd27779..e8044186d8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt @@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.crypto.algorithms.megolm import im.vector.matrix.android.internal.crypto.DeviceListManager import im.vector.matrix.android.internal.crypto.MXOlmDevice -import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequestManager +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.store.IMXCryptoStore @@ -32,7 +32,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor( @UserId private val userId: String, private val olmDevice: MXOlmDevice, private val deviceListManager: DeviceListManager, - private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager, + private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, private val messageEncrypter: MessageEncrypter, private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, private val cryptoStore: IMXCryptoStore, @@ -46,7 +46,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor( userId, olmDevice, deviceListManager, - outgoingRoomKeyRequestManager, + outgoingGossipingRequestManager, messageEncrypter, ensureOlmSessionsForDevicesAction, cryptoStore, 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 e0454aea0d..0a8ef3993b 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 @@ -210,4 +210,8 @@ internal class MXOlmDecryption( return res["payload"] } + + override fun requestKeysForEvent(event: Event) { + // nop + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt index acc9f4134d..2166e4be3a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt @@ -17,21 +17,17 @@ package im.vector.matrix.android.internal.crypto.crosssigning import androidx.lifecycle.LiveData -import dagger.Lazy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.internal.crypto.DeviceListManager -import im.vector.matrix.android.internal.crypto.MXOlmDevice -import im.vector.matrix.android.internal.crypto.MyDeviceInfoHolder -import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey -import im.vector.matrix.android.internal.crypto.model.KeyUsage import im.vector.matrix.android.internal.crypto.model.rest.UploadSignatureQueryBuilder import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore +import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo +import im.vector.matrix.android.internal.crypto.tasks.InitializeCrossSigningTask import im.vector.matrix.android.internal.crypto.tasks.UploadSignaturesTask -import im.vector.matrix.android.internal.crypto.tasks.UploadSigningKeysTask import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.task.TaskExecutor @@ -52,12 +48,9 @@ import javax.inject.Inject internal class DefaultCrossSigningService @Inject constructor( @UserId private val userId: String, private val cryptoStore: IMXCryptoStore, - private val myDeviceInfoHolder: Lazy, - private val olmDevice: MXOlmDevice, private val deviceListManager: DeviceListManager, - private val uploadSigningKeysTask: UploadSigningKeysTask, + private val initializeCrossSigningTask: InitializeCrossSigningTask, private val uploadSignaturesTask: UploadSignaturesTask, - private val computeTrustTask: ComputeTrustTask, private val taskExecutor: TaskExecutor, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val cryptoCoroutineScope: CoroutineScope, @@ -150,151 +143,82 @@ internal class DefaultCrossSigningService @Inject constructor( override fun initializeCrossSigning(authParams: UserPasswordAuth?, callback: MatrixCallback?) { Timber.d("## CrossSigning initializeCrossSigning") - // ================= - // MASTER KEY - // ================= - val masterPkOlm = OlmPkSigning() - val masterKeyPrivateKey = OlmPkSigning.generateSeed() - val masterPublicKey = masterPkOlm.initWithSeed(masterKeyPrivateKey) - - Timber.v("## CrossSigning - masterPublicKey:$masterPublicKey") - - // ================= - // USER KEY - // ================= - val userSigningPkOlm = OlmPkSigning() - val uskPrivateKey = OlmPkSigning.generateSeed() - val uskPublicKey = userSigningPkOlm.initWithSeed(uskPrivateKey) - - Timber.v("## CrossSigning - uskPublicKey:$uskPublicKey") - - // Sign userSigningKey with master - val signedUSK = CryptoCrossSigningKey.Builder(userId, KeyUsage.USER_SIGNING) - .key(uskPublicKey) - .build() - .canonicalSignable() - .let { masterPkOlm.sign(it) } - - // ================= - // SELF SIGNING KEY - // ================= - val selfSigningPkOlm = OlmPkSigning() - val sskPrivateKey = OlmPkSigning.generateSeed() - val sskPublicKey = selfSigningPkOlm.initWithSeed(sskPrivateKey) - - Timber.v("## CrossSigning - sskPublicKey:$sskPublicKey") - - // Sign userSigningKey with master - val signedSSK = JsonCanonicalizer.getCanonicalJson(Map::class.java, CryptoCrossSigningKey.Builder(userId, KeyUsage.SELF_SIGNING) - .key(sskPublicKey) - .build().signalableJSONDictionary()).let { masterPkOlm.sign(it) } - - // I need to upload the keys - val mskCrossSigningKeyInfo = CryptoCrossSigningKey.Builder(userId, KeyUsage.MASTER) - .key(masterPublicKey) - .build() - val params = UploadSigningKeysTask.Params( - masterKey = mskCrossSigningKeyInfo, - userKey = CryptoCrossSigningKey.Builder(userId, KeyUsage.USER_SIGNING) - .key(uskPublicKey) - .signature(userId, masterPublicKey, signedUSK) - .build(), - selfSignedKey = CryptoCrossSigningKey.Builder(userId, KeyUsage.SELF_SIGNING) - .key(sskPublicKey) - .signature(userId, masterPublicKey, signedSSK) - .build(), - userPasswordAuth = authParams + val params = InitializeCrossSigningTask.Params( + authParams = authParams ) - - this.masterPkSigning = masterPkOlm - this.userPkSigning = userSigningPkOlm - this.selfSigningPkSigning = selfSigningPkOlm - - val crossSigningInfo = MXCrossSigningInfo(userId, listOf(params.masterKey, params.userKey, params.selfSignedKey)) - cryptoStore.setMyCrossSigningInfo(crossSigningInfo) - setUserKeysAsTrusted(userId, true) - cryptoStore.storePrivateKeysInfo(masterKeyPrivateKey?.toBase64NoPadding(), uskPrivateKey?.toBase64NoPadding(), sskPrivateKey?.toBase64NoPadding()) - - uploadSigningKeysTask.configureWith(params) { - this.executionThread = TaskThread.CRYPTO - this.callback = object : MatrixCallback { - override fun onSuccess(data: Unit) { - Timber.i("## CrossSigning - Keys successfully uploaded") - - // Sign the current device with SSK - val uploadSignatureQueryBuilder = UploadSignatureQueryBuilder() - - val myDevice = myDeviceInfoHolder.get().myDevice - val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, myDevice.signalableJSONDictionary()) - val signedDevice = selfSigningPkOlm.sign(canonicalJson) - val updateSignatures = (myDevice.signatures?.toMutableMap() ?: HashMap()) - .also { - it[userId] = (it[userId] - ?: HashMap()) + mapOf("ed25519:$sskPublicKey" to signedDevice) - } - myDevice.copy(signatures = updateSignatures).let { - uploadSignatureQueryBuilder.withDeviceInfo(it) - } - - // sign MSK with device key (migration) and upload signatures - val message = JsonCanonicalizer.getCanonicalJson(Map::class.java, mskCrossSigningKeyInfo.signalableJSONDictionary()) - olmDevice.signMessage(message)?.let { sign -> - val mskUpdatedSignatures = (mskCrossSigningKeyInfo.signatures?.toMutableMap() - ?: HashMap()).also { - it[userId] = (it[userId] - ?: HashMap()) + mapOf("ed25519:${myDevice.deviceId}" to sign) - } - mskCrossSigningKeyInfo.copy( - signatures = mskUpdatedSignatures - ).let { - uploadSignatureQueryBuilder.withSigningKeyInfo(it) - } - } - - resetTrustOnKeyChange() - uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadSignatureQueryBuilder.build())) { - // this.retryCount = 3 - this.executionThread = TaskThread.CRYPTO - this.callback = object : MatrixCallback { - override fun onSuccess(data: Unit) { - Timber.i("## CrossSigning - signatures successfully uploaded") - callback?.onSuccess(Unit) - } - - override fun onFailure(failure: Throwable) { - // Clear - Timber.e(failure, "## CrossSigning - Failed to upload signatures") - clearSigningKeys() - } - } - }.executeBy(taskExecutor) + initializeCrossSigningTask.configureWith(params) { + this.callbackThread = TaskThread.CRYPTO + this.callback = object : MatrixCallback { + override fun onFailure(failure: Throwable) { + callback?.onFailure(failure) } - override fun onFailure(failure: Throwable) { - Timber.e(failure, "## CrossSigning - Failed to upload signing keys") - clearSigningKeys() - callback?.onFailure(failure) + override fun onSuccess(data: InitializeCrossSigningTask.Result) { + val crossSigningInfo = MXCrossSigningInfo(userId, listOf(data.masterKeyInfo, data.userKeyInfo, data.selfSignedKeyInfo)) + cryptoStore.setMyCrossSigningInfo(crossSigningInfo) + setUserKeysAsTrusted(userId, true) + cryptoStore.storePrivateKeysInfo(data.masterKeyPK, data.userKeyPK, data.selfSigningKeyPK) + masterPkSigning = OlmPkSigning().apply { initWithSeed(data.masterKeyPK.fromBase64()) } + userPkSigning = OlmPkSigning().apply { initWithSeed(data.userKeyPK.fromBase64()) } + selfSigningPkSigning = OlmPkSigning().apply { initWithSeed(data.selfSigningKeyPK.fromBase64()) } + + callback?.onSuccess(Unit) } } }.executeBy(taskExecutor) } - private fun clearSigningKeys() { - masterPkSigning?.releaseSigning() - userPkSigning?.releaseSigning() - selfSigningPkSigning?.releaseSigning() + override fun onSecretSSKGossip(sskPrivateKey: String) { + Timber.i("## CrossSigning - onSecretSSKGossip") + val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return Unit.also { + Timber.e("## CrossSigning - onSecretSSKGossip() received secret but public key is not known") + } - masterPkSigning = null - userPkSigning = null - selfSigningPkSigning = null - - cryptoStore.setMyCrossSigningInfo(null) - cryptoStore.storePrivateKeysInfo(null, null, null) + sskPrivateKey.fromBase64() + .let { privateKeySeed -> + val pkSigning = OlmPkSigning() + try { + if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) { + selfSigningPkSigning?.releaseSigning() + selfSigningPkSigning = pkSigning + Timber.i("## CrossSigning - Loading SSK success") + cryptoStore.storeSSKPrivateKey(sskPrivateKey) + return + } else { + Timber.e("## CrossSigning - onSecretSSKGossip() private key do not match public key") + pkSigning.releaseSigning() + } + } catch (failure: Throwable) { + Timber.e("## CrossSigning - onSecretSSKGossip() ${failure.localizedMessage}") + pkSigning.releaseSigning() + } + } } - private fun resetTrustOnKeyChange() { - Timber.i("## CrossSigning - Clear all other user trust") - cryptoStore.clearOtherUserTrust() + override fun onSecretUSKGossip(uskPrivateKey: String) { + Timber.i("## CrossSigning - onSecretUSKGossip") + val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return Unit.also { + Timber.e("## CrossSigning - onSecretUSKGossip() received secret but public key is not knwow ") + } + + uskPrivateKey.fromBase64() + .let { privateKeySeed -> + val pkSigning = OlmPkSigning() + try { + if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) { + userPkSigning?.releaseSigning() + userPkSigning = pkSigning + Timber.i("## CrossSigning - Loading USK success") + cryptoStore.storeUSKPrivateKey(uskPrivateKey) + return + } else { + Timber.e("## CrossSigning - onSecretUSKGossip() private key do not match public key") + pkSigning.releaseSigning() + } + } catch (failure: Throwable) { + pkSigning.releaseSigning() + } + } } override fun checkTrustFromPrivateKeys(masterKeyPrivateKey: String?, @@ -396,7 +320,7 @@ internal class DefaultCrossSigningService @Inject constructor( * Will not force a download of the key, but will verify signatures trust chain */ override fun checkUserTrust(otherUserId: String): UserTrustResult { - Timber.d("## CrossSigning checkUserTrust for $otherUserId") + Timber.v("## CrossSigning checkUserTrust for $otherUserId") if (otherUserId == userId) { return checkSelfTrust() } @@ -542,102 +466,113 @@ internal class DefaultCrossSigningService @Inject constructor( return cryptoStore.getMyCrossSigningInfo() } + override fun getCrossSigningPrivateKeys(): PrivateKeysInfo? { + return cryptoStore.getCrossSigningPrivateKeys() + } + override fun canCrossSign(): Boolean { return checkSelfTrust().isVerified() && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null + && cryptoStore.getCrossSigningPrivateKeys()?.user != null } override fun trustUser(otherUserId: String, callback: MatrixCallback) { - Timber.d("## CrossSigning - Mark user $userId as trusted ") - // We should have this user keys - val otherMasterKeys = getUserCrossSigningKeys(otherUserId)?.masterKey() - if (otherMasterKeys == null) { - callback.onFailure(Throwable("## CrossSigning - Other master signing key is not known")) - return - } - val myKeys = getUserCrossSigningKeys(userId) - if (myKeys == null) { - callback.onFailure(Throwable("## CrossSigning - CrossSigning is not setup for this account")) - return - } - val userPubKey = myKeys.userKey()?.unpaddedBase64PublicKey - if (userPubKey == null || userPkSigning == null) { - callback.onFailure(Throwable("## CrossSigning - Cannot sign from this account, privateKeyUnknown $userPubKey")) - return - } + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + Timber.d("## CrossSigning - Mark user $userId as trusted ") + // We should have this user keys + val otherMasterKeys = getUserCrossSigningKeys(otherUserId)?.masterKey() + if (otherMasterKeys == null) { + callback.onFailure(Throwable("## CrossSigning - Other master signing key is not known")) + return@launch + } + val myKeys = getUserCrossSigningKeys(userId) + if (myKeys == null) { + callback.onFailure(Throwable("## CrossSigning - CrossSigning is not setup for this account")) + return@launch + } + val userPubKey = myKeys.userKey()?.unpaddedBase64PublicKey + if (userPubKey == null || userPkSigning == null) { + callback.onFailure(Throwable("## CrossSigning - Cannot sign from this account, privateKeyUnknown $userPubKey")) + return@launch + } - // Sign the other MasterKey with our UserSigning key - val newSignature = JsonCanonicalizer.getCanonicalJson(Map::class.java, - otherMasterKeys.signalableJSONDictionary()).let { userPkSigning?.sign(it) } + // Sign the other MasterKey with our UserSigning key + val newSignature = JsonCanonicalizer.getCanonicalJson(Map::class.java, + otherMasterKeys.signalableJSONDictionary()).let { userPkSigning?.sign(it) } - if (newSignature == null) { - // race?? - callback.onFailure(Throwable("## CrossSigning - Failed to sign")) - return + if (newSignature == null) { + // race?? + callback.onFailure(Throwable("## CrossSigning - Failed to sign")) + return@launch + } + + cryptoStore.setUserKeysAsTrusted(otherUserId, true) + // TODO update local copy with new signature directly here? kind of local echo of trust? + + Timber.d("## CrossSigning - Upload signature of $userId MSK signed by USK") + val uploadQuery = UploadSignatureQueryBuilder() + .withSigningKeyInfo(otherMasterKeys.copyForSignature(userId, userPubKey, newSignature)) + .build() + uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) { + this.executionThread = TaskThread.CRYPTO + this.callback = callback + }.executeBy(taskExecutor) } - - cryptoStore.setUserKeysAsTrusted(otherUserId, true) - // TODO update local copy with new signature directly here? kind of local echo of trust? - - Timber.d("## CrossSigning - Upload signature of $userId MSK signed by USK") - val uploadQuery = UploadSignatureQueryBuilder() - .withSigningKeyInfo(otherMasterKeys.copyForSignature(userId, userPubKey, newSignature)) - .build() - uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) { - this.executionThread = TaskThread.CRYPTO - this.callback = callback - }.executeBy(taskExecutor) } override fun markMyMasterKeyAsTrusted() { - cryptoStore.markMyMasterKeyAsLocallyTrusted(true) - checkSelfTrust() + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + cryptoStore.markMyMasterKeyAsLocallyTrusted(true) + checkSelfTrust() + } } override fun trustDevice(deviceId: String, callback: MatrixCallback) { - // This device should be yours - val device = cryptoStore.getUserDevice(userId, deviceId) - if (device == null) { - callback.onFailure(IllegalArgumentException("This device [$deviceId] is not known, or not yours")) - return + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + // This device should be yours + val device = cryptoStore.getUserDevice(userId, deviceId) + if (device == null) { + callback.onFailure(IllegalArgumentException("This device [$deviceId] is not known, or not yours")) + return@launch + } + + val myKeys = getUserCrossSigningKeys(userId) + if (myKeys == null) { + callback.onFailure(Throwable("CrossSigning is not setup for this account")) + return@launch + } + + val ssPubKey = myKeys.selfSigningKey()?.unpaddedBase64PublicKey + if (ssPubKey == null || selfSigningPkSigning == null) { + callback.onFailure(Throwable("Cannot sign from this account, public and/or privateKey Unknown $ssPubKey")) + return@launch + } + + // Sign with self signing + val newSignature = selfSigningPkSigning?.sign(device.canonicalSignable()) + + if (newSignature == null) { + // race?? + callback.onFailure(Throwable("Failed to sign")) + return@launch + } + val toUpload = device.copy( + signatures = mapOf( + userId + to + mapOf( + "ed25519:$ssPubKey" to newSignature + ) + ) + ) + + val uploadQuery = UploadSignatureQueryBuilder() + .withDeviceInfo(toUpload) + .build() + uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) { + this.executionThread = TaskThread.CRYPTO + this.callback = callback + }.executeBy(taskExecutor) } - - val myKeys = getUserCrossSigningKeys(userId) - if (myKeys == null) { - callback.onFailure(Throwable("CrossSigning is not setup for this account")) - return - } - - val ssPubKey = myKeys.selfSigningKey()?.unpaddedBase64PublicKey - if (ssPubKey == null || selfSigningPkSigning == null) { - callback.onFailure(Throwable("Cannot sign from this account, public and/or privateKey Unknown $ssPubKey")) - return - } - - // Sign with self signing - val newSignature = selfSigningPkSigning?.sign(device.canonicalSignable()) - - if (newSignature == null) { - // race?? - callback.onFailure(Throwable("Failed to sign")) - return - } - val toUpload = device.copy( - signatures = mapOf( - userId - to - mapOf( - "ed25519:$ssPubKey" to newSignature - ) - ) - ) - - val uploadQuery = UploadSignatureQueryBuilder() - .withDeviceInfo(toUpload) - .build() - uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) { - this.executionThread = TaskThread.CRYPTO - this.callback = callback - }.executeBy(taskExecutor) } override fun checkDeviceTrust(otherUserId: String, otherDeviceId: String, locallyTrusted: Boolean?): DeviceTrustResult { @@ -706,15 +641,20 @@ internal class DefaultCrossSigningService @Inject constructor( Timber.d("## CrossSigning - onUsersDeviceUpdate for ${userIds.size} users") userIds.forEach { otherUserId -> checkUserTrust(otherUserId).let { - Timber.d("## CrossSigning - update trust for $otherUserId , verified=${it.isVerified()}") + Timber.v("## CrossSigning - update trust for $otherUserId , verified=${it.isVerified()}") setUserKeysAsTrusted(otherUserId, it.isVerified()) } + } + } + // now check device trust + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + userIds.forEach { otherUserId -> // TODO if my keys have changes, i should recheck all devices of all users? val devices = cryptoStore.getUserDeviceList(otherUserId) devices?.forEach { device -> val updatedTrust = checkDeviceTrust(otherUserId, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false) - Timber.d("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust") + Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust") cryptoStore.setDeviceTrust(otherUserId, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified()) } @@ -730,25 +670,39 @@ internal class DefaultCrossSigningService @Inject constructor( } private fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - val currentTrust = cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted() - cryptoStore.setUserKeysAsTrusted(otherUserId, trusted) - // If it's me, recheck trust of all users and devices? - val users = ArrayList() - if (otherUserId == userId && currentTrust != trusted) { - cryptoStore.updateUsersTrust { - users.add(it) - checkUserTrust(it).isVerified() - } + val currentTrust = cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted() + cryptoStore.setUserKeysAsTrusted(otherUserId, trusted) + // If it's me, recheck trust of all users and devices? + val users = ArrayList() + if (otherUserId == userId && currentTrust != trusted) { +// reRequestAllPendingRoomKeyRequest() + cryptoStore.updateUsersTrust { + users.add(it) + checkUserTrust(it).isVerified() + } - users.forEach { - cryptoStore.getUserDeviceList(it)?.forEach { device -> - val updatedTrust = checkDeviceTrust(it, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false) - Timber.d("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust") - cryptoStore.setDeviceTrust(it, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified()) - } + users.forEach { + cryptoStore.getUserDeviceList(it)?.forEach { device -> + val updatedTrust = checkDeviceTrust(it, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false) + Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust") + cryptoStore.setDeviceTrust(it, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified()) } } } } + +// private fun reRequestAllPendingRoomKeyRequest() { +// cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { +// Timber.d("## CrossSigning - reRequest pending outgoing room key requests") +// cryptoStore.getOutgoingRoomKeyRequests().forEach { +// it.requestBody?.let { requestBody -> +// if (cryptoStore.getInboundGroupSession(requestBody.sessionId ?: "", requestBody.senderKey ?: "") == null) { +// outgoingRoomKeyRequestManager.resendRoomKeyRequest(requestBody) +// } else { +// outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody) +// } +// } +// } +// } +// } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/DefaultKeysBackupService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/DefaultKeysBackupService.kt index 3ec5a2f979..75e37d27f6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/DefaultKeysBackupService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/DefaultKeysBackupService.kt @@ -36,6 +36,7 @@ import im.vector.matrix.android.internal.crypto.MXOlmDevice import im.vector.matrix.android.internal.crypto.MegolmSessionData import im.vector.matrix.android.internal.crypto.ObjectSigner import im.vector.matrix.android.internal.crypto.actions.MegolmSessionDataImporter +import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64 import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrust import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrustSignature import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupAuthData @@ -67,6 +68,7 @@ import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyF import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore +import im.vector.matrix.android.internal.crypto.store.SavedKeyBackupKeyInfo import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.UserId @@ -580,6 +582,31 @@ internal class DefaultKeysBackupService @Inject constructor( } } + override fun onSecretKeyGossip(secret: String) { + Timber.i("## CrossSigning - onSecretKeyGossip") + + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + try { + val keysBackupVersion = getKeysBackupLastVersionTask.execute(Unit) + val recoveryKey = computeRecoveryKey(secret.fromBase64()) + if (isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) { + awaitCallback { + trustKeysBackupVersion(keysBackupVersion, true, it) + } + val importResult = awaitCallback { + restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, null, null, null, it) + } + cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version) + Timber.i("onSecretKeyGossip: Recovered keys ${importResult.successfullyNumberOfImportedKeys} out of ${importResult.totalNumberOfKeys}") + } else { + Timber.e("onSecretKeyGossip: Recovery key is not valid ${keysBackupVersion.version}") + } + } catch (failure: Throwable) { + Timber.e("onSecretKeyGossip: failed to trust key backup version ${keysBackupVersion?.version}") + } + } + } + /** * Get public key from a Recovery key * @@ -1391,6 +1418,14 @@ internal class DefaultKeysBackupService @Inject constructor( .executeBy(taskExecutor) } + override fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? { + return cryptoStore.getKeyBackupRecoveryKeyInfo() + } + + override fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) { + cryptoStore.saveBackupRecoveryKey(recoveryKey, version) + } + companion object { // Maximum delay in ms in {@link maybeBackupKeys} private const val KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS = 10_000L diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoDeviceInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoDeviceInfo.kt index e3e8f3de27..b124f7590e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoDeviceInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoDeviceInfo.kt @@ -33,7 +33,7 @@ data class CryptoDeviceInfo( ) : CryptoInfo { val isVerified: Boolean - get() = trustLevel?.isVerified() ?: false + get() = trustLevel?.isVerified() == true val isUnknown: Boolean get() = trustLevel == null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/SecretSendEventContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/SecretSendEventContent.kt new file mode 100644 index 0000000000..4a856b32a1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/SecretSendEventContent.kt @@ -0,0 +1,28 @@ +/* + * 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 + +/** + * Class representing an encrypted event content + */ +@JsonClass(generateAdapter = true) +data class SecretSendEventContent( + @Json(name = "request_id") val requestId: String, + @Json(name = "secret") val secretValue: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShare.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/GossipingToDeviceObject.kt similarity index 70% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShare.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/GossipingToDeviceObject.kt index 4ea95d84ae..deaccdef16 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShare.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/GossipingToDeviceObject.kt @@ -15,11 +15,14 @@ */ package im.vector.matrix.android.internal.crypto.model.rest +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + /** * Interface representing an room key action request * Note: this class cannot be abstract because of [org.matrix.androidsdk.core.JsonUtils.toRoomKeyShare] */ -internal interface RoomKeyShare : SendToDeviceObject { +interface GossipingToDeviceObject : SendToDeviceObject { val action: String? @@ -32,3 +35,10 @@ internal interface RoomKeyShare : SendToDeviceObject { const val ACTION_SHARE_CANCELLATION = "request_cancellation" } } + +@JsonClass(generateAdapter = true) +data class GossipingDefaultContent( + @Json(name = "action") override val action: String?, + @Json(name = "requesting_device_id") override val requestingDeviceId: String?, + @Json(name = "m.request_id") override val requestId: String? = null +) : GossipingToDeviceObject diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryResponse.kt index dd3cb049dd..aea8eeb301 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryResponse.kt @@ -28,7 +28,7 @@ import com.squareup.moshi.JsonClass * The user_signing_keys property will only be included when a user requests their own keys. */ @JsonClass(generateAdapter = true) -internal data class KeysQueryResponse( +internal data class KeysQueryResponse( /** * The device keys per devices per users. * Map from userId to map from deviceId to MXDeviceInfo diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyRequestBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyRequestBody.kt index 3eb6600e5e..0b7c3a201f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyRequestBody.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyRequestBody.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.crypto.model.rest import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.di.MoshiProvider /** * Class representing an room key request body content @@ -35,4 +36,14 @@ data class RoomKeyRequestBody( @Json(name = "session_id") val sessionId: String? = null -) +) { + fun toJson(): String { + return MoshiProvider.providesMoshi().adapter(RoomKeyRequestBody::class.java).toJson(this) + } + + companion object { + fun fromJson(json: String?): RoomKeyRequestBody? { + return json?.let { MoshiProvider.providesMoshi().adapter(RoomKeyRequestBody::class.java).fromJson(it) } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareRequest.kt index d92bc03aab..c2fc6fe96b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareRequest.kt @@ -23,9 +23,9 @@ import com.squareup.moshi.JsonClass * Class representing a room key request content */ @JsonClass(generateAdapter = true) -internal data class RoomKeyShareRequest( +data class RoomKeyShareRequest( @Json(name = "action") - override val action: String? = RoomKeyShare.ACTION_SHARE_REQUEST, + override val action: String? = GossipingToDeviceObject.ACTION_SHARE_REQUEST, @Json(name = "requesting_device_id") override val requestingDeviceId: String? = null, @@ -35,4 +35,4 @@ internal data class RoomKeyShareRequest( @Json(name = "body") val body: RoomKeyRequestBody? = null -) : RoomKeyShare +) : GossipingToDeviceObject diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/SecretShareRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/SecretShareRequest.kt new file mode 100644 index 0000000000..86ae042166 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/SecretShareRequest.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.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing a room key request content + */ +@JsonClass(generateAdapter = true) +data class SecretShareRequest( + @Json(name = "action") + override val action: String? = GossipingToDeviceObject.ACTION_SHARE_REQUEST, + + @Json(name = "requesting_device_id") + override val requestingDeviceId: String? = null, + + @Json(name = "request_id") + override val requestId: String? = null, + + @Json(name = "name") + val secretName: String? = null +) : GossipingToDeviceObject diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareCancellation.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/ShareRequestCancellation.kt similarity index 80% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareCancellation.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/ShareRequestCancellation.kt index b394993338..10be81be7d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareCancellation.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/ShareRequestCancellation.kt @@ -17,18 +17,19 @@ package im.vector.matrix.android.internal.crypto.model.rest import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.crypto.model.rest.GossipingToDeviceObject.Companion.ACTION_SHARE_CANCELLATION /** * Class representing a room key request cancellation content */ @JsonClass(generateAdapter = true) -internal data class RoomKeyShareCancellation( +internal data class ShareRequestCancellation( @Json(name = "action") - override val action: String? = RoomKeyShare.ACTION_SHARE_CANCELLATION, + override val action: String? = ACTION_SHARE_CANCELLATION, @Json(name = "requesting_device_id") override val requestingDeviceId: String? = null, @Json(name = "request_id") override val requestId: String? = null -) : RoomKeyShare +) : GossipingToDeviceObject diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt index 649f60887d..62bc4774c6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt @@ -33,6 +33,7 @@ import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageSer import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo import im.vector.matrix.android.api.session.securestorage.SsssKeySpec import im.vector.matrix.android.api.session.securestorage.SsssPassphrase +import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager import im.vector.matrix.android.internal.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2 import im.vector.matrix.android.internal.crypto.SSSS_ALGORITHM_CURVE25519_AES_SHA2 import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64 @@ -41,6 +42,7 @@ import im.vector.matrix.android.internal.crypto.keysbackup.generatePrivateKeyWit import im.vector.matrix.android.internal.crypto.keysbackup.util.computeRecoveryKey import im.vector.matrix.android.internal.crypto.tools.HkdfSha256 import im.vector.matrix.android.internal.crypto.tools.withOlmDecryption +import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import kotlinx.coroutines.CoroutineScope @@ -55,7 +57,9 @@ import javax.inject.Inject import kotlin.experimental.and internal class DefaultSharedSecretStorageService @Inject constructor( + @UserId private val userId: String, private val accountDataService: AccountDataService, + private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val cryptoCoroutineScope: CoroutineScope ) : SharedSecretStorageService { @@ -98,7 +102,8 @@ internal class DefaultSharedSecretStorageService @Inject constructor( callback.onSuccess(SsssKeyCreationInfo( keyId = keyId, content = storageKeyContent, - recoveryKey = computeRecoveryKey(key) + recoveryKey = computeRecoveryKey(key), + keySpec = RawBytesKeySpec(key) )) } } @@ -138,7 +143,8 @@ internal class DefaultSharedSecretStorageService @Inject constructor( callback.onSuccess(SsssKeyCreationInfo( keyId = keyId, content = storageKeyContent, - recoveryKey = computeRecoveryKey(privatePart.privateKey) + recoveryKey = computeRecoveryKey(privatePart.privateKey), + keySpec = RawBytesKeySpec(privatePart.privateKey) )) } } @@ -268,7 +274,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor( val ivParameterSpec = IvParameterSpec(iv) cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec) // secret are not that big, just do Final - val cipherBytes = cipher.doFinal(clearDataBase64.fromBase64()) + val cipherBytes = cipher.doFinal(clearDataBase64.toByteArray()) require(cipherBytes.isNotEmpty()) val macKeySpec = SecretKeySpec(macKey, "HmacSHA256") @@ -299,6 +305,15 @@ internal class DefaultSharedSecretStorageService @Inject constructor( val cipherRawBytes = cipherContent.ciphertext?.fromBase64() ?: throw SharedSecretStorageError.BadCipherText + // Check Signature + val macKeySpec = SecretKeySpec(macKey, "HmacSHA256") + val mac = Mac.getInstance("HmacSHA256").apply { init(macKeySpec) } + val digest = mac.doFinal(cipherRawBytes) + + if (!cipherContent.mac?.fromBase64()?.contentEquals(digest).orFalse()) { + throw SharedSecretStorageError.BadMac + } + val cipher = Cipher.getInstance("AES/CTR/NoPadding") val secretKeySpec = SecretKeySpec(aesKey, "AES") @@ -309,17 +324,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor( require(decryptedSecret.isNotEmpty()) - // Check Signature - val macKeySpec = SecretKeySpec(macKey, "HmacSHA256") - val mac = Mac.getInstance("HmacSHA256").apply { init(macKeySpec) } - val digest = mac.doFinal(cipherRawBytes) - - if (!cipherContent.mac?.fromBase64()?.contentEquals(digest).orFalse()) { - throw SharedSecretStorageError.BadMac - } else { - // we are good - return decryptedSecret.toBase64NoPadding() - } + return String(decryptedSecret, Charsets.UTF_8) } override fun getAlgorithmsForSecret(name: String): List { @@ -429,4 +434,11 @@ internal class DefaultSharedSecretStorageService @Inject constructor( return IntegrityResult.Success(keyInfo.content.passphrase != null) } + + override fun requestSecret(name: String, myOtherDeviceId: String) { + outgoingGossipingRequestManager.sendSecretShareRequest( + name, + mapOf(userId to listOf(myOtherDeviceId)) + ) + } } 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 e89f4a49ed..a8f65e9219 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 @@ -19,11 +19,15 @@ package im.vector.matrix.android.internal.crypto.store import androidx.lifecycle.LiveData import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo +import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.util.Optional +import im.vector.matrix.android.internal.crypto.GossipingRequestState import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest -import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequestCommon +import im.vector.matrix.android.internal.crypto.IncomingShareRequestCommon import im.vector.matrix.android.internal.crypto.NewSessionListener +import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestState 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.OlmInboundGroupSessionWrapper @@ -45,7 +49,9 @@ internal interface IMXCryptoStore { /** * @return the olm account */ - fun getAccount(): OlmAccount? + fun getOlmAccount(): OlmAccount + + fun getOrCreateOlmAccount(): OlmAccount /** * Retrieve the known inbound group sessions. @@ -117,6 +123,10 @@ internal interface IMXCryptoStore { */ fun getPendingIncomingRoomKeyRequests(): List + fun getPendingIncomingGossipingRequests(): List + fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?) +// fun getPendingIncomingSecretShareRequests(): List + /** * Indicate if the store contains data for the passed account. * @@ -151,7 +161,7 @@ internal interface IMXCryptoStore { * * @param account the account to save */ - fun storeAccount(account: OlmAccount) + fun saveOlmAccount() /** * Store a device for a user. @@ -187,8 +197,8 @@ internal interface IMXCryptoStore { fun storeUserDevices(userId: String, devices: Map?) fun storeUserCrossSigningKeys(userId: String, masterKey: CryptoCrossSigningKey?, - selfSigningKey: CryptoCrossSigningKey?, - userSigningKey: CryptoCrossSigningKey?) + selfSigningKey: CryptoCrossSigningKey?, + userSigningKey: CryptoCrossSigningKey?) /** * Retrieve the known devices for a user. @@ -206,6 +216,7 @@ internal interface IMXCryptoStore { // TODO temp fun getLiveDeviceList(): LiveData> + /** * Store the crypto algorithm for a room. * @@ -347,43 +358,13 @@ internal interface IMXCryptoStore { * @param request the request * @return either the same instance as passed in, or the existing one. */ - fun getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): OutgoingRoomKeyRequest? + fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map>): OutgoingRoomKeyRequest? - /** - * Look for room key requests by state. - * - * @param states the states - * @return an OutgoingRoomKeyRequest or null - */ - fun getOutgoingRoomKeyRequestByState(states: Set): OutgoingRoomKeyRequest? + fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map>): OutgoingSecretRequest? - /** - * Update an existing outgoing request. - * - * @param request the request - */ - fun updateOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest) + fun saveGossipingEvent(event: Event) - /** - * Delete an outgoing room key request. - * - * @param transactionId the transaction id. - */ - fun deleteOutgoingRoomKeyRequest(transactionId: String) - - /** - * Store an incomingRoomKeyRequest instance - * - * @param incomingRoomKeyRequest the incoming key request - */ - fun storeIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequest?) - - /** - * Delete an incomingRoomKeyRequest instance - * - * @param incomingRoomKeyRequest the incoming key request - */ - fun deleteIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequestCommon) + fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState) /** * Search an IncomingRoomKeyRequest @@ -395,6 +376,8 @@ internal interface IMXCryptoStore { */ fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest? + fun updateOutgoingGossipingRequestState(requestId: String, state: OutgoingGossipingRequestState) + fun addNewSessionListener(listener: NewSessionListener) fun removeSessionListener(listener: NewSessionListener) @@ -406,22 +389,37 @@ internal interface IMXCryptoStore { /** * Gets the current crosssigning info */ - fun getMyCrossSigningInfo() : MXCrossSigningInfo? + fun getMyCrossSigningInfo(): MXCrossSigningInfo? + fun setMyCrossSigningInfo(info: MXCrossSigningInfo?) - fun getCrossSigningInfo(userId: String) : MXCrossSigningInfo? - fun getLiveCrossSigningInfo(userId: String) : LiveData> + fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? + fun getLiveCrossSigningInfo(userId: String): LiveData> fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?) fun markMyMasterKeyAsLocallyTrusted(trusted: Boolean) fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) - fun getCrossSigningPrivateKeys() : PrivateKeysInfo? + fun storeSSKPrivateKey(ssk: String?) + fun storeUSKPrivateKey(usk: String?) + + fun getCrossSigningPrivateKeys(): PrivateKeysInfo? + + fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) + fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo? fun setUserKeysAsTrusted(userId: String, trusted: Boolean = true) - fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified : Boolean) + fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified: Boolean) fun clearOtherUserTrust() fun updateUsersTrust(check: (String) -> Boolean) + + // Dev tools + + fun getOutgoingRoomKeyRequests(): List + fun getOutgoingSecretKeyRequests(): List + fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest? + fun getIncomingRoomKeyRequests(): List + fun getGossipingEventsTrail(): List } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/SavedKeyBackupKeyInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/SavedKeyBackupKeyInfo.kt new file mode 100644 index 0000000000..fda9bb1d72 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/SavedKeyBackupKeyInfo.kt @@ -0,0 +1,22 @@ +/* + * 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 + +data class SavedKeyBackupKeyInfo( + val recoveryKey : String, + val version: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/Helper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/Helper.kt index 81988fe209..642c466e42 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/Helper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/Helper.kt @@ -59,7 +59,12 @@ fun doRealmQueryAndCopyList(realmConfiguration: RealmConfigura */ fun doRealmTransaction(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) { Realm.getInstance(realmConfiguration).use { realm -> - realm.executeTransaction { action.invoke(realm) } + realm.executeTransaction { action.invoke(it) } + } +} +fun doRealmTransactionAsync(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) { + Realm.getInstance(realmConfiguration).use { realm -> + realm.executeTransactionAsync { action.invoke(it) } } } 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 a93203fc21..bd51cf8539 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 @@ -21,12 +21,21 @@ import androidx.lifecycle.Transformations import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.LocalEcho +import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.toOptional +import im.vector.matrix.android.internal.crypto.GossipRequestType +import im.vector.matrix.android.internal.crypto.GossipingRequestState import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest -import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequestCommon +import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest +import im.vector.matrix.android.internal.crypto.IncomingShareRequestCommon import im.vector.matrix.android.internal.crypto.NewSessionListener +import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestState import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest +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.crosssigning.DeviceTrustLevel import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo @@ -36,6 +45,7 @@ import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody import im.vector.matrix.android.internal.crypto.model.toEntity import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo +import im.vector.matrix.android.internal.crypto.store.SavedKeyBackupKeyInfo import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntity import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMapper @@ -44,16 +54,17 @@ import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntity import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntity import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntityFields -import im.vector.matrix.android.internal.crypto.store.db.model.IncomingRoomKeyRequestEntity -import im.vector.matrix.android.internal.crypto.store.db.model.IncomingRoomKeyRequestEntityFields +import im.vector.matrix.android.internal.crypto.store.db.model.GossipingEventEntity +import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntity +import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields 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.OutgoingRoomKeyRequestEntity -import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingRoomKeyRequestEntityFields +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.TrustLevelEntity import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields @@ -62,7 +73,9 @@ 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 import im.vector.matrix.android.internal.crypto.store.db.query.getOrCreate +import im.vector.matrix.android.internal.database.mapper.ContentMapper import im.vector.matrix.android.internal.di.CryptoDatabase +import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.session.SessionScope import io.realm.Realm import io.realm.RealmConfiguration @@ -110,27 +123,7 @@ internal class RealmCryptoStore @Inject constructor( .setRealmConfiguration(realmConfiguration) .build() - /* ========================================================================================== - * Other data - * ========================================================================================== */ - - override fun hasData(): Boolean { - return doWithRealm(realmConfiguration) { - !it.isEmpty - // Check if there is a MetaData object - && it.where().count() > 0 - } - } - - override fun deleteStore() { - doRealmTransaction(realmConfiguration) { - it.deleteAll() - } - } - - override fun open() { - realmLocker = Realm.getInstance(realmConfiguration) - + init { // Ensure CryptoMetadataEntity is inserted in DB doRealmTransaction(realmConfiguration) { realm -> var currentMetadata = realm.where().findFirst() @@ -161,6 +154,27 @@ internal class RealmCryptoStore @Inject constructor( } } } + /* ========================================================================================== + * Other data + * ========================================================================================== */ + + override fun hasData(): Boolean { + return doWithRealm(realmConfiguration) { + !it.isEmpty + // Check if there is a MetaData object + && it.where().count() > 0 + } + } + + override fun deleteStore() { + doRealmTransaction(realmConfiguration) { + it.deleteAll() + } + } + + override fun open() { + realmLocker = Realm.getInstance(realmConfiguration) + } override fun close() { olmSessionsToRelease.forEach { @@ -191,20 +205,31 @@ internal class RealmCryptoStore @Inject constructor( }?.deviceId ?: "" } - override fun storeAccount(account: OlmAccount) { - olmAccount = account - + override fun saveOlmAccount() { doRealmTransaction(realmConfiguration) { - it.where().findFirst()?.putOlmAccount(account) + it.where().findFirst()?.putOlmAccount(olmAccount) } } - override fun getAccount(): OlmAccount? { - if (olmAccount == null) { - olmAccount = doRealmQueryAndCopy(realmConfiguration) { it.where().findFirst() }?.getOlmAccount() - } + override fun getOlmAccount(): OlmAccount { + return olmAccount!! + } - return olmAccount + override fun getOrCreateOlmAccount(): OlmAccount { + doRealmTransaction(realmConfiguration) { + val metaData = it.where().findFirst() + val existing = metaData!!.getOlmAccount() + if (existing == null) { + Timber.d("## Crypto Creating olm account") + val created = OlmAccount() + metaData.putOlmAccount(created) + olmAccount = created + } else { + Timber.d("## Crypto Access existing account") + olmAccount = existing + } + } + return olmAccount!! } override fun storeUserDevice(userId: String?, deviceInfo: CryptoDeviceInfo?) { @@ -359,7 +384,46 @@ internal class RealmCryptoStore @Inject constructor( doRealmTransaction(realmConfiguration) { realm -> realm.where().findFirst()?.apply { xSignMasterPrivateKey = msk + xSignUserPrivateKey = usk xSignSelfSignedPrivateKey = ssk + } + } + } + + override fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where().findFirst()?.apply { + keyBackupRecoveryKey = recoveryKey + keyBackupRecoveryKeyVersion = version + } + } + } + + override fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? { + return doRealmQueryAndCopy(realmConfiguration) { realm -> + realm.where().findFirst() + }?.let { + val key = it.keyBackupRecoveryKey + val version = it.keyBackupRecoveryKeyVersion + if (!key.isNullOrBlank() && !version.isNullOrBlank()) { + SavedKeyBackupKeyInfo(recoveryKey = key, version = version) + } else { + null + } + } + } + + override fun storeSSKPrivateKey(ssk: String?) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where().findFirst()?.apply { + xSignSelfSignedPrivateKey = ssk + } + } + } + + override fun storeUSKPrivateKey(usk: String?) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where().findFirst()?.apply { xSignUserPrivateKey = usk } } @@ -797,131 +861,328 @@ internal class RealmCryptoStore @Inject constructor( } override fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingRoomKeyRequest? { - return doRealmQueryAndCopy(realmConfiguration) { - it.where() - .equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_BODY_ALGORITHM, requestBody.algorithm) - .equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_BODY_ROOM_ID, requestBody.roomId) - .equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_BODY_SENDER_KEY, requestBody.senderKey) - .equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_BODY_SESSION_ID, requestBody.sessionId) - .findFirst() + return monarchy.fetchAllCopiedSync { realm -> + realm.where() + .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + }.mapNotNull { + it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest + }.firstOrNull { + it.requestBody?.algorithm == requestBody.algorithm + it.requestBody?.roomId == requestBody.roomId + it.requestBody?.senderKey == requestBody.senderKey + it.requestBody?.sessionId == requestBody.sessionId } - ?.toOutgoingRoomKeyRequest() } - override fun getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): OutgoingRoomKeyRequest? { - if (request.requestBody == null) { - return null - } + override fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest? { + return monarchy.fetchAllCopiedSync { realm -> + realm.where() + .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name) + .equalTo(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, secretName) + }.mapNotNull { + it.toOutgoingGossipingRequest() as? OutgoingSecretRequest + }.firstOrNull() + } - val existingOne = getOutgoingRoomKeyRequest(request.requestBody!!) - - if (existingOne != null) { - return existingOne + override fun getIncomingRoomKeyRequests(): List { + return monarchy.fetchAllCopiedSync { realm -> + realm.where() + .equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + }.mapNotNull { + it.toIncomingGossipingRequest() as? IncomingRoomKeyRequest } + } + + override fun getGossipingEventsTrail(): List { + return monarchy.fetchAllCopiedSync { realm -> + realm.where() + }.map { + it.toModel() + } + } + + override fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map>): OutgoingRoomKeyRequest? { + // Insert the request and return the one passed in parameter + var request: OutgoingRoomKeyRequest? = null + doRealmTransaction(realmConfiguration) { realm -> + + val existing = realm.where() + .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + .findAll() + .mapNotNull { + it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest + }.firstOrNull { + it.requestBody?.algorithm == requestBody.algorithm + && it.requestBody?.sessionId == requestBody.sessionId + && it.requestBody?.senderKey == requestBody.senderKey + && it.requestBody?.roomId == requestBody.roomId + } + + if (existing == null) { + request = realm.createObject(OutgoingGossipingRequestEntity::class.java).apply { + this.requestId = LocalEcho.createLocalEchoId() + this.setRecipients(recipients) + this.requestState = OutgoingGossipingRequestState.UNSENT + this.type = GossipRequestType.KEY + this.requestedInfoStr = requestBody.toJson() + }.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest + } else { + request = existing + } + } + return request + } + + override fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map>): OutgoingSecretRequest? { + var request: OutgoingSecretRequest? = null // Insert the request and return the one passed in parameter - doRealmTransaction(realmConfiguration) { - it.createObject(OutgoingRoomKeyRequestEntity::class.java, request.requestId).apply { - putRequestBody(request.requestBody) - putRecipients(request.recipients) - cancellationTxnId = request.cancellationTxnId - state = request.state.ordinal + doRealmTransaction(realmConfiguration) { realm -> + val existing = realm.where() + .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name) + .equalTo(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, secretName) + .findAll() + .mapNotNull { + it.toOutgoingGossipingRequest() as? OutgoingSecretRequest + }.firstOrNull() + if (existing == null) { + request = realm.createObject(OutgoingGossipingRequestEntity::class.java).apply { + this.type = GossipRequestType.SECRET + setRecipients(recipients) + this.requestState = OutgoingGossipingRequestState.UNSENT + this.requestId = LocalEcho.createLocalEchoId() + this.requestedInfoStr = secretName + }.toOutgoingGossipingRequest() as? OutgoingSecretRequest + } else { + request = existing } } return request } - override fun getOutgoingRoomKeyRequestByState(states: Set): OutgoingRoomKeyRequest? { - val statesIndex = states.map { it.ordinal }.toTypedArray() - return doRealmQueryAndCopy(realmConfiguration) { - it.where() - .`in`(OutgoingRoomKeyRequestEntityFields.STATE, statesIndex) - .findFirst() + override fun saveGossipingEvent(event: Event) { + val now = System.currentTimeMillis() + val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now + val entity = GossipingEventEntity( + type = event.type, + sender = event.senderId, + ageLocalTs = ageLocalTs, + content = ContentMapper.map(event.content) + ).apply { + sendState = SendState.SYNCED + decryptionResultJson = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult) + decryptionErrorCode = event.mCryptoError?.name } - ?.toOutgoingRoomKeyRequest() - } - - override fun updateOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest) { - doRealmTransaction(realmConfiguration) { - val obj = OutgoingRoomKeyRequestEntity().apply { - requestId = request.requestId - cancellationTxnId = request.cancellationTxnId - state = request.state.ordinal - putRecipients(request.recipients) - putRequestBody(request.requestBody) - } - - it.insertOrUpdate(obj) + doRealmTransaction(realmConfiguration) { realm -> + realm.insertOrUpdate(entity) } } - override fun deleteOutgoingRoomKeyRequest(transactionId: String) { - doRealmTransaction(realmConfiguration) { - it.where() - .equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_ID, transactionId) - .findFirst() - ?.deleteFromRealm() +// override fun getOutgoingRoomKeyRequestByState(states: Set): OutgoingRoomKeyRequest? { +// val statesIndex = states.map { it.ordinal }.toTypedArray() +// return doRealmQueryAndCopy(realmConfiguration) { realm -> +// realm.where() +// .equalTo(GossipingEventEntityFields.SENDER, credentials.userId) +// .findAll() +// .filter {entity -> +// states.any { it == entity.requestState} +// } +// }.mapNotNull { +// ContentMapper.map(it.content)?.toModel() +// } +// ?.toOutgoingRoomKeyRequest() +// } +// +// override fun getOutgoingSecretShareRequestByState(states: Set): OutgoingSecretRequest? { +// val statesIndex = states.map { it.ordinal }.toTypedArray() +// return doRealmQueryAndCopy(realmConfiguration) { +// it.where() +// .`in`(OutgoingSecretRequestEntityFields.STATE, statesIndex) +// .findFirst() +// } +// ?.toOutgoingSecretRequest() +// } + +// override fun updateOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest) { +// doRealmTransaction(realmConfiguration) { +// val obj = OutgoingRoomKeyRequestEntity().apply { +// requestId = request.requestId +// cancellationTxnId = request.cancellationTxnId +// state = request.state.ordinal +// putRecipients(request.recipients) +// putRequestBody(request.requestBody) +// } +// +// it.insertOrUpdate(obj) +// } +// } + +// override fun deleteOutgoingRoomKeyRequest(transactionId: String) { +// doRealmTransaction(realmConfiguration) { +// it.where() +// .equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_ID, transactionId) +// .findFirst() +// ?.deleteFromRealm() +// } +// } + +// override fun storeIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequest?) { +// if (incomingRoomKeyRequest == null) { +// return +// } +// +// doRealmTransaction(realmConfiguration) { +// // Delete any previous store request with the same parameters +// it.where() +// .equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, incomingRoomKeyRequest.userId) +// .equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, incomingRoomKeyRequest.deviceId) +// .equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, incomingRoomKeyRequest.requestId) +// .findAll() +// .deleteAllFromRealm() +// +// // Then store it +// it.createObject(IncomingRoomKeyRequestEntity::class.java).apply { +// userId = incomingRoomKeyRequest.userId +// deviceId = incomingRoomKeyRequest.deviceId +// requestId = incomingRoomKeyRequest.requestId +// putRequestBody(incomingRoomKeyRequest.requestBody) +// } +// } +// } + +// override fun deleteIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingShareRequestCommon) { +// doRealmTransaction(realmConfiguration) { +// it.where() +// .equalTo(GossipingEventEntityFields.TYPE, EventType.ROOM_KEY_REQUEST) +// .notEqualTo(GossipingEventEntityFields.SENDER, credentials.userId) +// .findAll() +// .filter { +// ContentMapper.map(it.content).toModel()?.let { +// +// } +// } +// // .equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, incomingRoomKeyRequest.userId) +// // .equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, incomingRoomKeyRequest.deviceId) +// // .equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, incomingRoomKeyRequest.requestId) +// // .findAll() +// // .deleteAllFromRealm() +// } +// } + + override fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where() + .equalTo(IncomingGossipingRequestEntityFields.OTHER_USER_ID, request.userId) + .equalTo(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, request.deviceId) + .equalTo(IncomingGossipingRequestEntityFields.REQUEST_ID, request.requestId) + .findAll().forEach { + it.requestState = state + } } } - override fun storeIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequest?) { - if (incomingRoomKeyRequest == null) { - return - } - - doRealmTransaction(realmConfiguration) { - // Delete any previous store request with the same parameters - it.where() - .equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, incomingRoomKeyRequest.userId) - .equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, incomingRoomKeyRequest.deviceId) - .equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, incomingRoomKeyRequest.requestId) - .findAll() - .deleteAllFromRealm() - - // Then store it - it.createObject(IncomingRoomKeyRequestEntity::class.java).apply { - userId = incomingRoomKeyRequest.userId - deviceId = incomingRoomKeyRequest.deviceId - requestId = incomingRoomKeyRequest.requestId - putRequestBody(incomingRoomKeyRequest.requestBody) - } - } - } - - override fun deleteIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequestCommon) { - doRealmTransaction(realmConfiguration) { - it.where() - .equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, incomingRoomKeyRequest.userId) - .equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, incomingRoomKeyRequest.deviceId) - .equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, incomingRoomKeyRequest.requestId) - .findAll() - .deleteAllFromRealm() + override fun updateOutgoingGossipingRequestState(requestId: String, state: OutgoingGossipingRequestState) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where() + .equalTo(OutgoingGossipingRequestEntityFields.REQUEST_ID, requestId) + .findAll().forEach { + it.requestState = state + } } } override fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest? { - return doRealmQueryAndCopy(realmConfiguration) { - it.where() - .equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, userId) - .equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, deviceId) - .equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, requestId) - .findFirst() - } - ?.toIncomingRoomKeyRequest() + return doRealmQueryAndCopyList(realmConfiguration) { realm -> + realm.where() + .equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + .equalTo(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, deviceId) + .equalTo(IncomingGossipingRequestEntityFields.OTHER_USER_ID, userId) + .findAll() + }.mapNotNull { entity -> + entity.toIncomingGossipingRequest() as? IncomingRoomKeyRequest + }.firstOrNull() } - override fun getPendingIncomingRoomKeyRequests(): MutableList { + override fun getPendingIncomingRoomKeyRequests(): List { return doRealmQueryAndCopyList(realmConfiguration) { - it.where() + it.where() + .equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + .equalTo(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, GossipingRequestState.PENDING.name) .findAll() } - .map { - it.toIncomingRoomKeyRequest() + .map { entity -> + IncomingRoomKeyRequest( + userId = entity.otherUserId, + deviceId = entity.otherDeviceId, + requestId = entity.requestId, + requestBody = entity.getRequestedKeyInfo(), + localCreationTimestamp = entity.localCreationTimestamp + ) } - .toMutableList() } + override fun getPendingIncomingGossipingRequests(): List { + return doRealmQueryAndCopyList(realmConfiguration) { + it.where() + .equalTo(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, GossipingRequestState.PENDING.name) + .findAll() + } + .mapNotNull { entity -> + when (entity.type) { + GossipRequestType.KEY -> { + IncomingRoomKeyRequest( + userId = entity.otherUserId, + deviceId = entity.otherDeviceId, + requestId = entity.requestId, + requestBody = entity.getRequestedKeyInfo(), + localCreationTimestamp = entity.localCreationTimestamp + ) + } + GossipRequestType.SECRET -> { + IncomingSecretShareRequest( + userId = entity.otherUserId, + deviceId = entity.otherDeviceId, + requestId = entity.requestId, + secretName = entity.getRequestedSecretName(), + localCreationTimestamp = entity.localCreationTimestamp + ) + } + } + } + } + + override fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?) { + doRealmTransactionAsync(realmConfiguration) { realm -> + + // After a clear cache, we might have a + + realm.createObject(IncomingGossipingRequestEntity::class.java).let { + it.otherDeviceId = request.deviceId + it.otherUserId = request.userId + it.requestId = request.requestId ?: "" + it.requestState = GossipingRequestState.PENDING + it.localCreationTimestamp = ageLocalTS ?: System.currentTimeMillis() + if (request is IncomingSecretShareRequest) { + it.type = GossipRequestType.SECRET + it.requestedInfoStr = request.secretName + } else if (request is IncomingRoomKeyRequest) { + it.type = GossipRequestType.KEY + it.requestedInfoStr = request.requestBody?.toJson() + } + } + } + } + +// override fun getPendingIncomingSecretShareRequests(): List { +// return doRealmQueryAndCopyList(realmConfiguration) { +// it.where() +// .findAll() +// }.map { +// it.toIncomingSecretShareRequest() +// } +// } + /* ========================================================================================== * Cross Signing * ========================================================================================== */ @@ -1024,6 +1285,28 @@ internal class RealmCryptoStore @Inject constructor( } } + override fun getOutgoingRoomKeyRequests(): List { + return monarchy.fetchAllMappedSync({ realm -> + realm + .where(OutgoingGossipingRequestEntity::class.java) + .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + }, { entity -> + entity.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest + }) + .filterNotNull() + } + + override fun getOutgoingSecretKeyRequests(): List { + return monarchy.fetchAllMappedSync({ realm -> + realm + .where(OutgoingGossipingRequestEntity::class.java) + .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name) + }, { entity -> + entity.toOutgoingGossipingRequest() as? OutgoingSecretRequest + }) + .filterNotNull() + } + override fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? { return doRealmQueryAndCopy(realmConfiguration) { realm -> realm.where(CrossSigningInfoEntity::class.java) 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 6839f6995b..d5972b5686 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 @@ -23,7 +23,10 @@ import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMetadataEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntityFields +import im.vector.matrix.android.internal.crypto.store.db.model.GossipingEventEntityFields +import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntityFields +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.di.SerializeNulls @@ -34,67 +37,73 @@ import timber.log.Timber internal object RealmCryptoStoreMigration : RealmMigration { // Version 1L added Cross Signing info persistence - const val CRYPTO_STORE_SCHEMA_VERSION = 1L + const val CRYPTO_STORE_SCHEMA_VERSION = 3L override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { Timber.v("Migrating Realm Crypto from $oldVersion to $newVersion") - if (oldVersion <= 0) { - Timber.d("Step 0 -> 1") - Timber.d("Create KeyInfoEntity") + if (oldVersion <= 0) migrateTo1(realm) + if (oldVersion <= 1) migrateTo2(realm) + if (oldVersion <= 2) migrateTo3(realm) + } - val trustLevelentityEntitySchema = realm.schema.create("TrustLevelEntity") - .addField(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, Boolean::class.java) - .setNullable(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, true) - .addField(TrustLevelEntityFields.LOCALLY_VERIFIED, Boolean::class.java) - .setNullable(TrustLevelEntityFields.LOCALLY_VERIFIED, true) + private fun migrateTo1(realm: DynamicRealm) { + Timber.d("Step 0 -> 1") + Timber.d("Create KeyInfoEntity") - val keyInfoEntitySchema = realm.schema.create("KeyInfoEntity") - .addField(KeyInfoEntityFields.PUBLIC_KEY_BASE64, String::class.java) - .addField(KeyInfoEntityFields.SIGNATURES, String::class.java) - .addRealmListField(KeyInfoEntityFields.USAGES.`$`, String::class.java) - .addRealmObjectField(KeyInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevelentityEntitySchema) + val trustLevelentityEntitySchema = realm.schema.create("TrustLevelEntity") + .addField(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, Boolean::class.java) + .setNullable(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, true) + .addField(TrustLevelEntityFields.LOCALLY_VERIFIED, Boolean::class.java) + .setNullable(TrustLevelEntityFields.LOCALLY_VERIFIED, true) - Timber.d("Create CrossSigningInfoEntity") + val keyInfoEntitySchema = realm.schema.create("KeyInfoEntity") + .addField(KeyInfoEntityFields.PUBLIC_KEY_BASE64, String::class.java) + .addField(KeyInfoEntityFields.SIGNATURES, String::class.java) + .addRealmListField(KeyInfoEntityFields.USAGES.`$`, String::class.java) + .addRealmObjectField(KeyInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevelentityEntitySchema) - val crossSigningInfoSchema = realm.schema.create("CrossSigningInfoEntity") - .addField(CrossSigningInfoEntityFields.USER_ID, String::class.java) - .addPrimaryKey(CrossSigningInfoEntityFields.USER_ID) - .addRealmListField(CrossSigningInfoEntityFields.CROSS_SIGNING_KEYS.`$`, keyInfoEntitySchema) + Timber.d("Create CrossSigningInfoEntity") - Timber.d("Updating UserEntity table") - realm.schema.get("UserEntity") - ?.addRealmObjectField(UserEntityFields.CROSS_SIGNING_INFO_ENTITY.`$`, crossSigningInfoSchema) + val crossSigningInfoSchema = realm.schema.create("CrossSigningInfoEntity") + .addField(CrossSigningInfoEntityFields.USER_ID, String::class.java) + .addPrimaryKey(CrossSigningInfoEntityFields.USER_ID) + .addRealmListField(CrossSigningInfoEntityFields.CROSS_SIGNING_KEYS.`$`, keyInfoEntitySchema) - Timber.d("Updating CryptoMetadataEntity table") - realm.schema.get("CryptoMetadataEntity") - ?.addField(CryptoMetadataEntityFields.X_SIGN_MASTER_PRIVATE_KEY, String::class.java) - ?.addField(CryptoMetadataEntityFields.X_SIGN_USER_PRIVATE_KEY, String::class.java) - ?.addField(CryptoMetadataEntityFields.X_SIGN_SELF_SIGNED_PRIVATE_KEY, String::class.java) + Timber.d("Updating UserEntity table") + realm.schema.get("UserEntity") + ?.addRealmObjectField(UserEntityFields.CROSS_SIGNING_INFO_ENTITY.`$`, crossSigningInfoSchema) - val moshi = Moshi.Builder().add(SerializeNulls.JSON_ADAPTER_FACTORY).build() - val listMigrationAdapter = moshi.adapter>(Types.newParameterizedType( - List::class.java, - String::class.java, - Any::class.java - )) - val mapMigrationAdapter = moshi.adapter(Types.newParameterizedType( - Map::class.java, - String::class.java, - Any::class.java - )) + Timber.d("Updating CryptoMetadataEntity table") + realm.schema.get("CryptoMetadataEntity") + ?.addField(CryptoMetadataEntityFields.X_SIGN_MASTER_PRIVATE_KEY, String::class.java) + ?.addField(CryptoMetadataEntityFields.X_SIGN_USER_PRIVATE_KEY, String::class.java) + ?.addField(CryptoMetadataEntityFields.X_SIGN_SELF_SIGNED_PRIVATE_KEY, String::class.java) - realm.schema.get("DeviceInfoEntity") - ?.addField(DeviceInfoEntityFields.USER_ID, String::class.java) - ?.addField(DeviceInfoEntityFields.ALGORITHM_LIST_JSON, String::class.java) - ?.addField(DeviceInfoEntityFields.KEYS_MAP_JSON, String::class.java) - ?.addField(DeviceInfoEntityFields.SIGNATURE_MAP_JSON, String::class.java) - ?.addField(DeviceInfoEntityFields.UNSIGNED_MAP_JSON, String::class.java) - ?.addField(DeviceInfoEntityFields.IS_BLOCKED, Boolean::class.java) - ?.setNullable(DeviceInfoEntityFields.IS_BLOCKED, true) - ?.addRealmObjectField(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevelentityEntitySchema) - ?.transform { obj -> + val moshi = Moshi.Builder().add(SerializeNulls.JSON_ADAPTER_FACTORY).build() + val listMigrationAdapter = moshi.adapter>(Types.newParameterizedType( + List::class.java, + String::class.java, + Any::class.java + )) + val mapMigrationAdapter = moshi.adapter(Types.newParameterizedType( + Map::class.java, + String::class.java, + Any::class.java + )) + realm.schema.get("DeviceInfoEntity") + ?.addField(DeviceInfoEntityFields.USER_ID, String::class.java) + ?.addField(DeviceInfoEntityFields.ALGORITHM_LIST_JSON, String::class.java) + ?.addField(DeviceInfoEntityFields.KEYS_MAP_JSON, String::class.java) + ?.addField(DeviceInfoEntityFields.SIGNATURE_MAP_JSON, String::class.java) + ?.addField(DeviceInfoEntityFields.UNSIGNED_MAP_JSON, String::class.java) + ?.addField(DeviceInfoEntityFields.IS_BLOCKED, Boolean::class.java) + ?.setNullable(DeviceInfoEntityFields.IS_BLOCKED, true) + ?.addRealmObjectField(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevelentityEntitySchema) + ?.transform { obj -> + + try { val oldSerializedData = obj.getString("deviceInfoData") deserializeFromRealm(oldSerializedData)?.let { oldDevice -> @@ -128,8 +137,60 @@ internal object RealmCryptoStoreMigration : RealmMigration { obj.setString(DeviceInfoEntityFields.SIGNATURE_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.signatures)) obj.setString(DeviceInfoEntityFields.UNSIGNED_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.unsigned)) } + } catch (failure: Throwable) { + Timber.w(failure, "Crypto Data base migration error") + // an unfortunate refactor did modify that class, making deserialization failing + // so we just skip and ignore.. } - ?.removeField("deviceInfoData") - } + } + ?.removeField("deviceInfoData") + } + + private fun migrateTo2(realm: DynamicRealm) { + Timber.d("Step 1 -> 2") + realm.schema.remove("OutgoingRoomKeyRequestEntity") + realm.schema.remove("IncomingRoomKeyRequestEntity") + + // Not need to migrate existing request, just start fresh? + + realm.schema.create("GossipingEventEntity") + .addField(GossipingEventEntityFields.TYPE, String::class.java) + .addIndex(GossipingEventEntityFields.TYPE) + .addField(GossipingEventEntityFields.CONTENT, String::class.java) + .addField(GossipingEventEntityFields.SENDER, String::class.java) + .addIndex(GossipingEventEntityFields.SENDER) + .addField(GossipingEventEntityFields.DECRYPTION_RESULT_JSON, String::class.java) + .addField(GossipingEventEntityFields.DECRYPTION_ERROR_CODE, String::class.java) + .addField(GossipingEventEntityFields.AGE_LOCAL_TS, Long::class.java) + .setNullable(GossipingEventEntityFields.AGE_LOCAL_TS, true) + .addField(GossipingEventEntityFields.SEND_STATE_STR, String::class.java) + + realm.schema.create("IncomingGossipingRequestEntity") + .addField(IncomingGossipingRequestEntityFields.REQUEST_ID, String::class.java) + .addIndex(IncomingGossipingRequestEntityFields.REQUEST_ID) + .addField(IncomingGossipingRequestEntityFields.TYPE_STR, String::class.java) + .addIndex(IncomingGossipingRequestEntityFields.TYPE_STR) + .addField(IncomingGossipingRequestEntityFields.OTHER_USER_ID, String::class.java) + .addField(IncomingGossipingRequestEntityFields.REQUESTED_INFO_STR, String::class.java) + .addField(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, String::class.java) + .addField(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, String::class.java) + .addField(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, Long::class.java) + .setNullable(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, true) + + realm.schema.create("OutgoingGossipingRequestEntity") + .addField(OutgoingGossipingRequestEntityFields.REQUEST_ID, String::class.java) + .addIndex(OutgoingGossipingRequestEntityFields.REQUEST_ID) + .addField(OutgoingGossipingRequestEntityFields.RECIPIENTS_DATA, String::class.java) + .addField(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, String::class.java) + .addField(OutgoingGossipingRequestEntityFields.TYPE_STR, String::class.java) + .addIndex(OutgoingGossipingRequestEntityFields.TYPE_STR) + .addField(OutgoingGossipingRequestEntityFields.REQUEST_STATE_STR, String::class.java) + } + + private fun migrateTo3(realm: DynamicRealm) { + Timber.d("Updating CryptoMetadataEntity table") + realm.schema.get("CryptoMetadataEntity") + ?.addField(CryptoMetadataEntityFields.KEY_BACKUP_RECOVERY_KEY, String::class.java) + ?.addField(CryptoMetadataEntityFields.KEY_BACKUP_RECOVERY_KEY_VERSION, 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 1053cc5f43..3da91c6268 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 @@ -20,12 +20,13 @@ import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoE import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMetadataEntity import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntity import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntity -import im.vector.matrix.android.internal.crypto.store.db.model.IncomingRoomKeyRequestEntity +import im.vector.matrix.android.internal.crypto.store.db.model.GossipingEventEntity +import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntity import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity 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.OutgoingRoomKeyRequestEntity +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 io.realm.annotations.RealmModule @@ -38,14 +39,15 @@ import io.realm.annotations.RealmModule CryptoMetadataEntity::class, CryptoRoomEntity::class, DeviceInfoEntity::class, - IncomingRoomKeyRequestEntity::class, KeysBackupDataEntity::class, OlmInboundGroupSessionEntity::class, OlmSessionEntity::class, - OutgoingRoomKeyRequestEntity::class, UserEntity::class, KeyInfoEntity::class, CrossSigningInfoEntity::class, - TrustLevelEntity::class + TrustLevelEntity::class, + GossipingEventEntity::class, + IncomingGossipingRequestEntity::class, + OutgoingGossipingRequestEntity::class ]) internal class RealmCryptoStoreModule diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CryptoMetadataEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CryptoMetadataEntity.kt index 8a2c2914da..2d4706ba76 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CryptoMetadataEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CryptoMetadataEntity.kt @@ -38,7 +38,9 @@ internal open class CryptoMetadataEntity( var xSignMasterPrivateKey: String? = null, var xSignUserPrivateKey: String? = null, - var xSignSelfSignedPrivateKey: String? = null + var xSignSelfSignedPrivateKey: String? = null, + var keyBackupRecoveryKey: String? = null, + var keyBackupRecoveryKeyVersion: String? = null // var crossSigningInfoEntity: CrossSigningInfoEntity? = null ) : RealmObject() { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/GossipingEventEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/GossipingEventEntity.kt new file mode 100644 index 0000000000..131ddfafc6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/GossipingEventEntity.kt @@ -0,0 +1,88 @@ +/* + * 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 com.squareup.moshi.JsonDataException +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.room.send.SendState +import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult +import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult +import im.vector.matrix.android.internal.database.mapper.ContentMapper +import im.vector.matrix.android.internal.di.MoshiProvider +import io.realm.RealmObject +import io.realm.annotations.Index +import timber.log.Timber + +/** + * Keep track of gossiping event received in toDevice messages + * (room key request, or sss secret sharing, as well as cancellations) + * + */ +internal open class GossipingEventEntity(@Index var type: String? = "", + var content: String? = null, + @Index var sender: String? = null, + var decryptionResultJson: String? = null, + var decryptionErrorCode: String? = null, + var ageLocalTs: Long? = null) : RealmObject() { + + private var sendStateStr: String = SendState.UNKNOWN.name + + var sendState: SendState + get() { + return SendState.valueOf(sendStateStr) + } + set(value) { + sendStateStr = value.name + } + + companion object + + fun setDecryptionResult(result: MXEventDecryptionResult) { + val decryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) + val adapter = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java) + decryptionResultJson = adapter.toJson(decryptionResult) + decryptionErrorCode = null + } + + fun toModel(): Event { + return Event( + type = this.type ?: "", + content = ContentMapper.map(this.content), + senderId = this.sender + ).also { + it.ageLocalTs = this.ageLocalTs + it.sendState = this.sendState + this.decryptionResultJson?.let { json -> + try { + it.mxDecryptionResult = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).fromJson(json) + } catch (t: JsonDataException) { + Timber.e(t, "Failed to parse decryption result") + } + } + // TODO get the full crypto error object + it.mCryptoError = this.decryptionErrorCode?.let { errorCode -> + MXCryptoError.ErrorType.valueOf(errorCode) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt new file mode 100644 index 0000000000..bb7d497a0e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt @@ -0,0 +1,89 @@ +/* + * 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.api.extensions.tryThis +import im.vector.matrix.android.internal.crypto.GossipRequestType +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.model.rest.RoomKeyRequestBody +import io.realm.RealmObject +import io.realm.annotations.Index + +internal open class IncomingGossipingRequestEntity(@Index var requestId: String? = "", + @Index var typeStr: String? = null, + var otherUserId: String? = null, + var requestedInfoStr: String? = null, + var otherDeviceId: String? = null, + var localCreationTimestamp: Long? = null +) : RealmObject() { + + fun getRequestedSecretName(): String? = if (type == GossipRequestType.SECRET) { + requestedInfoStr + } else null + + fun getRequestedKeyInfo(): RoomKeyRequestBody? = if (type == GossipRequestType.KEY) { + RoomKeyRequestBody.fromJson(requestedInfoStr) + } else null + + var type: GossipRequestType + get() { + return tryThis { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY + } + set(value) { + typeStr = value.name + } + + private var requestStateStr: String = GossipingRequestState.NONE.name + + var requestState: GossipingRequestState + get() { + return tryThis { GossipingRequestState.valueOf(requestStateStr) } + ?: GossipingRequestState.NONE + } + set(value) { + requestStateStr = value.name + } + + companion object + + fun toIncomingGossipingRequest(): IncomingShareRequestCommon { + return when (type) { + GossipRequestType.KEY -> { + IncomingRoomKeyRequest( + requestBody = getRequestedKeyInfo(), + deviceId = otherDeviceId, + userId = otherUserId, + requestId = requestId, + state = requestState, + localCreationTimestamp = localCreationTimestamp + ) + } + GossipRequestType.SECRET -> { + IncomingSecretShareRequest( + secretName = getRequestedSecretName(), + deviceId = otherDeviceId, + userId = otherUserId, + requestId = requestId, + localCreationTimestamp = localCreationTimestamp + ) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/IncomingRoomKeyRequestEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/IncomingRoomKeyRequestEntity.kt deleted file mode 100644 index 38cece99ac..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/IncomingRoomKeyRequestEntity.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2018 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.matrix.android.internal.crypto.store.db.model - -import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest -import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody -import io.realm.RealmObject - -internal open class IncomingRoomKeyRequestEntity( - var requestId: String? = null, - var userId: String? = null, - var deviceId: String? = null, - // RoomKeyRequestBody fields - var requestBodyAlgorithm: String? = null, - var requestBodyRoomId: String? = null, - var requestBodySenderKey: String? = null, - var requestBodySessionId: String? = null -) : RealmObject() { - - fun toIncomingRoomKeyRequest(): IncomingRoomKeyRequest { - return IncomingRoomKeyRequest( - requestId = requestId, - userId = userId, - deviceId = deviceId, - requestBody = RoomKeyRequestBody( - algorithm = requestBodyAlgorithm, - roomId = requestBodyRoomId, - senderKey = requestBodySenderKey, - sessionId = requestBodySessionId - ) - ) - } - - fun putRequestBody(requestBody: RoomKeyRequestBody?) { - requestBody?.let { - requestBodyAlgorithm = it.algorithm - requestBodyRoomId = it.roomId - requestBodySenderKey = it.senderKey - requestBodySessionId = it.sessionId - } - } -} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt new file mode 100644 index 0000000000..19049c099c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt @@ -0,0 +1,104 @@ +/* + * 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 com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Types +import im.vector.matrix.android.api.extensions.tryThis +import im.vector.matrix.android.internal.crypto.GossipRequestType +import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequest +import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestState +import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest +import im.vector.matrix.android.internal.crypto.OutgoingSecretRequest +import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody +import im.vector.matrix.android.internal.di.MoshiProvider +import io.realm.RealmObject +import io.realm.annotations.Index + +internal open class OutgoingGossipingRequestEntity( + @Index var requestId: String? = null, + var recipientsData: String? = null, + var requestedInfoStr: String? = null, + @Index var typeStr: String? = null +) : RealmObject() { + + fun getRequestedSecretName(): String? = if (type == GossipRequestType.SECRET) { + requestedInfoStr + } else null + + fun getRequestedKeyInfo(): RoomKeyRequestBody? = if (type == GossipRequestType.KEY) { + RoomKeyRequestBody.fromJson(requestedInfoStr) + } else null + + var type: GossipRequestType + get() { + return tryThis { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY + } + set(value) { + typeStr = value.name + } + + private var requestStateStr: String = OutgoingGossipingRequestState.UNSENT.name + + var requestState: OutgoingGossipingRequestState + get() { + return tryThis { OutgoingGossipingRequestState.valueOf(requestStateStr) } + ?: OutgoingGossipingRequestState.UNSENT + } + set(value) { + requestStateStr = value.name + } + + companion object { + + private val recipientsDataMapper: JsonAdapter>> = + MoshiProvider + .providesMoshi() + .adapter>>( + Types.newParameterizedType(Map::class.java, String::class.java, List::class.java) + ) + } + + fun toOutgoingGossipingRequest(): OutgoingGossipingRequest { + return when (type) { + GossipRequestType.KEY -> { + OutgoingRoomKeyRequest( + requestBody = getRequestedKeyInfo(), + recipients = getRecipients() ?: emptyMap(), + requestId = requestId ?: "", + state = requestState + ) + } + GossipRequestType.SECRET -> { + OutgoingSecretRequest( + secretName = getRequestedSecretName(), + recipients = getRecipients() ?: emptyMap(), + requestId = requestId ?: "", + state = requestState + ) + } + } + } + + private fun getRecipients(): Map>? { + return this.recipientsData?.let { recipientsDataMapper.fromJson(it) } + } + + fun setRecipients(recipients: Map>) { + this.recipientsData = recipientsDataMapper.toJson(recipients) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingRoomKeyRequestEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingRoomKeyRequestEntity.kt deleted file mode 100644 index 86fc177f2b..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingRoomKeyRequestEntity.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2018 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.matrix.android.internal.crypto.store.db.model - -import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest -import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody -import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm -import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm -import io.realm.RealmObject -import io.realm.annotations.PrimaryKey - -internal open class OutgoingRoomKeyRequestEntity( - @PrimaryKey var requestId: String? = null, - var cancellationTxnId: String? = null, - // Serialized Json - var recipientsData: String? = null, - // RoomKeyRequestBody fields - var requestBodyAlgorithm: String? = null, - var requestBodyRoomId: String? = null, - var requestBodySenderKey: String? = null, - var requestBodySessionId: String? = null, - // State - var state: Int = 0 -) : RealmObject() { - - /** - * Convert to OutgoingRoomKeyRequest - */ - fun toOutgoingRoomKeyRequest(): OutgoingRoomKeyRequest { - val cancellationTxnId = this.cancellationTxnId - return OutgoingRoomKeyRequest( - RoomKeyRequestBody( - algorithm = requestBodyAlgorithm, - roomId = requestBodyRoomId, - senderKey = requestBodySenderKey, - sessionId = requestBodySessionId - ), - getRecipients()!!, - requestId!!, - OutgoingRoomKeyRequest.RequestState.from(state) - ).apply { - this.cancellationTxnId = cancellationTxnId - } - } - - private fun getRecipients(): List>? { - return deserializeFromRealm(recipientsData) - } - - fun putRecipients(recipients: List>?) { - recipientsData = serializeForRealm(recipients) - } - - fun putRequestBody(requestBody: RoomKeyRequestBody?) { - requestBody?.let { - requestBodyAlgorithm = it.algorithm - requestBodyRoomId = it.roomId - requestBodySenderKey = it.senderKey - requestBodySessionId = it.sessionId - } - } -} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/InitializeCrossSigningTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/InitializeCrossSigningTask.kt new file mode 100644 index 0000000000..9a7d84e235 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/InitializeCrossSigningTask.kt @@ -0,0 +1,172 @@ +/* + * 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.tasks + +import dagger.Lazy +import im.vector.matrix.android.internal.crypto.MXOlmDevice +import im.vector.matrix.android.internal.crypto.MyDeviceInfoHolder +import im.vector.matrix.android.internal.crypto.crosssigning.canonicalSignable +import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding +import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey +import im.vector.matrix.android.internal.crypto.model.KeyUsage +import im.vector.matrix.android.internal.crypto.model.rest.UploadSignatureQueryBuilder +import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth +import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.task.Task +import im.vector.matrix.android.internal.util.JsonCanonicalizer +import org.matrix.olm.OlmPkSigning +import timber.log.Timber +import javax.inject.Inject + +internal interface InitializeCrossSigningTask : Task { + data class Params( + val authParams: UserPasswordAuth? + ) + + data class Result( + val masterKeyPK: String, + val userKeyPK: String, + val selfSigningKeyPK: String, + val masterKeyInfo: CryptoCrossSigningKey, + val userKeyInfo: CryptoCrossSigningKey, + val selfSignedKeyInfo: CryptoCrossSigningKey + ) +} + +internal class DefaultInitializeCrossSigningTask @Inject constructor( + @UserId private val userId: String, + private val olmDevice: MXOlmDevice, + private val myDeviceInfoHolder: Lazy, + private val uploadSigningKeysTask: UploadSigningKeysTask, + private val uploadSignaturesTask: UploadSignaturesTask +) : InitializeCrossSigningTask { + + override suspend fun execute(params: InitializeCrossSigningTask.Params): InitializeCrossSigningTask.Result { + var masterPkOlm: OlmPkSigning? = null + var userSigningPkOlm: OlmPkSigning? = null + var selfSigningPkOlm: OlmPkSigning? = null + + try { + // ================= + // MASTER KEY + // ================= + + masterPkOlm = OlmPkSigning() + val masterKeyPrivateKey = OlmPkSigning.generateSeed() + val masterPublicKey = masterPkOlm.initWithSeed(masterKeyPrivateKey) + + Timber.v("## CrossSigning - masterPublicKey:$masterPublicKey") + + // ================= + // USER KEY + // ================= + userSigningPkOlm = OlmPkSigning() + val uskPrivateKey = OlmPkSigning.generateSeed() + val uskPublicKey = userSigningPkOlm.initWithSeed(uskPrivateKey) + + Timber.v("## CrossSigning - uskPublicKey:$uskPublicKey") + + // Sign userSigningKey with master + val signedUSK = CryptoCrossSigningKey.Builder(userId, KeyUsage.USER_SIGNING) + .key(uskPublicKey) + .build() + .canonicalSignable() + .let { masterPkOlm.sign(it) } + + // ================= + // SELF SIGNING KEY + // ================= + selfSigningPkOlm = OlmPkSigning() + val sskPrivateKey = OlmPkSigning.generateSeed() + val sskPublicKey = selfSigningPkOlm.initWithSeed(sskPrivateKey) + + Timber.v("## CrossSigning - sskPublicKey:$sskPublicKey") + + // Sign userSigningKey with master + val signedSSK = CryptoCrossSigningKey.Builder(userId, KeyUsage.SELF_SIGNING) + .key(sskPublicKey) + .build() + .canonicalSignable() + .let { masterPkOlm.sign(it) } + + // I need to upload the keys + val mskCrossSigningKeyInfo = CryptoCrossSigningKey.Builder(userId, KeyUsage.MASTER) + .key(masterPublicKey) + .build() + val uploadSigningKeysParams = UploadSigningKeysTask.Params( + masterKey = mskCrossSigningKeyInfo, + userKey = CryptoCrossSigningKey.Builder(userId, KeyUsage.USER_SIGNING) + .key(uskPublicKey) + .signature(userId, masterPublicKey, signedUSK) + .build(), + selfSignedKey = CryptoCrossSigningKey.Builder(userId, KeyUsage.SELF_SIGNING) + .key(sskPublicKey) + .signature(userId, masterPublicKey, signedSSK) + .build(), + userPasswordAuth = params.authParams + ) + + uploadSigningKeysTask.execute(uploadSigningKeysParams) + + // Sign the current device with SSK + val uploadSignatureQueryBuilder = UploadSignatureQueryBuilder() + + val myDevice = myDeviceInfoHolder.get().myDevice + val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, myDevice.signalableJSONDictionary()) + val signedDevice = selfSigningPkOlm.sign(canonicalJson) + val updateSignatures = (myDevice.signatures?.toMutableMap() ?: HashMap()) + .also { + it[userId] = (it[userId] + ?: HashMap()) + mapOf("ed25519:$sskPublicKey" to signedDevice) + } + myDevice.copy(signatures = updateSignatures).let { + uploadSignatureQueryBuilder.withDeviceInfo(it) + } + + // sign MSK with device key (migration) and upload signatures + val message = JsonCanonicalizer.getCanonicalJson(Map::class.java, mskCrossSigningKeyInfo.signalableJSONDictionary()) + olmDevice.signMessage(message)?.let { sign -> + val mskUpdatedSignatures = (mskCrossSigningKeyInfo.signatures?.toMutableMap() + ?: HashMap()).also { + it[userId] = (it[userId] + ?: HashMap()) + mapOf("ed25519:${myDevice.deviceId}" to sign) + } + mskCrossSigningKeyInfo.copy( + signatures = mskUpdatedSignatures + ).let { + uploadSignatureQueryBuilder.withSigningKeyInfo(it) + } + } + + // TODO should we ignore failure of that? + uploadSignaturesTask.execute(UploadSignaturesTask.Params(uploadSignatureQueryBuilder.build())) + + return InitializeCrossSigningTask.Result( + masterKeyPK = masterKeyPrivateKey.toBase64NoPadding(), + userKeyPK = uskPrivateKey.toBase64NoPadding(), + selfSigningKeyPK = sskPrivateKey.toBase64NoPadding(), + masterKeyInfo = uploadSigningKeysParams.masterKey, + userKeyInfo = uploadSigningKeysParams.userKey, + selfSignedKeyInfo = uploadSigningKeysParams.selfSignedKey + ) + } finally { + masterPkOlm?.releaseSigning() + userSigningPkOlm?.releaseSigning() + selfSigningPkOlm?.releaseSigning() + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt index 51c5015a1d..400febc15f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt @@ -35,7 +35,6 @@ import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.task.Task import timber.log.Timber import java.util.ArrayList -import java.util.UUID import javax.inject.Inject internal interface RoomVerificationUpdateTask : Task { @@ -60,8 +59,7 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor( // TODO ignore initial sync or back pagination? params.events.forEach { event -> - Timber.d("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}") - Timber.v("## SAS Verification live observer: received msgId: $event") + Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}") // If the request is in the future by more than 5 minutes or more than 10 minutes in the past, // the message should be ignored by the receiver. @@ -76,7 +74,7 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor( // TODO use a global event decryptor? attache to session and that listen to new sessionId? // for now decrypt sync try { - val result = cryptoService.decryptEvent(event, event.roomId + UUID.randomUUID().toString()) + val result = cryptoService.decryptEvent(event, "") event.mxDecryptionResult = OlmDecryptionResult( payload = result.clearEvent, senderKey = result.senderCurve25519Key, @@ -85,6 +83,7 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor( ) } catch (e: MXCryptoError) { Timber.e("## SAS Failed to decrypt event: ${event.eventId}") + params.verificationService.onPotentiallyInterestingEventRoomFailToDecrypt(event) } } Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}") diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt index af097f4431..6574132c7f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt @@ -25,6 +25,7 @@ import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.convertToUTF8 import org.greenrobot.eventbus.EventBus +import timber.log.Timber import javax.inject.Inject internal interface UploadKeysTask : Task { @@ -50,6 +51,8 @@ internal class DefaultUploadKeysTask @Inject constructor( oneTimeKeys = params.oneTimeKeys ) + Timber.i("## Uploading device keys -> $body") + return executeRequest(eventBus) { apiCall = if (encodedDeviceId.isBlank()) { cryptoApi.uploadKeys(body) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tools/Tools.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tools/Tools.kt index 260e6165ba..c3d2c30079 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tools/Tools.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tools/Tools.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.crypto.tools import org.matrix.olm.OlmPkDecryption import org.matrix.olm.OlmPkEncryption +import org.matrix.olm.OlmPkSigning fun withOlmEncryption(block: (OlmPkEncryption) -> T): T { val olmPkEncryption = OlmPkEncryption() @@ -36,3 +37,12 @@ fun withOlmDecryption(block: (OlmPkDecryption) -> T): T { olmPkDecryption.releaseDecryption() } } + +fun withOlmSigning(block: (OlmPkSigning) -> T): T { + val olmPkSigning = OlmPkSigning() + try { + return block(olmPkSigning) + } finally { + olmPkSigning.releaseSigning() + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt index f88e6e4f35..e3a765f95c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt @@ -23,6 +23,8 @@ import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerif import im.vector.matrix.android.api.session.crypto.verification.SasMode import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.internal.crypto.IncomingGossipingRequestManager +import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import timber.log.Timber @@ -33,6 +35,8 @@ internal class DefaultIncomingSASDefaultVerificationTransaction( override val deviceId: String?, private val cryptoStore: IMXCryptoStore, crossSigningService: CrossSigningService, + outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + incomingGossipingRequestManager: IncomingGossipingRequestManager, deviceFingerprint: String, transactionId: String, otherUserID: String, @@ -43,6 +47,8 @@ internal class DefaultIncomingSASDefaultVerificationTransaction( deviceId, cryptoStore, crossSigningService, + outgoingGossipingRequestManager, + incomingGossipingRequestManager, deviceFingerprint, transactionId, otherUserID, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt index 85ab8f0bf6..7fd97d0231 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt @@ -20,6 +20,8 @@ import im.vector.matrix.android.api.session.crypto.verification.CancelCode import im.vector.matrix.android.api.session.crypto.verification.OutgoingSasVerificationTransaction import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.internal.crypto.IncomingGossipingRequestManager +import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import timber.log.Timber @@ -30,6 +32,8 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction( deviceId: String?, cryptoStore: IMXCryptoStore, crossSigningService: CrossSigningService, + outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + incomingGossipingRequestManager: IncomingGossipingRequestManager, deviceFingerprint: String, transactionId: String, otherUserId: String, @@ -40,6 +44,8 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction( deviceId, cryptoStore, crossSigningService, + outgoingGossipingRequestManager, + incomingGossipingRequestManager, deviceFingerprint, transactionId, otherUserId, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt index 3b9d62a5dd..77dcc483bd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt @@ -22,6 +22,9 @@ import dagger.Lazy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService +import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME +import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME +import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME import im.vector.matrix.android.api.session.crypto.verification.CancelCode import im.vector.matrix.android.api.session.crypto.verification.PendingVerificationRequest import im.vector.matrix.android.api.session.crypto.verification.QrCodeVerificationTransaction @@ -35,6 +38,7 @@ import im.vector.matrix.android.api.session.crypto.verification.safeValueOf 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.LocalEcho +import im.vector.matrix.android.api.session.events.model.RelationType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent @@ -49,13 +53,17 @@ import im.vector.matrix.android.api.session.room.model.message.MessageVerificati import im.vector.matrix.android.api.session.room.model.message.MessageVerificationStartContent import im.vector.matrix.android.api.session.room.model.message.ValidVerificationDone import im.vector.matrix.android.internal.crypto.DeviceListManager +import im.vector.matrix.android.internal.crypto.IncomingGossipingRequestManager import im.vector.matrix.android.internal.crypto.MyDeviceInfoHolder +import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel 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.EncryptedEventContent import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationAccept import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationCancel +import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationDone import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationKey import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationMac import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationReady @@ -86,6 +94,8 @@ internal class DefaultVerificationService @Inject constructor( @UserId private val userId: String, @DeviceId private val deviceId: String?, private val cryptoStore: IMXCryptoStore, + private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + private val incomingGossipingRequestManager: IncomingGossipingRequestManager, private val myDeviceInfoHolder: Lazy, private val deviceListManager: DeviceListManager, private val setDeviceVerificationAction: SetDeviceVerificationAction, @@ -103,6 +113,10 @@ internal class DefaultVerificationService @Inject constructor( // map [sender : [transaction]] private val txMap = HashMap>() + // we need to keep track of finished transaction + // It will be used for gossiping (to send request after request is completed and 'done' by other) + private val pastTransactions = HashMap>() + /** * Map [sender: [PendingVerificationRequest]] * For now we keep all requests (even terminated ones) during the lifetime of the app. @@ -131,6 +145,9 @@ internal class DefaultVerificationService @Inject constructor( EventType.KEY_VERIFICATION_READY -> { onReadyReceived(event) } + EventType.KEY_VERIFICATION_DONE -> { + onDoneReceived(event) + } MessageType.MSGTYPE_VERIFICATION_REQUEST -> { onRequestReceived(event) } @@ -354,6 +371,27 @@ internal class DefaultVerificationService @Inject constructor( */ } + override fun onPotentiallyInterestingEventRoomFailToDecrypt(event: Event) { + // When Should/Can we cancel?? + val relationContent = event.content.toModel()?.relatesTo + if (relationContent?.type == RelationType.REFERENCE) { + val relatedId = relationContent.eventId ?: return + // at least if request was sent by me, I can safely cancel without interfering + pendingRequests[event.senderId]?.firstOrNull { + it.transactionId == relatedId && !it.isIncoming + }?.let { pr -> + verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", null) + .cancelTransaction( + relatedId, + event.senderId ?: "", + event.getSenderKey() ?: "", + CancelCode.InvalidMessage + ) + updatePendingRequest(pr.copy(cancelConclusion = CancelCode.InvalidMessage)) + } + } + } + private suspend fun onRoomStartRequestReceived(event: Event) { val startReq = event.getClearContent().toModel() ?.copy( @@ -429,10 +467,34 @@ internal class DefaultVerificationService @Inject constructor( private suspend fun handleStart(otherUserId: String?, startReq: ValidVerificationInfoStart, txConfigure: (DefaultVerificationTransaction) -> Unit): CancelCode? { - Timber.d("## SAS onStartRequestReceived ${startReq.transactionId}") - if (checkKeysAreDownloaded(otherUserId!!, startReq.fromDevice) != null) { + Timber.d("## SAS onStartRequestReceived $startReq") + if (otherUserId?.let { checkKeysAreDownloaded(it, startReq.fromDevice) } != null) { val tid = startReq.transactionId - val existing = getExistingTransaction(otherUserId, tid) + var existing = getExistingTransaction(otherUserId, tid) + + // After the m.key.verification.ready event is sent, either party can send an + // m.key.verification.start event to begin the verification. If both parties + // send an m.key.verification.start event, and they both specify the same + // verification method, then the event sent by the user whose user ID is the + // smallest is used, and the other m.key.verification.start event is ignored. + // In the case of a single user verifying two of their devices, the device ID is + // compared instead . + if (existing is DefaultOutgoingSASDefaultVerificationTransaction) { + val readyRequest = getExistingVerificationRequest(otherUserId, tid) + if (readyRequest?.isReady == true) { + if (isOtherPrioritary(otherUserId, existing.otherDeviceId ?: "")) { + Timber.d("## SAS concurrent start isOtherPrioritary, clear") + // The other is prioritary! + // I should replace my outgoing with an incoming + removeTransaction(otherUserId, tid) + existing = null + } else { + Timber.d("## SAS concurrent start i am prioritary, ignore") + // i am prioritary, ignore this start event! + return null + } + } + } when (startReq) { is ValidVerificationInfoStart.SasVerificationInfoStart -> { @@ -482,6 +544,8 @@ internal class DefaultVerificationService @Inject constructor( deviceId, cryptoStore, crossSigningService, + outgoingGossipingRequestManager, + incomingGossipingRequestManager, myDeviceInfoHolder.get().myDevice.fingerprint()!!, startReq.transactionId, otherUserId, @@ -496,7 +560,7 @@ internal class DefaultVerificationService @Inject constructor( existing.onStartReceived(startReq) return null } else { - Timber.w("## SAS onStartRequestReceived - unexpected message ${startReq.transactionId}") + Timber.w("## SAS onStartRequestReceived - unexpected message ${startReq.transactionId} / $existing") return CancelCode.UnexpectedMessage } } @@ -506,6 +570,16 @@ internal class DefaultVerificationService @Inject constructor( } } + private fun isOtherPrioritary(otherUserId: String, otherDeviceId: String): Boolean { + if (userId < otherUserId) { + return false + } else if (userId > otherUserId) { + return true + } else { + return otherDeviceId < deviceId ?: "" + } + } + // TODO Refacto: It could just return a boolean private suspend fun checkKeysAreDownloaded(otherUserId: String, otherDeviceId: String): MXUsersDevicesMap? { @@ -572,9 +646,7 @@ internal class DefaultVerificationService @Inject constructor( )) } - if (existingTransaction is SASDefaultVerificationTransaction) { - existingTransaction.state = VerificationTxState.Cancelled(safeValueOf(cancelReq.code), false) - } + existingTransaction?.state = VerificationTxState.Cancelled(safeValueOf(cancelReq.code), false) } private fun onRoomAcceptReceived(event: Event) { @@ -696,6 +768,7 @@ internal class DefaultVerificationService @Inject constructor( private suspend fun onReadyReceived(event: Event) { val readyReq = event.getClearContent().toModel()?.asValidObject() + Timber.v("## SAS onReadyReceived $readyReq") if (readyReq == null || event.senderId == null) { // ignore @@ -714,6 +787,58 @@ internal class DefaultVerificationService @Inject constructor( } } + private fun onDoneReceived(event: Event) { + Timber.v("## onDoneReceived") + val doneReq = event.getClearContent().toModel()?.asValidObject() + if (doneReq == null || event.senderId == null) { + // ignore + Timber.e("## SAS Received invalid done request") + return + } + + handleDoneReceived(event.senderId, doneReq) + + if (event.senderId == userId) { + // We only send gossiping request when the other sent us a done + // We can ask without checking too much thinks (like trust), because we will check validity of secret on reception + getExistingTransaction(userId, doneReq.transactionId) + ?: getOldTransaction(userId, doneReq.transactionId) + ?.let { vt -> + val otherDeviceId = vt.otherDeviceId + if (!crossSigningService.canCrossSign()) { + outgoingGossipingRequestManager.sendSecretShareRequest(SELF_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId + ?: "*"))) + outgoingGossipingRequestManager.sendSecretShareRequest(USER_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId + ?: "*"))) + } + outgoingGossipingRequestManager.sendSecretShareRequest(KEYBACKUP_SECRET_SSSS_NAME, mapOf(userId to listOf(otherDeviceId + ?: "*"))) + } + } + } + + private fun handleDoneReceived(senderId: String, doneReq: ValidVerificationDone) { + Timber.v("## SAS Done receieved $doneReq") + val existing = getExistingTransaction(senderId, doneReq.transactionId) + if (existing == null) { + Timber.e("## SAS Received invalid Done request") + return + } + if (existing is DefaultQrCodeVerificationTransaction) { + existing.onDoneReceived() + } else { + // SAS do not care for now? + } + + // Now transactions are udated, let's also update Requests + val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == doneReq.transactionId } + if (existingRequest == null) { + Timber.e("## SAS Received Done for unknown request txId:${doneReq.transactionId}") + return + } + updatePendingRequest(existingRequest.copy(isSuccessful = true)) + } + private fun onRoomDoneReceived(event: Event) { val doneReq = event.getClearContent().toModel() ?.copy( @@ -776,16 +901,18 @@ internal class DefaultVerificationService @Inject constructor( if (readyReq.methods.contains(VERIFICATION_METHOD_RECIPROCATE)) { // Create the pending transaction val tx = DefaultQrCodeVerificationTransaction( - setDeviceVerificationAction, - readyReq.transactionId, - senderId, - readyReq.fromDevice, - crossSigningService, - cryptoStore, - qrCodeData, - userId, - deviceId ?: "", - false) + setDeviceVerificationAction = setDeviceVerificationAction, + transactionId = readyReq.transactionId, + otherUserId = senderId, + otherDeviceId = readyReq.fromDevice, + crossSigningService = crossSigningService, + outgoingGossipingRequestManager = outgoingGossipingRequestManager, + incomingGossipingRequestManager = incomingGossipingRequestManager, + cryptoStore = cryptoStore, + qrCodeData = qrCodeData, + userId = userId, + deviceId = deviceId ?: "", + isIncoming = false) tx.transport = transportCreator.invoke(tx) @@ -891,14 +1018,14 @@ internal class DefaultVerificationService @Inject constructor( ) } - private fun handleDoneReceived(senderId: String, doneInfo: ValidVerificationDone) { - val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == doneInfo.transactionId } - if (existingRequest == null) { - Timber.e("## SAS Received Done for unknown request txId:${doneInfo.transactionId}") - return - } - updatePendingRequest(existingRequest.copy(isSuccessful = true)) - } +// private fun handleDoneReceived(senderId: String, doneInfo: ValidVerificationDone) { +// val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == doneInfo.transactionId } +// if (existingRequest == null) { +// Timber.e("## SAS Received Done for unknown request txId:${doneInfo.transactionId}") +// return +// } +// updatePendingRequest(existingRequest.copy(isSuccessful = true)) +// } // TODO All this methods should be delegated to a TransactionStore override fun getExistingTransaction(otherUserId: String, tid: String): VerificationTransaction? { @@ -937,17 +1064,33 @@ internal class DefaultVerificationService @Inject constructor( private fun removeTransaction(otherUser: String, tid: String) { synchronized(txMap) { - txMap[otherUser]?.remove(tid)?.removeListener(this) + txMap[otherUser]?.remove(tid)?.also { + it.removeListener(this) + } + }?.let { + rememberOldTransaction(it) } } private fun addTransaction(tx: DefaultVerificationTransaction) { - tx.otherUserId.let { otherUserId -> - synchronized(txMap) { - val txInnerMap = txMap.getOrPut(otherUserId) { HashMap() } - txInnerMap[tx.transactionId] = tx - dispatchTxAdded(tx) - tx.addListener(this) + synchronized(txMap) { + val txInnerMap = txMap.getOrPut(tx.otherUserId) { HashMap() } + txInnerMap[tx.transactionId] = tx + dispatchTxAdded(tx) + tx.addListener(this) + } + } + + private fun rememberOldTransaction(tx: DefaultVerificationTransaction) { + synchronized(pastTransactions) { + pastTransactions.getOrPut(tx.otherUserId) { HashMap() }[tx.transactionId] = tx + } + } + + private fun getOldTransaction(userId: String, tid: String?): DefaultVerificationTransaction? { + return tid?.let { + synchronized(pastTransactions) { + pastTransactions[userId]?.get(it) } } } @@ -962,6 +1105,8 @@ internal class DefaultVerificationService @Inject constructor( deviceId, cryptoStore, crossSigningService, + outgoingGossipingRequestManager, + incomingGossipingRequestManager, myDeviceInfoHolder.get().myDevice.fingerprint()!!, txID, otherUserId, @@ -1036,6 +1181,18 @@ internal class DefaultVerificationService @Inject constructor( return verificationRequest } + override fun cancelVerificationRequest(request: PendingVerificationRequest) { + if (request.roomId != null) { + val transport = verificationTransportRoomMessageFactory.createTransport(request.roomId, null) + transport.cancelTransaction(request.transactionId ?: "", request.otherUserId, null, CancelCode.User) + } else { + val transport = verificationTransportToDeviceFactory.createTransport(null) + request.targetDevices?.forEach { deviceId -> + transport.cancelTransaction(request.transactionId ?: "", request.otherUserId, deviceId, CancelCode.User) + } + } + } + override fun requestKeyVerification(methods: List, otherUserId: String, otherDevices: List?): PendingVerificationRequest { // TODO refactor this with the DM one Timber.i("## Requesting verification to user: $otherUserId with device list $otherDevices") @@ -1137,6 +1294,8 @@ internal class DefaultVerificationService @Inject constructor( deviceId, cryptoStore, crossSigningService, + outgoingGossipingRequestManager, + incomingGossipingRequestManager, myDeviceInfoHolder.get().myDevice.fingerprint()!!, transactionId, otherUserId, @@ -1268,16 +1427,18 @@ internal class DefaultVerificationService @Inject constructor( if (VERIFICATION_METHOD_RECIPROCATE in result) { // Create the pending transaction val tx = DefaultQrCodeVerificationTransaction( - setDeviceVerificationAction, - transactionId, - otherUserId, - otherDeviceId, - crossSigningService, - cryptoStore, - qrCodeData, - userId, - deviceId ?: "", - false) + setDeviceVerificationAction = setDeviceVerificationAction, + transactionId = transactionId, + otherUserId = otherUserId, + otherDeviceId = otherDeviceId, + crossSigningService = crossSigningService, + outgoingGossipingRequestManager = outgoingGossipingRequestManager, + incomingGossipingRequestManager = incomingGossipingRequestManager, + cryptoStore = cryptoStore, + qrCodeData = qrCodeData, + userId = userId, + deviceId = deviceId ?: "", + isIncoming = false) tx.transport = transportCreator.invoke(tx) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationTransaction.kt index 6396447352..eb78aee42d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationTransaction.kt @@ -19,6 +19,8 @@ import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState +import im.vector.matrix.android.internal.crypto.IncomingGossipingRequestManager +import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel import timber.log.Timber @@ -29,6 +31,8 @@ import timber.log.Timber internal abstract class DefaultVerificationTransaction( private val setDeviceVerificationAction: SetDeviceVerificationAction, private val crossSigningService: CrossSigningService, + private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + private val incomingGossipingRequestManager: IncomingGossipingRequestManager, private val userId: String, override val transactionId: String, override val otherUserId: String, @@ -53,7 +57,15 @@ internal abstract class DefaultVerificationTransaction( protected fun trust(canTrustOtherUserMasterKey: Boolean, toVerifyDeviceIds: List, - eventuallyMarkMyMasterKeyAsTrusted: Boolean) { + eventuallyMarkMyMasterKeyAsTrusted: Boolean, autoDone : Boolean = true) { + Timber.d("## Verification: trust ($otherUserId,$otherDeviceId) , verifiedDevices:$toVerifyDeviceIds") + Timber.d("## Verification: trust Mark myMSK trusted $eventuallyMarkMyMasterKeyAsTrusted") + + // TODO what if the otherDevice is not in this list? and should we + toVerifyDeviceIds.forEach { + setDeviceVerified(otherUserId, it) + } + // If not me sign his MSK and upload the signature if (canTrustOtherUserMasterKey) { // we should trust this master key @@ -74,6 +86,8 @@ internal abstract class DefaultVerificationTransaction( } if (otherUserId == userId) { + incomingGossipingRequestManager.onVerificationCompleteForDevice(otherDeviceId!!) + // If me it's reasonable to sign and upload the device signature // Notice that i might not have the private keys, so may not be able to do it crossSigningService.trustDevice(otherDeviceId!!, object : MatrixCallback { @@ -83,12 +97,10 @@ internal abstract class DefaultVerificationTransaction( }) } - // TODO what if the otherDevice is not in this list? and should we - toVerifyDeviceIds.forEach { - setDeviceVerified(otherUserId, it) + if (autoDone) { + state = VerificationTxState.Verified + transport.done(transactionId) {} } - transport.done(transactionId) - state = VerificationTxState.Verified } private fun setDeviceVerified(userId: String, deviceId: String) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASDefaultVerificationTransaction.kt index 18532a2e26..2a502730fa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASDefaultVerificationTransaction.kt @@ -15,7 +15,6 @@ */ package im.vector.matrix.android.internal.crypto.verification -import android.os.Build import im.vector.matrix.android.api.extensions.orFalse import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService import im.vector.matrix.android.api.session.crypto.verification.CancelCode @@ -24,6 +23,8 @@ import im.vector.matrix.android.api.session.crypto.verification.SasMode import im.vector.matrix.android.api.session.crypto.verification.SasVerificationTransaction import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.internal.crypto.IncomingGossipingRequestManager +import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.model.MXKey import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore @@ -42,6 +43,8 @@ internal abstract class SASDefaultVerificationTransaction( open val deviceId: String?, private val cryptoStore: IMXCryptoStore, crossSigningService: CrossSigningService, + outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + incomingGossipingRequestManager: IncomingGossipingRequestManager, private val deviceFingerprint: String, transactionId: String, otherUserId: String, @@ -50,6 +53,8 @@ internal abstract class SASDefaultVerificationTransaction( ) : DefaultVerificationTransaction( setDeviceVerificationAction, crossSigningService, + outgoingGossipingRequestManager, + incomingGossipingRequestManager, userId, transactionId, otherUserId, @@ -68,13 +73,9 @@ internal abstract class SASDefaultVerificationTransaction( // ordered by preferred order val KNOWN_MACS = listOf(SAS_MAC_SHA256, SAS_MAC_SHA256_LONGKDF) - // older devices have limited support of emoji, so reply with decimal - val KNOWN_SHORT_CODES = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - listOf(SasMode.EMOJI, SasMode.DECIMAL) - } else { - listOf(SasMode.DECIMAL) - } + // older devices have limited support of emoji but SDK offers images for the 64 verification emojis + // so always send that we support EMOJI + val KNOWN_SHORT_CODES = listOf(SasMode.EMOJI, SasMode.DECIMAL) } override var state: VerificationTxState = VerificationTxState.None diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationEmoji.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationEmoji.kt index eb9acd045a..396b609f41 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationEmoji.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationEmoji.kt @@ -20,69 +20,69 @@ import im.vector.matrix.android.api.session.crypto.verification.EmojiRepresentat internal fun getEmojiForCode(code: Int): EmojiRepresentation { return when (code % 64) { - 0 -> EmojiRepresentation("🐶", R.string.verification_emoji_dog) - 1 -> EmojiRepresentation("🐱", R.string.verification_emoji_cat) - 2 -> EmojiRepresentation("🦁", R.string.verification_emoji_lion) - 3 -> EmojiRepresentation("🐎", R.string.verification_emoji_horse) - 4 -> EmojiRepresentation("🦄", R.string.verification_emoji_unicorn) - 5 -> EmojiRepresentation("🐷", R.string.verification_emoji_pig) - 6 -> EmojiRepresentation("🐘", R.string.verification_emoji_elephant) - 7 -> EmojiRepresentation("🐰", R.string.verification_emoji_rabbit) - 8 -> EmojiRepresentation("🐼", R.string.verification_emoji_panda) - 9 -> EmojiRepresentation("🐓", R.string.verification_emoji_rooster) - 10 -> EmojiRepresentation("🐧", R.string.verification_emoji_penguin) - 11 -> EmojiRepresentation("🐢", R.string.verification_emoji_turtle) - 12 -> EmojiRepresentation("🐟", R.string.verification_emoji_fish) - 13 -> EmojiRepresentation("🐙", R.string.verification_emoji_octopus) - 14 -> EmojiRepresentation("🦋", R.string.verification_emoji_butterfly) - 15 -> EmojiRepresentation("🌷", R.string.verification_emoji_flower) - 16 -> EmojiRepresentation("🌳", R.string.verification_emoji_tree) - 17 -> EmojiRepresentation("🌵", R.string.verification_emoji_cactus) - 18 -> EmojiRepresentation("🍄", R.string.verification_emoji_mushroom) - 19 -> EmojiRepresentation("🌏", R.string.verification_emoji_globe) - 20 -> EmojiRepresentation("🌙", R.string.verification_emoji_moon) - 21 -> EmojiRepresentation("☁️", R.string.verification_emoji_cloud) - 22 -> EmojiRepresentation("🔥", R.string.verification_emoji_fire) - 23 -> EmojiRepresentation("🍌", R.string.verification_emoji_banana) - 24 -> EmojiRepresentation("🍎", R.string.verification_emoji_apple) - 25 -> EmojiRepresentation("🍓", R.string.verification_emoji_strawberry) - 26 -> EmojiRepresentation("🌽", R.string.verification_emoji_corn) - 27 -> EmojiRepresentation("🍕", R.string.verification_emoji_pizza) - 28 -> EmojiRepresentation("🎂", R.string.verification_emoji_cake) - 29 -> EmojiRepresentation("❤️", R.string.verification_emoji_heart) - 30 -> EmojiRepresentation("😀", R.string.verification_emoji_smiley) - 31 -> EmojiRepresentation("🤖", R.string.verification_emoji_robot) - 32 -> EmojiRepresentation("🎩", R.string.verification_emoji_hat) - 33 -> EmojiRepresentation("👓", R.string.verification_emoji_glasses) - 34 -> EmojiRepresentation("🔧", R.string.verification_emoji_wrench) - 35 -> EmojiRepresentation("🎅", R.string.verification_emoji_santa) - 36 -> EmojiRepresentation("👍", R.string.verification_emoji_thumbsup) - 37 -> EmojiRepresentation("☂️", R.string.verification_emoji_umbrella) - 38 -> EmojiRepresentation("⌛", R.string.verification_emoji_hourglass) - 39 -> EmojiRepresentation("⏰", R.string.verification_emoji_clock) - 40 -> EmojiRepresentation("🎁", R.string.verification_emoji_gift) - 41 -> EmojiRepresentation("💡", R.string.verification_emoji_lightbulb) - 42 -> EmojiRepresentation("📕", R.string.verification_emoji_book) - 43 -> EmojiRepresentation("✏️", R.string.verification_emoji_pencil) - 44 -> EmojiRepresentation("📎", R.string.verification_emoji_paperclip) - 45 -> EmojiRepresentation("✂️", R.string.verification_emoji_scissors) - 46 -> EmojiRepresentation("🔒", R.string.verification_emoji_lock) - 47 -> EmojiRepresentation("🔑", R.string.verification_emoji_key) - 48 -> EmojiRepresentation("🔨", R.string.verification_emoji_hammer) - 49 -> EmojiRepresentation("☎️", R.string.verification_emoji_telephone) - 50 -> EmojiRepresentation("🏁", R.string.verification_emoji_flag) - 51 -> EmojiRepresentation("🚂", R.string.verification_emoji_train) - 52 -> EmojiRepresentation("🚲", R.string.verification_emoji_bicycle) - 53 -> EmojiRepresentation("✈️", R.string.verification_emoji_airplane) - 54 -> EmojiRepresentation("🚀", R.string.verification_emoji_rocket) - 55 -> EmojiRepresentation("🏆", R.string.verification_emoji_trophy) - 56 -> EmojiRepresentation("⚽", R.string.verification_emoji_ball) - 57 -> EmojiRepresentation("🎸", R.string.verification_emoji_guitar) - 58 -> EmojiRepresentation("🎺", R.string.verification_emoji_trumpet) - 59 -> EmojiRepresentation("🔔", R.string.verification_emoji_bell) - 60 -> EmojiRepresentation("⚓", R.string.verification_emoji_anchor) - 61 -> EmojiRepresentation("🎧", R.string.verification_emoji_headphone) - 62 -> EmojiRepresentation("📁", R.string.verification_emoji_folder) - /* 63 */ else -> EmojiRepresentation("📌", R.string.verification_emoji_pin) + 0 -> EmojiRepresentation("🐶", R.string.verification_emoji_dog, R.drawable.ic_verification_dog) + 1 -> EmojiRepresentation("🐱", R.string.verification_emoji_cat, R.drawable.ic_verification_cat) + 2 -> EmojiRepresentation("🦁", R.string.verification_emoji_lion, R.drawable.ic_verification_lion) + 3 -> EmojiRepresentation("🐎", R.string.verification_emoji_horse, R.drawable.ic_verification_horse) + 4 -> EmojiRepresentation("🦄", R.string.verification_emoji_unicorn, R.drawable.ic_verification_unicorn) + 5 -> EmojiRepresentation("🐷", R.string.verification_emoji_pig, R.drawable.ic_verification_pig) + 6 -> EmojiRepresentation("🐘", R.string.verification_emoji_elephant, R.drawable.ic_verification_elephant) + 7 -> EmojiRepresentation("🐰", R.string.verification_emoji_rabbit, R.drawable.ic_verification_rabbit) + 8 -> EmojiRepresentation("🐼", R.string.verification_emoji_panda, R.drawable.ic_verification_panda) + 9 -> EmojiRepresentation("🐓", R.string.verification_emoji_rooster, R.drawable.ic_verification_rooster) + 10 -> EmojiRepresentation("🐧", R.string.verification_emoji_penguin, R.drawable.ic_verification_penguin) + 11 -> EmojiRepresentation("🐢", R.string.verification_emoji_turtle, R.drawable.ic_verification_turtle) + 12 -> EmojiRepresentation("🐟", R.string.verification_emoji_fish, R.drawable.ic_verification_fish) + 13 -> EmojiRepresentation("🐙", R.string.verification_emoji_octopus, R.drawable.ic_verification_octopus) + 14 -> EmojiRepresentation("🦋", R.string.verification_emoji_butterfly, R.drawable.ic_verification_butterfly) + 15 -> EmojiRepresentation("🌷", R.string.verification_emoji_flower, R.drawable.ic_verification_flower) + 16 -> EmojiRepresentation("🌳", R.string.verification_emoji_tree, R.drawable.ic_verification_tree) + 17 -> EmojiRepresentation("🌵", R.string.verification_emoji_cactus, R.drawable.ic_verification_cactus) + 18 -> EmojiRepresentation("🍄", R.string.verification_emoji_mushroom, R.drawable.ic_verification_mushroom) + 19 -> EmojiRepresentation("🌏", R.string.verification_emoji_globe, R.drawable.ic_verification_globe) + 20 -> EmojiRepresentation("🌙", R.string.verification_emoji_moon, R.drawable.ic_verification_moon) + 21 -> EmojiRepresentation("☁️", R.string.verification_emoji_cloud, R.drawable.ic_verification_cloud) + 22 -> EmojiRepresentation("🔥", R.string.verification_emoji_fire, R.drawable.ic_verification_fire) + 23 -> EmojiRepresentation("🍌", R.string.verification_emoji_banana, R.drawable.ic_verification_banana) + 24 -> EmojiRepresentation("🍎", R.string.verification_emoji_apple, R.drawable.ic_verification_apple) + 25 -> EmojiRepresentation("🍓", R.string.verification_emoji_strawberry, R.drawable.ic_verification_strawberry) + 26 -> EmojiRepresentation("🌽", R.string.verification_emoji_corn, R.drawable.ic_verification_corn) + 27 -> EmojiRepresentation("🍕", R.string.verification_emoji_pizza, R.drawable.ic_verification_pizza) + 28 -> EmojiRepresentation("🎂", R.string.verification_emoji_cake, R.drawable.ic_verification_cake) + 29 -> EmojiRepresentation("❤️", R.string.verification_emoji_heart, R.drawable.ic_verification_heart) + 30 -> EmojiRepresentation("🙂", R.string.verification_emoji_smiley, R.drawable.ic_verification_smiley) + 31 -> EmojiRepresentation("🤖", R.string.verification_emoji_robot, R.drawable.ic_verification_robot) + 32 -> EmojiRepresentation("🎩", R.string.verification_emoji_hat, R.drawable.ic_verification_hat) + 33 -> EmojiRepresentation("👓", R.string.verification_emoji_glasses, R.drawable.ic_verification_glasses) + 34 -> EmojiRepresentation("🔧", R.string.verification_emoji_wrench, R.drawable.ic_verification_wrench) + 35 -> EmojiRepresentation("🎅", R.string.verification_emoji_santa, R.drawable.ic_verification_santa) + 36 -> EmojiRepresentation("👍", R.string.verification_emoji_thumbsup, R.drawable.ic_verification_thumbs_up) + 37 -> EmojiRepresentation("☂️", R.string.verification_emoji_umbrella, R.drawable.ic_verification_umbrella) + 38 -> EmojiRepresentation("⌛", R.string.verification_emoji_hourglass, R.drawable.ic_verification_hourglass) + 39 -> EmojiRepresentation("⏰", R.string.verification_emoji_clock, R.drawable.ic_verification_clock) + 40 -> EmojiRepresentation("🎁", R.string.verification_emoji_gift, R.drawable.ic_verification_gift) + 41 -> EmojiRepresentation("💡", R.string.verification_emoji_lightbulb, R.drawable.ic_verification_light_bulb) + 42 -> EmojiRepresentation("📕", R.string.verification_emoji_book, R.drawable.ic_verification_book) + 43 -> EmojiRepresentation("✏️", R.string.verification_emoji_pencil, R.drawable.ic_verification_pencil) + 44 -> EmojiRepresentation("📎", R.string.verification_emoji_paperclip, R.drawable.ic_verification_paperclip) + 45 -> EmojiRepresentation("✂️", R.string.verification_emoji_scissors, R.drawable.ic_verification_scissors) + 46 -> EmojiRepresentation("🔒", R.string.verification_emoji_lock, R.drawable.ic_verification_lock) + 47 -> EmojiRepresentation("🔑", R.string.verification_emoji_key, R.drawable.ic_verification_key) + 48 -> EmojiRepresentation("🔨", R.string.verification_emoji_hammer, R.drawable.ic_verification_hammer) + 49 -> EmojiRepresentation("☎️", R.string.verification_emoji_telephone, R.drawable.ic_verification_phone) + 50 -> EmojiRepresentation("🏁", R.string.verification_emoji_flag, R.drawable.ic_verification_flag) + 51 -> EmojiRepresentation("🚂", R.string.verification_emoji_train, R.drawable.ic_verification_train) + 52 -> EmojiRepresentation("🚲", R.string.verification_emoji_bicycle, R.drawable.ic_verification_bicycle) + 53 -> EmojiRepresentation("✈️", R.string.verification_emoji_airplane, R.drawable.ic_verification_airplane) + 54 -> EmojiRepresentation("🚀", R.string.verification_emoji_rocket, R.drawable.ic_verification_rocket) + 55 -> EmojiRepresentation("🏆", R.string.verification_emoji_trophy, R.drawable.ic_verification_trophy) + 56 -> EmojiRepresentation("⚽", R.string.verification_emoji_ball, R.drawable.ic_verification_ball) + 57 -> EmojiRepresentation("🎸", R.string.verification_emoji_guitar, R.drawable.ic_verification_guitar) + 58 -> EmojiRepresentation("🎺", R.string.verification_emoji_trumpet, R.drawable.ic_verification_trumpet) + 59 -> EmojiRepresentation("🔔", R.string.verification_emoji_bell, R.drawable.ic_verification_bell) + 60 -> EmojiRepresentation("⚓", R.string.verification_emoji_anchor, R.drawable.ic_verification_anchor) + 61 -> EmojiRepresentation("🎧", R.string.verification_emoji_headphone, R.drawable.ic_verification_headphone) + 62 -> EmojiRepresentation("📁", R.string.verification_emoji_folder, R.drawable.ic_verification_folder) + /* 63 */ else -> EmojiRepresentation("📌", R.string.verification_emoji_pin, R.drawable.ic_verification_pin) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoDone.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoDone.kt index abb1141355..8cf96d7d65 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoDone.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoDone.kt @@ -15,14 +15,12 @@ */ package im.vector.matrix.android.internal.crypto.verification -internal interface VerificationInfoDone : VerificationInfo { +import im.vector.matrix.android.api.session.room.model.message.ValidVerificationDone - override fun asValidObject(): ValidVerificationInfoDone? { - if (transactionId.isNullOrEmpty()) { - return null - } - return ValidVerificationInfoDone +internal interface VerificationInfoDone : VerificationInfo { + + override fun asValidObject(): ValidVerificationDone? { + val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null + return ValidVerificationDone(validTransactionId) } } - -internal object ValidVerificationInfoDone diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransport.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransport.kt index cedcf2865d..75ffa5e082 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransport.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransport.kt @@ -46,7 +46,8 @@ internal interface VerificationTransport { otherUserDeviceId: String?, code: CancelCode) - fun done(transactionId: String) + fun done(transactionId: String, + onDone: (() -> Unit)?) /** * Creates an accept message suitable for this transport diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt index 7a6e3b40ac..b7b7335011 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt @@ -22,8 +22,8 @@ import androidx.work.ExistingWorkPolicy import androidx.work.Operation import androidx.work.WorkInfo import im.vector.matrix.android.R -import im.vector.matrix.android.api.session.crypto.verification.ValidVerificationInfoRequest import im.vector.matrix.android.api.session.crypto.verification.CancelCode +import im.vector.matrix.android.api.session.crypto.verification.ValidVerificationInfoRequest import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Event @@ -107,7 +107,7 @@ internal class VerificationTransportRoomMessage( // }, listenerExecutor) val workLiveData = workManagerProvider.workManager - .getWorkInfosForUniqueWorkLiveData("${roomId}_VerificationWork") + .getWorkInfosForUniqueWorkLiveData(uniqueQueueName()) val observer = object : Observer> { override fun onChanged(workInfoList: List?) { @@ -228,7 +228,8 @@ internal class VerificationTransportRoomMessage( enqueueSendWork(workerParams) } - override fun done(transactionId: String) { + override fun done(transactionId: String, + onDone: (() -> Unit)?) { Timber.d("## SAS sending done for $transactionId") val event = createEventAndLocalEcho( type = EventType.KEY_VERIFICATION_DONE, @@ -244,7 +245,26 @@ internal class VerificationTransportRoomMessage( sessionId = sessionId, event = event )) - enqueueSendWork(workerParams) + val enqueueInfo = enqueueSendWork(workerParams) + + val workLiveData = workManagerProvider.workManager + .getWorkInfosForUniqueWorkLiveData(uniqueQueueName()) + val observer = object : Observer> { + override fun onChanged(workInfoList: List?) { + workInfoList + ?.filter { it.state == WorkInfo.State.SUCCEEDED } + ?.firstOrNull { it.id == enqueueInfo.second } + ?.let { _ -> + onDone?.invoke() + workLiveData.removeObserver(this) + } + } + } + + // TODO listen to DB to get synced info + GlobalScope.launch(Dispatchers.Main) { + workLiveData.observeForever(observer) + } } private fun enqueueSendWork(workerParams: Data): Pair { @@ -254,10 +274,12 @@ internal class VerificationTransportRoomMessage( .setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS) .build() return workManagerProvider.workManager - .beginUniqueWork("${roomId}_VerificationWork", ExistingWorkPolicy.APPEND, workRequest) + .beginUniqueWork(uniqueQueueName(), ExistingWorkPolicy.APPEND, workRequest) .enqueue() to workRequest.id } + private fun uniqueQueueName() = "${roomId}_VerificationWork" + override fun createAccept(tid: String, keyAgreementProtocol: String, hash: String, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportToDevice.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportToDevice.kt index d07a83d7b1..290fc88878 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportToDevice.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportToDevice.kt @@ -145,7 +145,7 @@ internal class VerificationTransportToDevice( .executeBy(taskExecutor) } - override fun done(transactionId: String) { + override fun done(transactionId: String, onDone: (() -> Unit)?) { val otherUserId = tx?.otherUserId ?: return val otherUserDeviceId = tx?.otherDeviceId ?: return val cancelMessage = KeyVerificationDone(transactionId) @@ -155,6 +155,7 @@ internal class VerificationTransportToDevice( .configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_DONE, contentMap, transactionId)) { this.callback = object : MatrixCallback { override fun onSuccess(data: Unit) { + onDone?.invoke() Timber.v("## SAS verification [$transactionId] done") } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt index f56e261416..59ee23cc62 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt @@ -21,6 +21,8 @@ import im.vector.matrix.android.api.session.crypto.verification.CancelCode import im.vector.matrix.android.api.session.crypto.verification.QrCodeVerificationTransaction import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.internal.crypto.IncomingGossipingRequestManager +import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64 import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64Safe @@ -36,6 +38,8 @@ internal class DefaultQrCodeVerificationTransaction( override val otherUserId: String, override var otherDeviceId: String?, private val crossSigningService: CrossSigningService, + outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + incomingGossipingRequestManager: IncomingGossipingRequestManager, private val cryptoStore: IMXCryptoStore, // Not null only if other user is able to scan QR code private val qrCodeData: QrCodeData?, @@ -45,6 +49,8 @@ internal class DefaultQrCodeVerificationTransaction( ) : DefaultVerificationTransaction( setDeviceVerificationAction, crossSigningService, + outgoingGossipingRequestManager, + incomingGossipingRequestManager, userId, transactionId, otherUserId, @@ -181,19 +187,22 @@ internal class DefaultQrCodeVerificationTransaction( // qrCodeData.sharedSecret will be used to send the start request start(otherQrCodeData.sharedSecret) - // Trust the other user - trust(canTrustOtherUserMasterKey, - toVerifyDeviceIds.distinct(), - eventuallyMarkMyMasterKeyAsTrusted = true) + trust( + canTrustOtherUserMasterKey = canTrustOtherUserMasterKey, + toVerifyDeviceIds = toVerifyDeviceIds.distinct(), + eventuallyMarkMyMasterKeyAsTrusted = true, + autoDone = false + ) } - private fun start(remoteSecret: String) { + private fun start(remoteSecret: String, onDone: (() -> Unit)? = null) { if (state != VerificationTxState.None) { Timber.e("## Verification QR: start verification from invalid state") // should I cancel?? throw IllegalStateException("Interactive Key verification already started") } + state = VerificationTxState.Started val startMessage = transport.createStartForQrCode( deviceId, transactionId, @@ -203,9 +212,9 @@ internal class DefaultQrCodeVerificationTransaction( transport.sendToOther( EventType.KEY_VERIFICATION_START, startMessage, - VerificationTxState.Started, + VerificationTxState.WaitingOtherReciprocateConfirm, CancelCode.User, - null + onDone ) } @@ -239,6 +248,15 @@ internal class DefaultQrCodeVerificationTransaction( } } + fun onDoneReceived() { + if (state != VerificationTxState.WaitingOtherReciprocateConfirm) { + cancel(CancelCode.UnexpectedMessage) + return + } + state = VerificationTxState.Verified + transport.done(transactionId) {} + } + override fun otherUserScannedMyQrCode() { when (qrCodeData) { is QrCodeData.VerifyingAnotherUser -> { @@ -260,6 +278,6 @@ internal class DefaultQrCodeVerificationTransaction( override fun otherUserDidNotScannedMyQrCode() { // What can I do then? // At least remove the transaction... - state = VerificationTxState.Cancelled(CancelCode.MismatchedKeys, true) + cancel(CancelCode.MismatchedKeys) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/FileQualifiers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/FileQualifiers.kt index acd6703c77..aa39fc6fe8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/FileQualifiers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/FileQualifiers.kt @@ -29,3 +29,7 @@ annotation class SessionCacheDirectory @Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class CacheDirectory + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class ExternalFilesDirectory 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 e929016d4f..4d6082d50b 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 @@ -56,6 +56,9 @@ internal interface MatrixComponent { @CacheDirectory fun cacheDir(): File + @ExternalFilesDirectory + fun externalFilesDir(): File? + fun olmManager(): OlmManager fun taskExecutor(): TaskExecutor diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt index 0af22dd65a..785aecdf8e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt @@ -57,6 +57,13 @@ internal object MatrixModule { return context.cacheDir } + @JvmStatic + @Provides + @ExternalFilesDirectory + fun providesExternalFilesDir(context: Context): File? { + return context.getExternalFilesDir(null) + } + @JvmStatic @Provides @MatrixScope diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkInfoReceiver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkInfoReceiver.kt index e8daf9b79b..645329628b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkInfoReceiver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkInfoReceiver.kt @@ -14,6 +14,9 @@ * limitations under the License. */ +// This BroadcastReceiver is used only if the build code is below 24. +@file:Suppress("DEPRECATION") + package im.vector.matrix.android.internal.network import android.content.BroadcastReceiver diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt index 9eaea8cc34..cfb0d23f2b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.session -import android.os.Environment import arrow.core.Try import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.content.ContentUrlResolver @@ -25,6 +24,7 @@ import im.vector.matrix.android.api.util.Cancelable 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.CacheDirectory +import im.vector.matrix.android.internal.di.ExternalFilesDirectory import im.vector.matrix.android.internal.di.SessionCacheDirectory import im.vector.matrix.android.internal.di.Unauthenticated import im.vector.matrix.android.internal.extensions.foldToCallback @@ -44,6 +44,8 @@ import javax.inject.Inject internal class DefaultFileService @Inject constructor( @CacheDirectory private val cacheDirectory: File, + @ExternalFilesDirectory + private val externalFilesDirectory: File?, @SessionCacheDirectory private val sessionCacheDirectory: File, private val contentUrlResolver: ContentUrlResolver, @@ -77,9 +79,15 @@ internal class DefaultFileService @Inject constructor( .url(resolvedUrl) .build() - val response = okHttpClient.newCall(request).execute() + val response = try { + okHttpClient.newCall(request).execute() + } catch (e: Throwable) { + return@flatMap Try.Failure(e) + } + var inputStream = response.body?.byteStream() Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${inputStream?.available()}") + if (!response.isSuccessful || inputStream == null) { return@flatMap Try.Failure(IOException()) } @@ -103,7 +111,7 @@ internal class DefaultFileService @Inject constructor( private fun copyFile(file: File, downloadMode: FileService.DownloadMode): File { return when (downloadMode) { FileService.DownloadMode.TO_EXPORT -> - file.copyTo(File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), file.name), true) + file.copyTo(File(externalFilesDirectory, file.name), true) FileService.DownloadMode.FOR_EXTERNAL_SHARE -> file.copyTo(File(File(cacheDirectory, "ext_share"), file.name), true) FileService.DownloadMode.FOR_INTERNAL_USE -> diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index 84b76345c8..25dc939196 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -193,6 +193,7 @@ internal class DefaultSession @Inject constructor( stopAnyBackgroundSync() liveEntityObservers.forEach { it.cancelProcess() } cacheService.get().clearCache(callback) + workManagerProvider.cancelAllWorks() } @Subscribe(threadMode = ThreadMode.MAIN) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt index 1b07377fa1..22ebb4273a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt @@ -20,7 +20,10 @@ import dagger.BindsInstance import dagger.Component import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.internal.crypto.CancelGossipRequestWorker import im.vector.matrix.android.internal.crypto.CryptoModule +import im.vector.matrix.android.internal.crypto.SendGossipRequestWorker +import im.vector.matrix.android.internal.crypto.SendGossipWorker import im.vector.matrix.android.internal.crypto.verification.SendVerificationMessageWorker import im.vector.matrix.android.internal.di.MatrixComponent import im.vector.matrix.android.internal.di.SessionAssistedInjectModule @@ -106,6 +109,12 @@ internal interface SessionComponent { fun inject(worker: SendVerificationMessageWorker) + fun inject(worker: SendGossipRequestWorker) + + fun inject(worker: CancelGossipRequestWorker) + + fun inject(worker: SendGossipWorker) + @Component.Factory interface Factory { fun create( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt index 4071c9224f..4fa0cb5013 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt @@ -53,9 +53,9 @@ internal class FileUploader @Inject constructor(@Authenticated suspend fun uploadByteArray(byteArray: ByteArray, filename: String?, - mimeType: String, + mimeType: String?, progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { - val uploadBody = byteArray.toRequestBody(mimeType.toMediaTypeOrNull()) + val uploadBody = byteArray.toRequestBody(mimeType?.toMediaTypeOrNull()) return upload(uploadBody, filename, progressListener) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ThumbnailExtractor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ThumbnailExtractor.kt index 083cac0278..eae2bf8f6d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ThumbnailExtractor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ThumbnailExtractor.kt @@ -16,12 +16,12 @@ package im.vector.matrix.android.internal.session.content +import android.content.Context import android.graphics.Bitmap -import android.media.ThumbnailUtils -import android.provider.MediaStore +import android.media.MediaMetadataRetriever import im.vector.matrix.android.api.session.content.ContentAttachmentData +import timber.log.Timber import java.io.ByteArrayOutputStream -import java.io.File internal object ThumbnailExtractor { @@ -33,34 +33,40 @@ internal object ThumbnailExtractor { val mimeType: String ) - fun extractThumbnail(attachment: ContentAttachmentData): ThumbnailData? { - val file = File(attachment.path) - if (!file.exists() || !file.isFile) { - return null - } + fun extractThumbnail(context: Context, attachment: ContentAttachmentData): ThumbnailData? { return if (attachment.type == ContentAttachmentData.Type.VIDEO) { - extractVideoThumbnail(attachment) + extractVideoThumbnail(context, attachment) } else { null } } - private fun extractVideoThumbnail(attachment: ContentAttachmentData): ThumbnailData? { - val thumbnail = ThumbnailUtils.createVideoThumbnail(attachment.path, MediaStore.Video.Thumbnails.MINI_KIND) ?: return null - val outputStream = ByteArrayOutputStream() - thumbnail.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) - val thumbnailWidth = thumbnail.width - val thumbnailHeight = thumbnail.height - val thumbnailSize = outputStream.size() - val thumbnailData = ThumbnailData( - width = thumbnailWidth, - height = thumbnailHeight, - size = thumbnailSize.toLong(), - bytes = outputStream.toByteArray(), - mimeType = "image/jpeg" - ) - thumbnail.recycle() - outputStream.reset() + private fun extractVideoThumbnail(context: Context, attachment: ContentAttachmentData): ThumbnailData? { + var thumbnailData: ThumbnailData? = null + val mediaMetadataRetriever = MediaMetadataRetriever() + try { + mediaMetadataRetriever.setDataSource(context, attachment.queryUri) + val thumbnail = mediaMetadataRetriever.frameAtTime + + val outputStream = ByteArrayOutputStream() + thumbnail.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) + val thumbnailWidth = thumbnail.width + val thumbnailHeight = thumbnail.height + val thumbnailSize = outputStream.size() + thumbnailData = ThumbnailData( + width = thumbnailWidth, + height = thumbnailHeight, + size = thumbnailSize.toLong(), + bytes = outputStream.toByteArray(), + mimeType = "image/jpeg" + ) + thumbnail.recycle() + outputStream.reset() + } catch (e: Exception) { + Timber.e(e, "Cannot extract video thumbnail") + } finally { + mediaMetadataRetriever.release() + } return thumbnailData } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt index 1dde25fd78..1b736d349f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt @@ -17,12 +17,9 @@ package im.vector.matrix.android.internal.session.content import android.content.Context -import android.graphics.BitmapFactory import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass -import id.zelory.compressor.Compressor -import id.zelory.compressor.constraint.default import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.toContent @@ -41,8 +38,6 @@ import im.vector.matrix.android.internal.worker.WorkerParamsFactory import im.vector.matrix.android.internal.worker.getSessionComponent import timber.log.Timber import java.io.ByteArrayInputStream -import java.io.File -import java.io.FileInputStream import javax.inject.Inject private data class NewImageAttributes( @@ -77,6 +72,16 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter return Result.success(inputData) } + // Just defensive code to ensure that we never have an uncaught exception that could break the queue + return try { + internalDoWork(params) + } catch (failure: Throwable) { + Timber.e(failure) + handleFailure(params, failure) + } + } + + private suspend fun internalDoWork(params: Params): Result { val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() sessionComponent.inject(this) @@ -84,8 +89,90 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter var newImageAttributes: NewImageAttributes? = null - val attachmentFile = try { - File(attachment.path) + try { + val inputStream = context.contentResolver.openInputStream(attachment.queryUri) + ?: return Result.success( + WorkerParamsFactory.toData( + params.copy( + lastFailureMessage = "Cannot openInputStream for file: " + attachment.queryUri.toString() + ) + ) + ) + + inputStream.use { + var uploadedThumbnailUrl: String? = null + var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null + + ThumbnailExtractor.extractThumbnail(context, params.attachment)?.let { thumbnailData -> + val thumbnailProgressListener = object : ProgressRequestBody.Listener { + override fun onProgress(current: Long, total: Long) { + notifyTracker(params) { contentUploadStateTracker.setProgressThumbnail(it, current, total) } + } + } + + try { + val contentUploadResponse = if (params.isEncrypted) { + Timber.v("Encrypt thumbnail") + notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) } + val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType) + uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo + fileUploader.uploadByteArray(encryptionResult.encryptedByteArray, + "thumb_${attachment.name}", + "application/octet-stream", + thumbnailProgressListener) + } else { + fileUploader.uploadByteArray(thumbnailData.bytes, + "thumb_${attachment.name}", + thumbnailData.mimeType, + thumbnailProgressListener) + } + + uploadedThumbnailUrl = contentUploadResponse.contentUri + } catch (t: Throwable) { + Timber.e(t, "Thumbnail update failed") + } + } + + val progressListener = object : ProgressRequestBody.Listener { + override fun onProgress(current: Long, total: Long) { + notifyTracker(params) { + if (isStopped) { + contentUploadStateTracker.setFailure(it, Throwable("Cancelled")) + } else { + contentUploadStateTracker.setProgress(it, current, total) + } + } + } + } + + var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null + + return try { + val contentUploadResponse = if (params.isEncrypted) { + Timber.v("Encrypt file") + notifyTracker(params) { contentUploadStateTracker.setEncrypting(it) } + + val encryptionResult = MXEncryptedAttachments.encryptAttachment(inputStream, attachment.getSafeMimeType()) + uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo + + fileUploader + .uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener) + } else { + fileUploader + .uploadByteArray(inputStream.readBytes(), attachment.name, attachment.getSafeMimeType(), progressListener) + } + + handleSuccess(params, + contentUploadResponse.contentUri, + uploadedFileEncryptedFileInfo, + uploadedThumbnailUrl, + uploadedThumbnailEncryptedFileInfo, + newImageAttributes) + } catch (t: Throwable) { + Timber.e(t) + handleFailure(params, t) + } + } } catch (e: Exception) { Timber.e(e) notifyTracker(params) { contentUploadStateTracker.setFailure(it, e) } @@ -96,109 +183,6 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter ) ) ) - } - .let { originalFile -> - if (attachment.type == ContentAttachmentData.Type.IMAGE) { - if (params.compressBeforeSending) { - Compressor.compress(context, originalFile) { - default( - width = MAX_IMAGE_SIZE, - height = MAX_IMAGE_SIZE - ) - }.also { compressedFile -> - // Update the params - val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } - BitmapFactory.decodeFile(compressedFile.absolutePath, options) - val fileSize = compressedFile.length().toInt() - - newImageAttributes = NewImageAttributes( - options.outWidth, - options.outHeight, - fileSize - ) - } - } else { - // TODO Fix here the image rotation issue - originalFile - } - } else { - // Other type - originalFile - } - } - - var uploadedThumbnailUrl: String? = null - var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null - - ThumbnailExtractor.extractThumbnail(params.attachment)?.let { thumbnailData -> - val thumbnailProgressListener = object : ProgressRequestBody.Listener { - override fun onProgress(current: Long, total: Long) { - notifyTracker(params) { contentUploadStateTracker.setProgressThumbnail(it, current, total) } - } - } - - try { - val contentUploadResponse = if (params.isEncrypted) { - Timber.v("Encrypt thumbnail") - notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) } - val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType) - uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo - fileUploader.uploadByteArray(encryptionResult.encryptedByteArray, - "thumb_${attachment.name}", - "application/octet-stream", - thumbnailProgressListener) - } else { - fileUploader.uploadByteArray(thumbnailData.bytes, - "thumb_${attachment.name}", - thumbnailData.mimeType, - thumbnailProgressListener) - } - - uploadedThumbnailUrl = contentUploadResponse.contentUri - } catch (t: Throwable) { - Timber.e(t) - return handleFailure(params, t) - } - } - - val progressListener = object : ProgressRequestBody.Listener { - override fun onProgress(current: Long, total: Long) { - notifyTracker(params) { - if (isStopped) { - contentUploadStateTracker.setFailure(it, Throwable("Cancelled")) - } else { - contentUploadStateTracker.setProgress(it, current, total) - } - } - } - } - - var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null - - return try { - val contentUploadResponse = if (params.isEncrypted) { - Timber.v("Encrypt file") - notifyTracker(params) { contentUploadStateTracker.setEncrypting(it) } - - val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.getSafeMimeType()) - uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo - - fileUploader - .uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener) - } else { - fileUploader - .uploadFile(attachmentFile, attachment.name, attachment.getSafeMimeType(), progressListener) - } - - handleSuccess(params, - contentUploadResponse.contentUri, - uploadedFileEncryptedFileInfo, - uploadedThumbnailUrl, - uploadedThumbnailEncryptedFileInfo, - newImageAttributes) - } catch (t: Throwable) { - Timber.e(t) - handleFailure(params, t) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt index 98046a4a36..dcc1de5e4f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt @@ -230,7 +230,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( private fun decryptIfNeeded(event: Event) { if (event.mxDecryptionResult == null) { try { - val result = cryptoService.decryptEvent(event, event.roomId ?: "") + val result = cryptoService.decryptEvent(event, "") event.mxDecryptionResult = OlmDecryptionResult( payload = result.clearEvent, senderKey = result.senderCurve25519Key, @@ -238,7 +238,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain ) } catch (e: MXCryptoError) { - Timber.w("Failed to decrypt e2e replace") + Timber.v("Failed to decrypt e2e replace") // TODO -> we should keep track of this and retry, or aggregation will be broken } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt index 17541de9aa..f4886c72da 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt @@ -161,7 +161,15 @@ internal class RoomSummaryUpdater @Inject constructor( roomSummaryEntity.otherMemberIds.clear() roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers) if (roomSummaryEntity.isEncrypted) { - eventBus.post(SessionToCryptoRoomMembersUpdate(roomId, roomSummaryEntity.otherMemberIds.toList() + userId)) + // The set of “all users” depends on the type of room: + // For regular / topic rooms, all users including yourself, are considered when decorating a room + // For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room + val listToCheck = if (roomSummaryEntity.isDirect) { + roomSummaryEntity.otherMemberIds.toList() + } else { + roomSummaryEntity.otherMemberIds.toList() + userId + } + eventBus.post(SessionToCryptoRoomMembersUpdate(roomId, listToCheck)) } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index f10c40ded5..a4a6eb6972 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.internal.session.room.send +import android.content.Context import android.graphics.Bitmap import android.media.MediaMetadataRetriever import androidx.exifinterface.media.ExifInterface @@ -74,6 +75,7 @@ import javax.inject.Inject * The transactionId is used as loc */ internal class LocalEchoEventFactory @Inject constructor( + private val context: Context, @UserId private val userId: String, private val stringProvider: StringProvider, private val textPillsUtils: TextPillsUtils, @@ -266,14 +268,14 @@ internal class LocalEchoEventFactory @Inject constructor( height = height?.toInt() ?: 0, size = attachment.size.toInt() ), - url = attachment.path + url = attachment.queryUri.toString() ) return createEvent(roomId, content) } private fun createVideoEvent(roomId: String, attachment: ContentAttachmentData): Event { val mediaDataRetriever = MediaMetadataRetriever() - mediaDataRetriever.setDataSource(attachment.path) + mediaDataRetriever.setDataSource(context, attachment.queryUri) // Use frame to calculate height and width as we are sure to get the right ones val firstFrame: Bitmap? = mediaDataRetriever.frameAtTime @@ -281,7 +283,7 @@ internal class LocalEchoEventFactory @Inject constructor( val width = firstFrame?.width ?: 0 mediaDataRetriever.release() - val thumbnailInfo = ThumbnailExtractor.extractThumbnail(attachment)?.let { + val thumbnailInfo = ThumbnailExtractor.extractThumbnail(context, attachment)?.let { ThumbnailInfo( width = it.width, height = it.height, @@ -299,10 +301,10 @@ internal class LocalEchoEventFactory @Inject constructor( size = attachment.size, duration = attachment.duration?.toInt() ?: 0, // Glide will be able to use the local path and extract a thumbnail. - thumbnailUrl = attachment.path, + thumbnailUrl = attachment.queryUri.toString(), thumbnailInfo = thumbnailInfo ), - url = attachment.path + url = attachment.queryUri.toString() ) return createEvent(roomId, content) } @@ -315,7 +317,7 @@ internal class LocalEchoEventFactory @Inject constructor( mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() } ?: "audio/mpeg", size = attachment.size ), - url = attachment.path + url = attachment.queryUri.toString() ) return createEvent(roomId, content) } @@ -329,7 +331,7 @@ internal class LocalEchoEventFactory @Inject constructor( ?: "application/octet-stream", size = attachment.size ), - url = attachment.path + url = attachment.queryUri.toString() ) return createEvent(roomId, content) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt index 03db817dd6..8c31dd1682 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt @@ -23,6 +23,7 @@ import androidx.work.OneTimeWorkRequest import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.internal.di.WorkManagerProvider import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon import im.vector.matrix.android.internal.worker.SessionWorkerParams @@ -49,6 +50,7 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo @Inject lateinit var workManagerProvider: WorkManagerProvider @Inject lateinit var timelineSendEventWorkCommon: TimelineSendEventWorkCommon + @Inject lateinit var localEchoUpdater: LocalEchoUpdater override suspend fun doWork(): Result { Timber.v("Start dispatch sending multiple event work") @@ -57,14 +59,17 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo Timber.e("Work cancelled due to input error from parent") } - if (params.lastFailureMessage != null) { - // Transmit the error - return Result.success(inputData) - } - val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() sessionComponent.inject(this) + if (params.lastFailureMessage != null) { + params.events.forEach { event -> + event.eventId?.let { localEchoUpdater.updateSendState(it, SendState.UNDELIVERED) } + } + // Transmit the error if needed? + return Result.success(inputData) + } + // Create a work for every event params.events.forEach { event -> if (params.isEncrypted) { 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 53bd620e51..f2bee734ce 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 @@ -171,10 +171,13 @@ internal class DefaultTimeline( val realm = Realm.getInstance(realmConfiguration) backgroundRealm.set(realm) - roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()?.also { - it.sendingTimelineEvents.addChangeListener { _ -> - postSnapshot() + roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() + roomEntity?.sendingTimelineEvents?.addChangeListener { events -> + // Remove in memory as soon as they are known by database + events.forEach { te -> + inMemorySendingEvents.removeAll { te.eventId == it.eventId } } + postSnapshot() } nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll() 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 2d6656c2e3..e129513d4b 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 @@ -114,7 +114,7 @@ internal class TimelineEventDecryptor @Inject constructor( Timber.v("Successfully decrypted event $eventId") eventEntity.setDecryptionResult(result) } catch (e: MXCryptoError) { - Timber.w(e, "Failed to decrypt event $eventId") + Timber.v(e, "Failed to decrypt event $eventId") 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 @@ -128,7 +128,7 @@ internal class TimelineEventDecryptor @Inject constructor( } } } catch (t: Throwable) { - Timber.e(t, "Failed to decrypt event $eventId") + Timber.e("Failed to decrypt event $eventId, ${t.localizedMessage}") } finally { synchronized(existingRequests) { existingRequests.remove(request) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Debouncer.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Debouncer.kt index 6a294d8d6c..575551da1b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Debouncer.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Debouncer.kt @@ -23,11 +23,9 @@ internal class Debouncer(private val handler: Handler) { private val runnables = HashMap() fun debounce(identifier: String, r: Runnable, millis: Long): Boolean { - if (runnables.containsKey(identifier)) { - // debounce - val old = runnables[identifier] - handler.removeCallbacks(old) - } + // debounce + runnables[identifier]?.let { runnable -> handler.removeCallbacks(runnable) } + insertRunnable(identifier, r, millis) return true } diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_airplane.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_airplane.xml new file mode 100644 index 0000000000..72026cd7a0 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_airplane.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_anchor.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_anchor.xml new file mode 100644 index 0000000000..b89d033b9e --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_anchor.xml @@ -0,0 +1,12 @@ + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_apple.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_apple.xml new file mode 100644 index 0000000000..54e0f9a3c0 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_apple.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_ball.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_ball.xml new file mode 100644 index 0000000000..b12c6d245b --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_ball.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_banana.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_banana.xml new file mode 100644 index 0000000000..cdd3cb1b9f --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_banana.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_bell.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_bell.xml new file mode 100644 index 0000000000..2f29828bcf --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_bell.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_bicycle.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_bicycle.xml new file mode 100644 index 0000000000..1427e793c5 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_bicycle.xml @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_book.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_book.xml new file mode 100644 index 0000000000..8e3ecc00c0 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_book.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_butterfly.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_butterfly.xml new file mode 100644 index 0000000000..d4b557a7ed --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_butterfly.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_cactus.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_cactus.xml new file mode 100644 index 0000000000..ce8aff0657 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_cactus.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_cake.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_cake.xml new file mode 100644 index 0000000000..9ebb3c0904 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_cake.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_cat.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_cat.xml new file mode 100644 index 0000000000..b34cf63d98 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_cat.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_clock.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_clock.xml new file mode 100644 index 0000000000..48d7150c36 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_clock.xml @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_cloud.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_cloud.xml new file mode 100644 index 0000000000..d390bd6e87 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_cloud.xml @@ -0,0 +1,12 @@ + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_corn.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_corn.xml new file mode 100644 index 0000000000..d863d03c2a --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_corn.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_dog.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_dog.xml new file mode 100644 index 0000000000..8346a5ebee --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_dog.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_elephant.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_elephant.xml new file mode 100644 index 0000000000..d0a2de42cb --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_elephant.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_fire.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_fire.xml new file mode 100644 index 0000000000..ebf42039b1 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_fire.xml @@ -0,0 +1,12 @@ + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_fish.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_fish.xml new file mode 100644 index 0000000000..30907f2496 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_fish.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_flag.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_flag.xml new file mode 100644 index 0000000000..250388dc4a --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_flag.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_flower.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_flower.xml new file mode 100644 index 0000000000..8a91221a80 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_flower.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_folder.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_folder.xml new file mode 100644 index 0000000000..9320766492 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_folder.xml @@ -0,0 +1,12 @@ + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_gift.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_gift.xml new file mode 100644 index 0000000000..d18c6e860a --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_gift.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_glasses.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_glasses.xml new file mode 100644 index 0000000000..8913d1ffd7 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_glasses.xml @@ -0,0 +1,12 @@ + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_globe.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_globe.xml new file mode 100644 index 0000000000..2a07829cb3 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_globe.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_guitar.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_guitar.xml new file mode 100644 index 0000000000..2622fbe416 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_guitar.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_hammer.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_hammer.xml new file mode 100644 index 0000000000..7b70654d52 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_hammer.xml @@ -0,0 +1,12 @@ + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_hat.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_hat.xml new file mode 100644 index 0000000000..15f980bdb1 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_hat.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_headphone.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_headphone.xml new file mode 100644 index 0000000000..cbc43e7601 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_headphone.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_heart.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_heart.xml new file mode 100644 index 0000000000..d37bcc33d1 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_heart.xml @@ -0,0 +1,9 @@ + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_horse.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_horse.xml new file mode 100644 index 0000000000..bedf0f6f46 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_horse.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_hourglass.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_hourglass.xml new file mode 100644 index 0000000000..8bb37a35bb --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_hourglass.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_key.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_key.xml new file mode 100644 index 0000000000..4cd1d033f7 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_key.xml @@ -0,0 +1,9 @@ + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_light_bulb.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_light_bulb.xml new file mode 100644 index 0000000000..18f3149500 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_light_bulb.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_lion.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_lion.xml new file mode 100644 index 0000000000..b97a508fc2 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_lion.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_lock.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_lock.xml new file mode 100644 index 0000000000..de3979434f --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_lock.xml @@ -0,0 +1,12 @@ + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_moon.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_moon.xml new file mode 100644 index 0000000000..3f5abe6ae3 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_moon.xml @@ -0,0 +1,12 @@ + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_mushroom.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_mushroom.xml new file mode 100644 index 0000000000..72f7036856 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_mushroom.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_octopus.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_octopus.xml new file mode 100644 index 0000000000..054760f3b8 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_octopus.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_panda.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_panda.xml new file mode 100644 index 0000000000..ab1e718c44 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_panda.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_paperclip.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_paperclip.xml new file mode 100644 index 0000000000..e8f89859d6 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_paperclip.xml @@ -0,0 +1,9 @@ + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_pencil.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_pencil.xml new file mode 100644 index 0000000000..3b9f51fca5 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_pencil.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_penguin.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_penguin.xml new file mode 100644 index 0000000000..fb2e05760f --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_penguin.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_phone.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_phone.xml new file mode 100644 index 0000000000..7beda09c4e --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_phone.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_pig.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_pig.xml new file mode 100644 index 0000000000..c31bd06c52 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_pig.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_pin.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_pin.xml new file mode 100644 index 0000000000..f10e4606a9 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_pin.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_pizza.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_pizza.xml new file mode 100644 index 0000000000..a514aeb3d6 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_pizza.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_rabbit.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_rabbit.xml new file mode 100644 index 0000000000..c8ff75c999 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_rabbit.xml @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_robot.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_robot.xml new file mode 100644 index 0000000000..a53cfe99c0 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_robot.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_rocket.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_rocket.xml new file mode 100644 index 0000000000..4097ed9030 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_rocket.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_rooster.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_rooster.xml new file mode 100644 index 0000000000..cb7ad563f0 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_rooster.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_santa.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_santa.xml new file mode 100644 index 0000000000..4f7bc1a24f --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_santa.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_scissors.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_scissors.xml new file mode 100644 index 0000000000..98e68c2071 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_scissors.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_smiley.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_smiley.xml new file mode 100644 index 0000000000..087adc8c6d --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_smiley.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_strawberry.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_strawberry.xml new file mode 100644 index 0000000000..0eeb290d9d --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_strawberry.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_thumbs_up.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_thumbs_up.xml new file mode 100644 index 0000000000..9761204ab6 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_thumbs_up.xml @@ -0,0 +1,12 @@ + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_train.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_train.xml new file mode 100644 index 0000000000..e317ce1642 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_train.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_tree.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_tree.xml new file mode 100644 index 0000000000..c5acc19a72 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_tree.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_trophy.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_trophy.xml new file mode 100644 index 0000000000..631da7320d --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_trophy.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_trumpet.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_trumpet.xml new file mode 100644 index 0000000000..84f95a8592 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_trumpet.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_turtle.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_turtle.xml new file mode 100644 index 0000000000..1cedc1b6ad --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_turtle.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_umbrella.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_umbrella.xml new file mode 100644 index 0000000000..ac1267cd3b --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_umbrella.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_unicorn.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_unicorn.xml new file mode 100644 index 0000000000..19cef5d339 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_unicorn.xml @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_wrench.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_wrench.xml new file mode 100644 index 0000000000..ba3c4313a3 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_wrench.xml @@ -0,0 +1,9 @@ + + + diff --git a/matrix-sdk-android/src/main/res/values-ca/strings.xml b/matrix-sdk-android/src/main/res/values-ca/strings.xml index 4c2f4a8a7f..c59ee3b23d 100644 --- a/matrix-sdk-android/src/main/res/values-ca/strings.xml +++ b/matrix-sdk-android/src/main/res/values-ca/strings.xml @@ -30,20 +30,20 @@ %1$s ha sol·licitat una conferència VoIP %1$s ha readmès a %2$s - %1$s ha expulsat a %2$s + %1$s ha vetat a %2$s %1$s ha retirat la invitació de %2$s %1$s ha canviat el seu avatar %1$s ha permès a %2$s veure l\'historial que es generi a partir d\'ara tots els membres de la sala, des del punt en què hi entrin. qualsevol. S\'ha iniciat la conferència VoIP - S\'ha finalitzat la conferència VoIP + S\'ha finalitzat la conferència de veu IP (s\'ha canviat també l\'avatar) %1$s ha eliminat el nom de la sala %1$s ha eliminat el tema de la sala %1$s ha actualitzat el seu perfil %2$s - %1$s ha enviat una invitació a l\'usuari %2$s per a entrar a la sala + %1$s ha enviat una invitació a %2$s per a entrar a la sala %1$s ha acceptat la invitació per a %2$s ** No s\'ha pogut desencriptar: %s ** diff --git a/matrix-sdk-android/src/main/res/values-cs/strings.xml b/matrix-sdk-android/src/main/res/values-cs/strings.xml index 61f3db0b25..7e5081b6e9 100644 --- a/matrix-sdk-android/src/main/res/values-cs/strings.xml +++ b/matrix-sdk-android/src/main/res/values-cs/strings.xml @@ -1,57 +1,57 @@ %1$s: %2$s - %1$s poslal obrázek. - %1$s poslal nálepku. + Uživatel %1$s poslal obrázek. + Uživatel %1$s poslal nálepku. - Pozvánka od %s - %1$s pozval %2$s - %1$s Vás pozval - %1$s se připojil - %1$s odešel - %1$s odmítl pozvání - %1$s vyhodil %2$s - %1$s znovu povolil vstup %2$s - %1$s zakázal vstup %2$s - %1$s stáhnul pozvání pro %2$s - %1$s změnil svůj profilový obrázek - %1$s nastavil své zobrazované jméno na %2$s - %1$s změnil své zobrazované jméno z %2$s na %3$s - %1$s odstranil své zobrazované jméno (%2$s) - %1$s změnil téma na: %2$s - %1$s změnil název místnosti na: %2$s - %s uskutečnil videohovor. - %s uskutečnil hlasový hovor. - %s přijal hovor. - %s ukončil hovor. - %1$s zviditelnil budoucí historii místnosti %2$s - všem členům místnosti od doby kdy byli pozváni. - všem členům místnosti od doby kdy se připojili. - všem členům místnosti. - komukoliv. + Pozvánka od uživatele %s + Uživatel %1$s pozval uživatele %2$s + Uživatel %1$s vás pozval + Uživatel %1$s se připojil + Uživatel %1$s odešel + Uživatel %1$s odmítl pozvání + Uživatel %1$s vykopl uživatele %2$s + Uživatel %1$s znovu povolil vstup uživateli %2$s + Uživatel %1$s vykázal uživatele %2$s + Uživatel %1$s zrušil pozvání pro uživatele %2$s + Uživatel %1$s změnil svůj profilový obrázek + Uživatel %1$s nastavil své zobrazované jméno na %2$s + Uživatel %1$s změnil své zobrazované jméno z %2$s na %3$s + Uživatel %1$s odstranil své zobrazované jméno (%2$s) + Uživatel %1$s změnil téma na: %2$s + Uživatel %1$s změnil název místnosti na: %2$s + Uživatel %s uskutečnil videohovor. + Uživatel %s uskutečnil hlasový hovor. + Uživatel %s přijal hovor. + Uživatel %s ukončil hovor. + Uživatel %1$s nastavit viditelnost budoucích zpráv v místnosti pro %2$s + všechny členy místnosti od chvíle, kdy budou pozváni. + všechny členy místnosti od chvíle, kdy se připojí. + všechny členy místnosti. + kohokoliv. neznámým (%s). - %1$s zapnul E2E šifrování (%2$s) + Uživatel %1$s zapnul end-to-end šifrování (%2$s) - %1$s požádal o VoIP konferenci + Uživatel %1$s požádal o VoIP konferenci Začala VoIP konference VoIP konference skončila (profilový obrázek byl také změněn) - %1$s odstranil název místnosti - %1$s odstranil téma místnosti - %1$s aktualizoval svůj profil %2$s - %1$s pozval %2$s aby se připojil do místnosti - %1$s přijal pozvání do %2$s + Uživatel %1$s odstranil název místnosti + Uživatel %1$s odstranil téma místnosti + Uživatel %1$s aktualizoval svůj profil %2$s + Uživatel %1$s do této místnosti pozval uživatele %2$s + Uživatel %1$s přijal pozvání pro %2$s - ** Není možno dešifrovat: %s ** - Odesílatelovo zařízení nám neposlalo klíče pro tuto zprávu. + ** Nelze dešifrovat: %s ** + Odesílatelovo zařízení neposlalo klíče pro tuto zprávu. - Odpověď na + V odpovědi na - Není možno sloučit - Není možné odeslat zprávu + Nelze vymazat + Zprávu nelze odeslat - Nepodařilo se nahrát obrázek + Obrázek nelze nahrát Chyba sítě Chyba v Matrixu @@ -74,19 +74,18 @@ %1$s a %2$s - %1$s a 1 další + %1$s a jeden další %1$s a %2$d další - %1$s a %2$d dalších - + %1$s a %2$d dalších Prázdná místnost - %s upravil/a tuto místnost. + Uživatel %s upgradoval tuto místnost. Zpráva byla smazána [důvod: %1$s] - Zpráva smazána [smazal/a %1$s] [důvod: %2$s] - "%1$s obnovil/a pozvánku do místnosti pro %2$s" + Zpráva smazána uživatelem %1$s [důvod: %2$s] + Uživatel %1$s obnovil pozvánku do místnosti pro uživatele %2$s Kočka Lev Kůň @@ -96,7 +95,7 @@ Králík Panda Kohout - Tučnák + Tučňák Želva Ryba Chobotnice @@ -105,7 +104,7 @@ Strom Kaktus Houba - Glóbus + Zeměkoule Měsíc Mrak Oheň @@ -120,14 +119,14 @@ Robot Klobouk Brýle - Santa + Santa Klaus Zvednutý palec Deštník Přesípací hodiny Hodiny Dárek Žárovka - Knížka + Kniha Tužka Sponka Nůžky @@ -137,31 +136,43 @@ Telefon Vlajka Vlak - Kolo + Jízdní kolo Letadlo Raketa - Pohár + Trofej Míč Kytara Trumpeta Zvon Kotva Sluchátka - Složka - Úvodní synchronizace: -\nStahuji účet… - Uvodní synchronizace: -\nStahuji klíče - Uvodní synchnizace: -\nStahuji místnost - Uvodní synchronizace: -\nStahuji moje místnosti - Uvodní synchonizace: -\nStahuji místnosti, které jsem opustil/a - Úvodní sychronizace: -\nImportuji komunity - Úvodní synchronizace: -\nImportuji data účtu + Desky + Úvodní synchronizace: +\nImport účtu… + Úvodní synchronizace: +\nImport klíčů + Úvodní synchronizace: +\nImport místností + Úvodní synchronizace: +\nImport místností, kterými jste členy + Úvodní synchronizace: +\nImport opuštěných místností + Úvodní synchronizace: +\nImport skupin + Úvodní synchronizace: +\nImport dat účtu - Posílám zprávu… + Odesílání zprávy… + Maticový klíč + Připínáček + + Úvodní synchronizace: +\nImport pozvánek + Vymazat frontu neodeslaných zpráv + + Uživatel %1$s pozval uživatele %2$s. Důvod: %3$s + Uživatel %1$s váš pozval. Důvod: %2$s + Uživatel %1$s odešel. Důvod: %2$s + Zpráva odstraněna + Zprávu odstranil/a %1$s diff --git a/matrix-sdk-android/src/main/res/values-de/strings.xml b/matrix-sdk-android/src/main/res/values-de/strings.xml index fa66f96aba..dc874c2b94 100644 --- a/matrix-sdk-android/src/main/res/values-de/strings.xml +++ b/matrix-sdk-android/src/main/res/values-de/strings.xml @@ -11,7 +11,7 @@ %1$s hat den Raum verlassen %1$s hat die Einladung abgelehnt %1$s hat %2$s gekickt - %1$s hat die Verbannung von %2$s aufgehoben + %1$s hat die Sperre von %2$s aufgehoben %1$s hat %2$s verbannt %1$s hat die Einladung für %2$s zurückgezogen %1$s hat das Profilbild geändert @@ -41,7 +41,7 @@ %1$s hat das Raum-Thema entfernt %1$s hat das Benutzerprofil aktualisiert %2$s %1$s hat eine Einladung an %2$s gesendet - %1$s hat die Einladung für %2$s akzeptiert + %1$s hat die Einladung in %2$s akzeptiert ** Nicht entschlüsselbar: %s ** Das absendende Gerät hat uns keine Schlüssel für diese Nachricht übermittelt. @@ -76,7 +76,7 @@ Als Antwort auf hat ein Bild gesendet. - sandte ein Video. + hat ein Video gesendet. hat eine Audio-Datei gesendet. sandte eine Datei. @@ -112,7 +112,7 @@ Pinguin Schildkröte Fisch - Tintenfisch + Oktopus Schmetterling Blume Baum @@ -128,12 +128,12 @@ Mais Kuchen Herz - Lächeln + Smiley Roboter Hut Brille Schraubenschlüssel - Nikolaus + Weihnachtsmann Daumen hoch Regenschirm Sanduhr @@ -141,10 +141,10 @@ Geschenk Glühbirne Buch - Stift + Bleistift Büroklammer - Scheren - sperren + Schere + Schloss Schlüssel Hammer Telefon @@ -183,11 +183,34 @@ %1$s ging. Grund: %2$s %1$s hat die Einladung abgelehnt. Grund: %2$s %1$s hat %2$s gekickt. Grund: %3$s - %1$s hat Verbannung für %2$s aufgehoben. Grund: %3$s + %1$s hat Sperre von %2$s aufgehoben. Grund: %3$s %1$s hat %2$s verbannt. Grund: %3$s %1$s hat eine Einladung an %2$s gesandt um diesem Raum beizutreten. Grund: %3$s %1$s hat Einladung an %2$s zu Betreten dieses Raumes zurückgezogen. Grund: %3$s %1$s hat die Einladung für %2$s angenommen. Grund: %3$s %1$s hat Einladung für %2$s verworfen. Grund: %3$s + + %1$s fügt %2$s als eine Adresse für diesen Raum hinzu. + %1$s fügt %2$s als Adressen für diesen Raum hinzu. + + + + %1$s entfernt %2$s als eine Adresse für diesen Raum. + %1$s entfernt %2$s als Adressen für diesen Raum. + + + %1$s fügt %2$s als Adresse für diesen Raum hinzu und entfernt %3$s. + + %1$s legt die Hauptadresse fest für diesen Raum als %2$s fest. + %1$s entfernt die Hauptadresse für diesen Raum. + + %1$s hat Gästen erlaubt den Raum zu betreten. + %1$s hat Gäste unterbunden den Raum zu betreten. + + %1$s aktivierte Ende-zu-Ende-Verschlüsselung. + %1$s aktivierte Ende-zu-Ende-Verschlüsselung (unbekannter Algorithmus %2$s). + + %s fordert zur Überprüfung Ihres Schlüssels auf, jedoch unterstützt Ihr Client nicht die Schlüsselüberprüfung im Chat. Sie müssen die herkömmliche Schlüsselüberprüfung verwenden, um die Schlüssel zu überprüfen. + diff --git a/matrix-sdk-android/src/main/res/values-es/strings.xml b/matrix-sdk-android/src/main/res/values-es/strings.xml index bcffeb0c87..69f02d2ef4 100644 --- a/matrix-sdk-android/src/main/res/values-es/strings.xml +++ b/matrix-sdk-android/src/main/res/values-es/strings.xml @@ -6,8 +6,8 @@ la invitación de %s %1$s invitó a %2$s - %1$s te invitó - %1$s se unió + %1$s te ha invitado + %1$s se ha unido %1$s salió %1$s rechazó la invitación %1$s expulsó a %2$s @@ -178,4 +178,45 @@ %1$s ha aceptado la invitación para %2$s. Razón: %3$s %1$s ha eliminado la dirección principal para esta sala. + %s ha actualizado la sala. + + Globo Terráqueo + Cara sonriente + Robot + Papá Noel + Pin + + Sincronización Inicial: +\nImportando criptografía + Sincronización Inicial: +\nImportando Salas a las que te has unido + Sincronización Inicial: +\nImportando Salas a las que has sido invitada + Sincronización Inicial: +\nImportando Salas Abandonadas + Invitación de %1$s. Razón: %2$s + %1$s ha desbaneado a %2$s. Razón: %3$s + %1$s envió una invitación a %2$s para que se una a la sala. Razón: %3$s + %1$s revocó la invitación de %2$s para unirse a la sala. Razón: %3$s + %1$s ha retirado la invitación de %2$s. Razón: %3$s + + + %1$s ha añadido %2$s como alias de esta sala. + %1$s ha añadido %2$s como alias de esta sala. + + + + %1$s ha quitado %2$s como alias de esta habitación. + %1$s ha quitado %2$s como alias de esta habitación. + + + %1$s ha establecido la dirección principal de esta sala a %2$s. + %1$s ha permitido que los invitados se unan a la sala. + %1$s ha impedido que los invitados se unan a la sala. + + %1$s ha activado la encriptación extremo a extremo. + %1$s ha activado la encriptación de extremo a extremo (algoritmo no reconocido %2$s). + + %s solicita verificar su clave, pero su cliente no soporta la verificación de la clave en chat. Necesitará usar la verificación de claves clásica para poder verificar las claves. + diff --git a/matrix-sdk-android/src/main/res/values-eu/strings.xml b/matrix-sdk-android/src/main/res/values-eu/strings.xml index 08b9b28332..4b2cc183a0 100644 --- a/matrix-sdk-android/src/main/res/values-eu/strings.xml +++ b/matrix-sdk-android/src/main/res/values-eu/strings.xml @@ -202,4 +202,12 @@ %1$s erabiltzaileak %2$s ezarri du gela honen helbide nagusi gisa. %1$s erabiltzaileak gela honen helbide nagusia kendu du. + %1$k gonbidatuak gelara sartzea onartu du. + %1%k gonbidatuak gelara sartzea galerazi du. + + %1$s erabiltzaileak muturretik muturrerako zifratzea gaitu du. + %1$s erabiltzaileak muturretik muturrerako zifratzea gaitu du. (%2$s algoritmo ezezaguna). + + %s(e)k zure gakoa egiaztatzea eskatu du, baina zure bezeroak ez du txatean gakoa egiaztatzea onartzen. Gako egiaztaketa zaharra erabili beharko duzu. + diff --git a/matrix-sdk-android/src/main/res/values-fa/strings.xml b/matrix-sdk-android/src/main/res/values-fa/strings.xml new file mode 100644 index 0000000000..eced727ce4 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-fa/strings.xml @@ -0,0 +1,210 @@ + + + %1$s: %2$s + %1$s تصویری فرستاد. + %1$s برچسبی فرستاد. + + دعوت %s + ‫%1$s، %2$s را دعوت کرد + %1$s دعوتتان کرد + %1$s پیوست + %1$s رفت + %1$s دعوت را رد کرد + %1$s، %2$s را اخراج کرد + %1$s، انسداد %2$s را رفع کرد + %1$s، %2$s را مسدود کرد + %1$s دعوت %2$s را نپذیرفت + %1$s تصویرش را عوض کرد + %1$s نام نمایشیش را به %2$s تنظیم کرد + %1$s نام نمایشیش را از %2$s به %3$s تغییر داد + %1$s نام نمایشیش (%2$s) را برداشت + %1$s موضوع را به %2$s تغییر داد + %1$s نام اتاق را به %2$s تغییر داد + %s یک تماس تصویری برقرار کرد. + %s یک تماس صوتی برقرار کرد. + %s تماس را پاسخ داد. + %s به تماس پایان داد. + %1$s تاریخچهٔ آیندهٔ اتاق را برای %2$s نمایان کرد + همهٔ اعضای اتاق، از زمان دعوت شدنشان. + همهٔ اعضای اتاق، از زمان پیوستنشان. + همهٔ اعضای اتاق. + هرکسی. + ناشناخته (%s). + %1$s رمزنگاری سرتاسری را روشن کرد (%2$s) + %s این اتاق را ارتقا داد. + + %1$s درخواست یک گردهمایی صوتی داد + گردهمایی صوتی آغاز شد + گردهمایی صوتی پایان یافت + + (تصویر هم عوض شد) + %1$s نام اتاق را برداشت + %1$s موضوع اتاق را برداشت + پیام برداشته شد + پیام به دست %1$s برداشته شد + پیام برداشته شد [دلیل: %1$s] + پیام به دست %1$s برداشته شد [دلیل: %2$s] + %1$s دعوتی برای پیوستن %2$s به اتاق فرستاد + %1$s دعوت پیوستن به اتاق %2$s را باطل کرد + %1$s دعوت برای %2$s را پذیرفت + + ** ناتوان در رمزگشایی: %s ** + افزارهٔ فرستنده، کلیدهای این پیام را برایمان نفرستاده است. + + در پاسخ به + + ناتوان در فرستادن پیام + + شکست در بارگذاری تصویر + + خطای شبکه + خطای ماتریس + + در حال حاضر امکان بازپیوست به اتاقی خالی وجود ندارد‌‌. + + پیام رمزنگاشته + + نشانی رایانامه + شماره تلفن + + تصویری فرستاد. + ویدیویی فرستاد. + پرونده‌ای صوتی فرستاد. + پرونده‌ای فرستاد. + + دعوت از %s + دعوت اتاق + + %1$s و %2$s + + + %1$s و ۱ نفر دیگر + %1$s و %2$d نفر دیگر + + + اتاق خالی + + + سگ + گربه + شیر + اسب + تک‌شاخ + خوک + فیل + خرگوش + پاندا + خروس + پنگوئن + لاک‌پشت + ماهی + هشت‌پا + پروانه + گل + درخت + کاکتوس + قارچ + جهان + ماه + ابر + آتش + موز + سیب + توت‌فرنگی + بلال + پیتزا + کیک + قلب + لبخندک + آدم‌آهنی + کلاه + عینک + آچار + بابانوئل + شست + چتر + ساعت شنی + ساعت + هدیه + حباب لامپ + کتاب + مداد + گیره کاغذ + قیچی + قفل + کلید + چکّش + تلفن + پرچم + قطار + دوچرخه + هواپیما + موشک + جام + توپ + گیتار + ترومپت + زنگ + لنگر + هدفون + پوشه + پونز + + همگام‌سازی نخستین: +\nدر حال درون‌ریزی حساب… + همگام‌سازی نخستین: +\nدر حال درون‌ریزی رمزنگاری + همگام‌سازی نخستین: +\nدر حال درون‌ریزی اتاق‌ها + همگام‌سازی نخستین: +\nدر حال درون‌ریزی اتاق‌های پیوسته + همگام‌سازی نخستین: +\nدر حال درون‌ریزی اتاق‌های دعوت‌شده + همگام‌سازی نخستین: +\nدر حال درون‌ریزی اتاق‌های ترک‌شده + همگام‌سازی نخستین: +\nدر حال درون‌ریزی اجتماع‌ها + همگام‌سازی نخستین: +\nدر حال درون‌ریزی داده‌های حساب + + در حال فرستادن پیام… + پاک‌سازی صفِ در حال فرستادن + + دعوت %1$s. دلیل: %2$s + %1$s، %2$s را دعوت کرد. دلیل: %3$s + %1$s دعوتتان کرد. دلیل: %2$s + %1$s پیوست. دلیل: %2$s + %1$s رفت. دلیل: %2$s + %1$s دعوت را رد کرد. دلیل: %2$s + %1$s، %2$s را اخراج کرد. دلیل: %3$s + %1$s انسداد %2$s را رفع کرد. دلیل: %3$s + %1$s، %2$s را مسدود کرد. دلیل: %3$s + %1$s دعوتی برای پیوستن %2$s به اتاق فرستاد. دلیل: %3$s + %1$s دعوت %2$s برای پیوستن به اتاق را باطل کرد. دلیل: %3$s + %1$s دعوت برای %2$s را پذیرفت. دلیل: %3$s + %1$s دعوت %2$s را نپذیرفت. دلیل: %3$s + + + %1$s، %2$s را به عنوان نشانی‌ای برای این اتاق افزود. + %1$s، %2$s را به عنوان نشانی‌هایی برای این اتاق افزود. + + + + %1$s، %2$s را به عنوان نشانی‌ای برای این اتاق برداشت. + %1$s، %3$s را به عنوان نشانی‌هایی برای این اتاق برداشت. + + + %1$s برای نشانی این اتاق، %2$s را افزود و %3$s را برداشت. + + %1$s نشانی اصلی این اتاق را به %2$s تنظیم کرد. + %1$s نشانی اصلی را برای این اتاق برداشت. + + %1$s اجازه داد میمهانان به گروه بپیوندند. + %1$s جلوی پیوستن میمهانان به گروه را گرفت. + + %1$s رمزنگاری سرتاسری را روشن کرد. + %1$s رمزنگاری سرتاسری را روشن کرد (الگوریتم تشخیص‌داده‌نشده %2$s ). + + %s درخواست تأیید کلیدتان را دارد، ولی کارخواهتان تأیید کلید درون گپ را پشتیبانی نمی‌کند. برای تأیید کلیدها لازم است از تأییدیهٔ کلید قدیمی استفاده کنید. + + diff --git a/matrix-sdk-android/src/main/res/values-fi/strings.xml b/matrix-sdk-android/src/main/res/values-fi/strings.xml index b101b91736..ba6ed1a58b 100644 --- a/matrix-sdk-android/src/main/res/values-fi/strings.xml +++ b/matrix-sdk-android/src/main/res/values-fi/strings.xml @@ -12,11 +12,11 @@ %1$s poisti porttikiellon käyttäjältä %2$s %1$s antoi porttikiellon käyttäjälle %2$s %1$s veti takaisin kutsun käyttäjälle %2$s - %1$s vaihtoi profiilikuvaa + %1$s vaihtoi profiilikuvaansa %1$s asetti näyttönimekseen %2$s %1$s muutti näyttönimensä nimestä %2$s nimeen %3$s %1$s poisti näyttönimensä (%2$s) - %1$s vaihtoi aiheeksi %2$s + %1$s vaihtoi aiheeksi: %2$s %1$s vaihtoi huoneen nimeksi %2$s %s soitti videopuhelun. %s soitti äänipuhelun. @@ -38,7 +38,7 @@ %1$s poisti huoneen nimen %1$s poisti huoneen aiheen %1$s päivitti profiilinsa %2$s - %1$s lähetti liittymiskutsun käyttäjälle %2$s + %1$s lähetti liittymiskutsun huoneeseen käyttäjälle %2$s %1$s hyväksyi kutsun käyttäjän %2$s puolesta ** Salauksen purku epäonnistui: %s ** Lähettäjän laite ei ole lähettänyt avaimia tähän viestiin. @@ -180,12 +180,30 @@ %1$s liittyi. Syy: %2$s %1$s poistui. Syy: %2$s %1$s hylkäsi kutsun. Syy: %2$s - %1$s potkaisi käyttäjän %2$s pois. Syy: %3$s - %1$s poisti eston käyttäjältä %2$s. Syy: %3$s - %1$s esti käyttäjän %2$s. Syy: %3$s - %1$s lähetti kutsun käyttäjälle %2$s huoneeseen liittymiseksi. Syy: %3$s - %1$s kumosi kutsun käyttäjälle %2$s huoneeseen liittymiseksi. Syy: %3$s + %1$s poisti käyttäjän %2$s huoneesta. Syy: %3$s + %1$s poisti porttikiellon käyttäjältä %2$s. Syy: %3$s + %1$s antoi porttikiellon käyttäjälle %2$s. Syy: %3$s + %1$s lähetti kutsun liittyä huoneeseen käyttäjälle %2$s. Syy: %3$s + %1$s kumosi kutsun liittyä huoneeseen käyttäjälle %2$s. Syy: %3$s %1$s hyväksyi kutsun liityäkseen huoneeseen %2$s. Syy: %3$s - %1$s poisti käyttäjän %2$s kutsun. Syy: %3$s + %1$s veti takaisin käyttäjän %2$s kutsun. Syy: %3$s + + + %1$s lisäsi tälle huoneelle osoitteen %2$s. + %1$s lisäsi tälle huoneelle osoitteet %2$s. + + + + %1$s poisti tältä huoneelta osoitteen %2$s. + %1$s poisti tältä huoneelta osoitteet %3$s. + + + %1$s lisäsi tälle huoneelle osoitteen %2$s ja poisti osoitteen %3$s. + + %1$s asetti tämän huoneen pääosoitteeksi %2$s. + %1$s poisti tämän huoneen pääosoitteen. + + %1$s salli vieraiden liittyä huoneeseen. + %1$s esti vieraita liittymästä huoneeseen. diff --git a/matrix-sdk-android/src/main/res/values-fr/strings.xml b/matrix-sdk-android/src/main/res/values-fr/strings.xml index 73341fe57b..257cb82e71 100644 --- a/matrix-sdk-android/src/main/res/values-fr/strings.xml +++ b/matrix-sdk-android/src/main/res/values-fr/strings.xml @@ -202,4 +202,12 @@ %1$s a défini %2$s comme adresse principale pour ce salon. %1$s a supprimé l’adresse principale de ce salon. + %1$s a autorisé les visiteurs à rejoindre le salon. + %1$s a empêché les visiteurs de rejoindre le salon. + + %1$s a activé le chiffrement de bout en bout. + %1$s a activé le chiffrement de bout en bout (algorithme %2$s inconnu). + + %s demande à vérifier votre clé, mais votre client ne supporte pas la vérification de clés dans les discussions. Vous devrez utiliser l’ancienne vérification de clés pour vérifier les clés. + diff --git a/matrix-sdk-android/src/main/res/values-hu/strings.xml b/matrix-sdk-android/src/main/res/values-hu/strings.xml index 1367b8bf43..aaa92ee398 100644 --- a/matrix-sdk-android/src/main/res/values-hu/strings.xml +++ b/matrix-sdk-android/src/main/res/values-hu/strings.xml @@ -201,4 +201,12 @@ %1$s a szoba elsődleges címét erre állította be: %2$s. %1$s eltávolította a szoba elsődleges címét. + %1$s megengedte a vendégeknek, hogy belépjenek ebbe a szobába. + %1$s megtiltotta a vendégeknek, hogy belépjenek ebbe a szobába. + + %1$s bekapcsolta a végpontok közötti titkosítást. + %1$s bekapcsolta a végpontok közötti titkosítást (ismeretlen algoritmus %2$s). + + %s kéri a kulcsok ellenőrzését de a kliens nem támogatja a szobán belüli kulcs ellenőrzést. A hagyományos módon kell ellenőrizned a kulcsokat. + diff --git a/matrix-sdk-android/src/main/res/values-it/strings.xml b/matrix-sdk-android/src/main/res/values-it/strings.xml index 2d726272c0..8c54924d9b 100644 --- a/matrix-sdk-android/src/main/res/values-it/strings.xml +++ b/matrix-sdk-android/src/main/res/values-it/strings.xml @@ -202,4 +202,12 @@ %1$s ha impostato l\'indirizzo principale per questa stanza a %2$s. %1$s ha rimosso l\'indirizzo principale per questa stanza. + %1$s ha permesso l\'accesso alla stanza per gli ospiti. + %1$s ha impedito l\'accesso alla stanza per gli ospiti. + + %1$s ha attivato la cifratura end-to-end. + %1$s ha attivato la cifratura end-to-end (algoritmo %2$s non riconosciuto). + + %s sta chiedendo di verificare la tua chiave, ma il tuo client non supporta la verifica in-chat. Dovrai usare il metodo di verifica obsoleto per verificare le chiavi. + diff --git a/matrix-sdk-android/src/main/res/values-nl/strings.xml b/matrix-sdk-android/src/main/res/values-nl/strings.xml index 179ee89698..d08b3c7845 100644 --- a/matrix-sdk-android/src/main/res/values-nl/strings.xml +++ b/matrix-sdk-android/src/main/res/values-nl/strings.xml @@ -182,4 +182,41 @@ Uitgaande wachtrij legen %1$s heeft de uitnodiging voor %2$s om het gesprek toe te treden ingetrokken + Uitnodiging van %1$s. Reden: %2$s + %1$s heeft %2$s uitgenodigd. Reden: %3$s + %1$s heeft u uitgenodigd. Reden: %2$s + %1$s neemt nu deel. Reden: %2$s + %1$s is weggegaan. Reden: %2$s + %1$s heeft de uitnodiging geweigerd. Reden: %2$s + %1$s heeft %2$s verwijderd. Reden: %3$s + %1$s heeft %2$s ontbannen. Reden: %3$s + %1$s heeft %2$s verbannen. Reden: %3$s + %1$s heeft %2$s een uitnodiging voor het gesprek gestuurd. Reden: %3$s + %1$s heeft de uitnodiging voor %2$s ingetrokken. Reden: %3$s + %1$s heeft de uitnodiging voor %2$s aanvaard. Reden: %3$s + %1$s heeft de uitnodiging van %2$s ingetrokken. Reden: %3$s + + + %1$s heeft %2$s als gespreksadres toegevoegd. + %1$s heeft %2$s als gespreksadressen toegevoegd. + + + + %1$s heeft %2$s als gespreksadres verwijderd. + %1$s heeft %3$s als gespreksadressen verwijderd. + + + %1$s heeft %2$s als gespreksadres toegevoegd en %3$s verwijderd. + + %1$s heeft het hoofdadres voor dit gesprek ingesteld op %2$s. + %1$s heeft het hoofdadres voor dit gesprek verwijderd. + + %1$s heeft gasten de toegang tot het gesprek verleend. + %1$s heeft gasten de toegang tot het gesprek verhinderd. + + %1$s heeft eind-tot-eind-versleuteling ingeschakeld. + %1$s heeft eind-tot-eind-versleuteling ingeschakeld (onbekend algoritme %2$s). + + %s vraagt om uw sleutel te verifiëren, maar uw cliënt biedt geen ondersteuning voor verificatie in het gesprek. U zult de verouderde sleutelverificatie moeten gebruiken om de sleutels te verifiëren. + diff --git a/matrix-sdk-android/src/main/res/values-nn/strings.xml b/matrix-sdk-android/src/main/res/values-nn/strings.xml index a8490200d3..d61849651d 100644 --- a/matrix-sdk-android/src/main/res/values-nn/strings.xml +++ b/matrix-sdk-android/src/main/res/values-nn/strings.xml @@ -8,10 +8,10 @@ %1$s sende eit klistremerke. %s si innbjoding - %1$s baud %2$s inn - %1$s baud deg inn + %1$s inviterte %2$s + %1$s inviterte deg %1$s kom inn - %1$s fór ut + %1$s forlot rommet %1$s sa nei til innbjodingi %1$s sparka %2$s %1$s slapp %2$s inn att @@ -24,13 +24,13 @@ %1$s gjorde emnet til: %2$s %1$s gjorde romnamnet til: %2$s %s starta ei videosamtala. - %s starta ei røystsamtala. + %s starta eit taleanrop. %s tok røyret. %s la på røyret. %1$s gjorde den framtidige romsoga synleg for %2$s - alle rommedlemer, frå då dei vart bodne inn. - alle rommedlemer, frå då dei kom inn. - alle rommedlemer. + alle rommedlemmar, frå då dei vart invitert inn. + alle rommedlemmar, frå då dei kom inn. + alle rommedlemmar. kven som heldst. uvisst (%s). %1$s skrudde ende-til-ende-kryptering på (%2$s) @@ -43,7 +43,7 @@ %1$s tok burt romnamnet %1$s tok burt romemnet %1$s gjorde um på skildringi si %2$s - %1$s baud %2$s inn i romet + %1$s inviterte %2$s til rommet %1$s sa ja til innbjodingi til %2$s ** Fekk ikkje til å dekryptera: %s ** @@ -149,4 +149,9 @@ Mappa Nål + %s oppgraderte rommet. + + Nullstill sendingskø + + %1$s forlot rommet. Grunn: %2$s diff --git a/matrix-sdk-android/src/main/res/values-sq/strings.xml b/matrix-sdk-android/src/main/res/values-sq/strings.xml index d4a28e28d7..e966f22064 100644 --- a/matrix-sdk-android/src/main/res/values-sq/strings.xml +++ b/matrix-sdk-android/src/main/res/values-sq/strings.xml @@ -198,4 +198,12 @@ %1$s caktoi %2$s si adresë kryesore për këtë dhomë. %1$s hoqi adresën kryesore për këtë dhomë. + %1$s ka lejuar vizitorë të marrin pjesë në dhomë. + %1$s ka penguar vizitorë të marrin pjesë në dhomë. + + %1$s aktivizoi fshehtëzim skaj-më-skaj. + %1$s aktivizoi fshehtëzim skaj-më-skaj (algoritëm i papranuar %2$s). + + %s po kërkon të verifikojë kyçin tuaj, por klienti juaj nuk mbulon verifikim kyçesh brenda fjalosjeje. Që të verifikoni kyça, do t’ju duhet të përdorni verifikim të dikurshëm kyçesh. + diff --git a/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml b/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml index 7182b07080..7b2f3563a2 100644 --- a/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml +++ b/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml @@ -198,4 +198,12 @@ %1$s 為此聊天室設定了 %2$s 為主地址。 %1$s 為此聊天室移除了主要地址。 + %1$s 已允許訪客加入聊天室。 + %1$s 已禁止訪客加入聊天室。 + + %1$s 已開啟端到端加密。 + %1$s 已開啟端到端加密(無法識別的演算法 %2$s)。 + + %s 正在請求驗證您的金鑰,但您的客戶端不支援聊天中金鑰驗證。您將必須使用舊版的金鑰驗證來驗證金鑰。 + diff --git a/multipicker/.gitignore b/multipicker/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/multipicker/.gitignore @@ -0,0 +1 @@ +/build diff --git a/multipicker/build.gradle b/multipicker/build.gradle new file mode 100644 index 0000000000..8b08a9d3ef --- /dev/null +++ b/multipicker/build.gradle @@ -0,0 +1,56 @@ +/* + * 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. + */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion 29 + + defaultConfig { + minSdkVersion 19 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles 'consumer-rules.pro' + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.core:core-ktx:1.2.0' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + + implementation 'androidx.exifinterface:exifinterface:1.3.0-alpha01' + + // Log + implementation 'com.jakewharton.timber:timber:4.7.1' +} diff --git a/multipicker/consumer-rules.pro b/multipicker/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/multipicker/proguard-rules.pro b/multipicker/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/multipicker/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/multipicker/src/main/AndroidManifest.xml b/multipicker/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..e1f12697e0 --- /dev/null +++ b/multipicker/src/main/AndroidManifest.xml @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/AudioPicker.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/AudioPicker.kt new file mode 100644 index 0000000000..05e4c337b6 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/AudioPicker.kt @@ -0,0 +1,93 @@ +/* + * 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.multipicker + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.media.MediaMetadataRetriever +import android.provider.MediaStore +import im.vector.riotx.multipicker.entity.MultiPickerAudioType + +/** + * Audio file picker implementation + */ +class AudioPicker(override val requestCode: Int) : Picker(requestCode) { + + /** + * Call this function from onActivityResult(int, int, Intent). + * Returns selected audio files or empty list if request code is wrong + * or result code is not Activity.RESULT_OK + * or user did not select any files. + */ + override fun getSelectedFiles(context: Context, requestCode: Int, resultCode: Int, data: Intent?): List { + if (requestCode != this.requestCode && resultCode != Activity.RESULT_OK) { + return emptyList() + } + + val audioList = mutableListOf() + + getSelectedUriList(data).forEach { selectedUri -> + val projection = arrayOf( + MediaStore.Audio.Media.DISPLAY_NAME, + MediaStore.Audio.Media.SIZE + ) + + context.contentResolver.query( + selectedUri, + projection, + null, + null, + null + )?.use { cursor -> + val nameColumn = cursor.getColumnIndex(MediaStore.Audio.Media.DISPLAY_NAME) + val sizeColumn = cursor.getColumnIndex(MediaStore.Audio.Media.SIZE) + + if (cursor.moveToNext()) { + val name = cursor.getString(nameColumn) + val size = cursor.getLong(sizeColumn) + var duration = 0L + + context.contentResolver.openFileDescriptor(selectedUri, "r")?.use { pfd -> + val mediaMetadataRetriever = MediaMetadataRetriever() + mediaMetadataRetriever.setDataSource(pfd.fileDescriptor) + duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION).toLong() + } + + audioList.add( + MultiPickerAudioType( + name, + size, + context.contentResolver.getType(selectedUri), + selectedUri, + duration + ) + ) + } + } + } + return audioList + } + + override fun createIntent(): Intent { + return Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !single) + type = "audio/*" + } + } +} diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/CameraPicker.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/CameraPicker.kt new file mode 100644 index 0000000000..240d809373 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/CameraPicker.kt @@ -0,0 +1,130 @@ +/* + * 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.multipicker + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.provider.MediaStore +import androidx.core.content.FileProvider +import androidx.fragment.app.Fragment +import im.vector.riotx.multipicker.entity.MultiPickerImageType +import im.vector.riotx.multipicker.utils.ImageUtils +import java.io.File +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +/** + * Implementation of taking a photo with Camera + */ +class CameraPicker(val requestCode: Int) { + + /** + * Start camera by using an Activity + * @param activity Activity to handle onActivityResult(). + * @return Uri of taken photo or null if the operation is cancelled. + */ + fun startWithExpectingFile(activity: Activity): Uri? { + val photoUri = createPhotoUri(activity) + val intent = createIntent().apply { + putExtra(MediaStore.EXTRA_OUTPUT, photoUri) + } + activity.startActivityForResult(intent, requestCode) + return photoUri + } + + /** + * Start camera by using a Fragment + * @param fragment Fragment to handle onActivityResult(). + * @return Uri of taken photo or null if the operation is cancelled. + */ + fun startWithExpectingFile(fragment: Fragment): Uri? { + val photoUri = createPhotoUri(fragment.requireContext()) + val intent = createIntent().apply { + putExtra(MediaStore.EXTRA_OUTPUT, photoUri) + } + fragment.startActivityForResult(intent, requestCode) + return photoUri + } + + /** + * Call this function from onActivityResult(int, int, Intent). + * @return Taken photo or null if request code is wrong + * or result code is not Activity.RESULT_OK + * or user cancelled the operation. + */ + fun getTakenPhoto(context: Context, requestCode: Int, resultCode: Int, photoUri: Uri): MultiPickerImageType? { + if (requestCode == this.requestCode && resultCode == Activity.RESULT_OK) { + val projection = arrayOf( + MediaStore.Images.Media.DISPLAY_NAME, + MediaStore.Images.Media.SIZE + ) + + context.contentResolver.query( + photoUri, + projection, + null, + null, + null + )?.use { cursor -> + val nameColumn = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME) + val sizeColumn = cursor.getColumnIndex(MediaStore.Images.Media.SIZE) + + if (cursor.moveToNext()) { + val name = cursor.getString(nameColumn) + val size = cursor.getLong(sizeColumn) + + val bitmap = ImageUtils.getBitmap(context, photoUri) + val orientation = ImageUtils.getOrientation(context, photoUri) + + return MultiPickerImageType( + name, + size, + context.contentResolver.getType(photoUri), + photoUri, + bitmap?.width ?: 0, + bitmap?.height ?: 0, + orientation + ) + } + } + } + return null + } + + private fun createIntent(): Intent { + return Intent(MediaStore.ACTION_IMAGE_CAPTURE) + } + + private fun createPhotoUri(context: Context): Uri { + val file = createImageFile(context) + val authority = context.packageName + ".multipicker.fileprovider" + return FileProvider.getUriForFile(context, authority, file) + } + + private fun createImageFile(context: Context): File { + val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) + val storageDir: File = context.filesDir + return File.createTempFile( + "${timeStamp}_", /* prefix */ + ".jpg", /* suffix */ + storageDir /* directory */ + ) + } +} diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/ContactPicker.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/ContactPicker.kt new file mode 100644 index 0000000000..b0ae0e4cda --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/ContactPicker.kt @@ -0,0 +1,135 @@ +/* + * 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.multipicker + +import android.app.Activity +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.provider.ContactsContract +import im.vector.riotx.multipicker.entity.MultiPickerContactType + +/** + * Contact Picker implementation + */ +class ContactPicker(override val requestCode: Int) : Picker(requestCode) { + + /** + * Call this function from onActivityResult(int, int, Intent). + * Returns selected contact or empty list if request code is wrong + * or result code is not Activity.RESULT_OK + * or user did not select any files. + */ + override fun getSelectedFiles(context: Context, requestCode: Int, resultCode: Int, data: Intent?): List { + if (requestCode != this.requestCode && resultCode != Activity.RESULT_OK) { + return emptyList() + } + + val contactList = mutableListOf() + + data?.data?.let { selectedUri -> + val projection = arrayOf( + ContactsContract.Contacts.DISPLAY_NAME, + ContactsContract.Contacts.PHOTO_URI, + ContactsContract.Contacts._ID + ) + + context.contentResolver.query( + selectedUri, + projection, + null, + null, + null + )?.use { cursor -> + if (cursor.moveToFirst()) { + val idColumn = cursor.getColumnIndex(ContactsContract.Contacts._ID) + val nameColumn = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME) + val photoUriColumn = cursor.getColumnIndex(ContactsContract.Contacts.PHOTO_URI) + + val contactId = cursor.getInt(idColumn) + var name = cursor.getString(nameColumn) + var photoUri = cursor.getString(photoUriColumn) + var phoneNumberList = mutableListOf() + var emailList = mutableListOf() + + getRawContactId(context.contentResolver, contactId)?.let { rawContactId -> + val selection = ContactsContract.Data.RAW_CONTACT_ID + " = ?" + val selectionArgs = arrayOf(rawContactId.toString()) + + context.contentResolver.query( + ContactsContract.Data.CONTENT_URI, + arrayOf( + ContactsContract.Data.MIMETYPE, + ContactsContract.Data.DATA1 + ), + selection, + selectionArgs, + null + )?.use { cursor -> + while (cursor.moveToNext()) { + val mimeType = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.MIMETYPE)) + val contactData = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.DATA1)) + + if (mimeType == ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) { + name = contactData + } + if (mimeType == ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE) { + phoneNumberList.add(contactData) + } + if (mimeType == ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE) { + emailList.add(contactData) + } + } + } + } + contactList.add( + MultiPickerContactType( + name, + photoUri, + phoneNumberList, + emailList + ) + ) + } + } + } + + return contactList + } + + private fun getRawContactId(contentResolver: ContentResolver, contactId: Int): Int? { + val projection = arrayOf(ContactsContract.RawContacts._ID) + val selection = ContactsContract.RawContacts.CONTACT_ID + " = ?" + val selectionArgs = arrayOf(contactId.toString() + "") + return contentResolver.query( + ContactsContract.RawContacts.CONTENT_URI, + projection, + selection, + selectionArgs, + null + )?.use { cursor -> + return if (cursor.moveToFirst()) cursor.getInt(cursor.getColumnIndex(ContactsContract.RawContacts._ID)) else null + } + } + + override fun createIntent(): Intent { + return Intent(Intent.ACTION_PICK).apply { + putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !single) + type = ContactsContract.Contacts.CONTENT_TYPE + } + } +} diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/FilePicker.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/FilePicker.kt new file mode 100644 index 0000000000..e8c74fad19 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/FilePicker.kt @@ -0,0 +1,73 @@ +/* + * 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.multipicker + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.provider.OpenableColumns +import im.vector.riotx.multipicker.entity.MultiPickerFileType + +/** + * Implementation of selecting any type of files + */ +class FilePicker(override val requestCode: Int) : Picker(requestCode) { + + /** + * Call this function from onActivityResult(int, int, Intent). + * Returns selected files or empty list if request code is wrong + * or result code is not Activity.RESULT_OK + * or user did not select any files. + */ + override fun getSelectedFiles(context: Context, requestCode: Int, resultCode: Int, data: Intent?): List { + if (requestCode != this.requestCode && resultCode != Activity.RESULT_OK) { + return emptyList() + } + + val fileList = mutableListOf() + + getSelectedUriList(data).forEach { selectedUri -> + context.contentResolver.query(selectedUri, null, null, null, null) + ?.use { cursor -> + val nameColumn = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + val sizeColumn = cursor.getColumnIndex(OpenableColumns.SIZE) + if (cursor.moveToFirst()) { + val name = cursor.getString(nameColumn) + val size = cursor.getLong(sizeColumn) + + fileList.add( + MultiPickerFileType( + name, + size, + context.contentResolver.getType(selectedUri), + selectedUri + ) + ) + } + } + } + return fileList + } + + override fun createIntent(): Intent { + return Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !single) + type = "*/*" + } + } +} diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/ImagePicker.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/ImagePicker.kt new file mode 100644 index 0000000000..d7bf383f03 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/ImagePicker.kt @@ -0,0 +1,91 @@ +/* + * 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.multipicker + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.provider.MediaStore +import im.vector.riotx.multipicker.entity.MultiPickerImageType +import im.vector.riotx.multipicker.utils.ImageUtils + +/** + * Image Picker implementation + */ +class ImagePicker(override val requestCode: Int) : Picker(requestCode) { + + /** + * Call this function from onActivityResult(int, int, Intent). + * Returns selected image files or empty list if request code is wrong + * or result code is not Activity.RESULT_OK + * or user did not select any files. + */ + override fun getSelectedFiles(context: Context, requestCode: Int, resultCode: Int, data: Intent?): List { + if (requestCode != this.requestCode && resultCode != Activity.RESULT_OK) { + return emptyList() + } + + val imageList = mutableListOf() + + getSelectedUriList(data).forEach { selectedUri -> + val projection = arrayOf( + MediaStore.Images.Media.DISPLAY_NAME, + MediaStore.Images.Media.SIZE + ) + + context.contentResolver.query( + selectedUri, + projection, + null, + null, + null + )?.use { cursor -> + val nameColumn = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME) + val sizeColumn = cursor.getColumnIndex(MediaStore.Images.Media.SIZE) + + if (cursor.moveToNext()) { + val name = cursor.getString(nameColumn) + val size = cursor.getLong(sizeColumn) + + val bitmap = ImageUtils.getBitmap(context, selectedUri) + val orientation = ImageUtils.getOrientation(context, selectedUri) + + imageList.add( + MultiPickerImageType( + name, + size, + context.contentResolver.getType(selectedUri), + selectedUri, + bitmap?.width ?: 0, + bitmap?.height ?: 0, + orientation + ) + ) + } + } + } + return imageList + } + + override fun createIntent(): Intent { + return Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !single) + type = "image/*" + } + } +} diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/MultiPicker.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/MultiPicker.kt new file mode 100644 index 0000000000..24769e11c3 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/MultiPicker.kt @@ -0,0 +1,49 @@ +/* + * 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.multipicker + +class MultiPicker { + + companion object Type { + val IMAGE by lazy { MultiPicker() } + val FILE by lazy { MultiPicker() } + val VIDEO by lazy { MultiPicker() } + val AUDIO by lazy { MultiPicker() } + val CONTACT by lazy { MultiPicker() } + val CAMERA by lazy { MultiPicker() } + + const val REQUEST_CODE_PICK_IMAGE = 5000 + const val REQUEST_CODE_PICK_VIDEO = 5001 + const val REQUEST_CODE_PICK_FILE = 5002 + const val REQUEST_CODE_PICK_AUDIO = 5003 + const val REQUEST_CODE_PICK_CONTACT = 5004 + const val REQUEST_CODE_TAKE_PHOTO = 5005 + + @Suppress("UNCHECKED_CAST") + fun get(type: MultiPicker): T { + return when (type) { + IMAGE -> ImagePicker(REQUEST_CODE_PICK_IMAGE) as T + VIDEO -> VideoPicker(REQUEST_CODE_PICK_VIDEO) as T + FILE -> FilePicker(REQUEST_CODE_PICK_FILE) as T + AUDIO -> AudioPicker(REQUEST_CODE_PICK_AUDIO) as T + CONTACT -> ContactPicker(REQUEST_CODE_PICK_CONTACT) as T + CAMERA -> CameraPicker(REQUEST_CODE_TAKE_PHOTO) as T + else -> throw IllegalArgumentException("Unsupported type $type") + } + } + } +} diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/Picker.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/Picker.kt new file mode 100644 index 0000000000..43ac5d5fdd --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/Picker.kt @@ -0,0 +1,116 @@ +/* + * 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.multipicker + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo +import android.net.Uri +import androidx.fragment.app.Fragment + +/** + * Abstract class to provide all types of Pickers + */ +abstract class Picker(open val requestCode: Int) { + + protected var single = false + + /** + * Call this function from onActivityResult(int, int, Intent). + * @return selected files or empty list if request code is wrong + * or result code is not Activity.RESULT_OK + * or user did not select any files. + */ + abstract fun getSelectedFiles(context: Context, requestCode: Int, resultCode: Int, data: Intent?): List + + /** + * Use this function to retrieve files which are shared from another application or internally + * by using android.intent.action.SEND or android.intent.action.SEND_MULTIPLE actions. + */ + fun getIncomingFiles(context: Context, data: Intent?): List { + if (data == null) return emptyList() + + val uriList = mutableListOf() + if (data.action == Intent.ACTION_SEND) { + (data.getParcelableExtra(Intent.EXTRA_STREAM) as? Uri)?.let { uriList.add(it) } + } else if (data.action == Intent.ACTION_SEND_MULTIPLE) { + val extraUriList: List? = data.getParcelableArrayListExtra(Intent.EXTRA_STREAM) + extraUriList?.let { uriList.addAll(it) } + } + + val resInfoList: List = context.packageManager.queryIntentActivities(data, PackageManager.MATCH_DEFAULT_ONLY) + uriList.forEach { + for (resolveInfo in resInfoList) { + val packageName: String = resolveInfo.activityInfo.packageName + context.grantUriPermission(packageName, it, Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + } + return getSelectedFiles(context, requestCode, Activity.RESULT_OK, data) + } + + /** + * Call this function to disable multiple selection of files. + */ + fun single(): Picker { + single = true + return this + } + + abstract fun createIntent(): Intent + + /** + * Start Storage Access Framework UI by using an Activity. + * @param activity Activity to handle onActivityResult(). + */ + fun startWith(activity: Activity) { + activity.startActivityForResult(createIntent().apply { addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) }, requestCode) + } + + /** + * Start Storage Access Framework UI by using a Fragment. + * @param fragment Fragment to handle onActivityResult(). + */ + fun startWith(fragment: Fragment) { + fragment.startActivityForResult(createIntent().apply { addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) }, requestCode) + } + + protected fun getSelectedUriList(data: Intent?): List { + val selectedUriList = mutableListOf() + val dataUri = data?.data + val clipData = data?.clipData + + if (clipData != null) { + for (i in 0 until clipData.itemCount) { + selectedUriList.add(clipData.getItemAt(i).uri) + } + } else if (dataUri != null) { + selectedUriList.add(dataUri) + } else { + data?.extras?.get(Intent.EXTRA_STREAM)?.let { + (it as? List<*>)?.filterIsInstance()?.let { uriList -> + selectedUriList.addAll(uriList) + } + if (it is Uri) { + selectedUriList.add(it) + } + } + } + return selectedUriList + } +} diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/VideoPicker.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/VideoPicker.kt new file mode 100644 index 0000000000..b85ffacd48 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/VideoPicker.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.riotx.multipicker + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.media.MediaMetadataRetriever +import android.provider.MediaStore +import im.vector.riotx.multipicker.entity.MultiPickerVideoType + +/** + * Video Picker implementation + */ +class VideoPicker(override val requestCode: Int) : Picker(requestCode) { + + /** + * Call this function from onActivityResult(int, int, Intent). + * Returns selected video files or empty list if request code is wrong + * or result code is not Activity.RESULT_OK + * or user did not select any files. + */ + override fun getSelectedFiles(context: Context, requestCode: Int, resultCode: Int, data: Intent?): List { + if (requestCode != this.requestCode && resultCode != Activity.RESULT_OK) { + return emptyList() + } + + val videoList = mutableListOf() + + getSelectedUriList(data).forEach { selectedUri -> + val projection = arrayOf( + MediaStore.Video.Media.DISPLAY_NAME, + MediaStore.Video.Media.SIZE + ) + + context.contentResolver.query( + selectedUri, + projection, + null, + null, + null + )?.use { cursor -> + val nameColumn = cursor.getColumnIndex(MediaStore.Video.Media.DISPLAY_NAME) + val sizeColumn = cursor.getColumnIndex(MediaStore.Video.Media.SIZE) + + if (cursor.moveToNext()) { + val name = cursor.getString(nameColumn) + val size = cursor.getLong(sizeColumn) + var duration = 0L + var width = 0 + var height = 0 + var orientation = 0 + + context.contentResolver.openFileDescriptor(selectedUri, "r")?.use { pfd -> + val mediaMetadataRetriever = MediaMetadataRetriever() + mediaMetadataRetriever.setDataSource(pfd.fileDescriptor) + duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION).toLong() + width = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH).toInt() + height = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT).toInt() + orientation = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION).toInt() + } + + videoList.add( + MultiPickerVideoType( + name, + size, + context.contentResolver.getType(selectedUri), + selectedUri, + width, + height, + orientation, + duration + ) + ) + } + } + } + return videoList + } + + override fun createIntent(): Intent { + return Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !single) + type = "video/*" + } + } +} diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerAudioType.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerAudioType.kt new file mode 100644 index 0000000000..6afe022024 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerAudioType.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.riotx.multipicker.entity + +import android.net.Uri + +data class MultiPickerAudioType( + override val displayName: String?, + override val size: Long, + override val mimeType: String?, + override val contentUri: Uri, + val duration: Long +) : MultiPickerBaseType diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerBaseType.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerBaseType.kt new file mode 100644 index 0000000000..777e4d8441 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerBaseType.kt @@ -0,0 +1,26 @@ +/* + * 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.multipicker.entity + +import android.net.Uri + +interface MultiPickerBaseType { + val displayName: String? + val size: Long + val mimeType: String? + val contentUri: Uri +} diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerContactType.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerContactType.kt new file mode 100644 index 0000000000..a9135443a2 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerContactType.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.riotx.multipicker.entity + +data class MultiPickerContactType( + val displayName: String, + val photoUri: String?, + val phoneNumberList: List, + val emailList: List +) diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerFileType.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerFileType.kt new file mode 100644 index 0000000000..5417520d28 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerFileType.kt @@ -0,0 +1,26 @@ +/* + * 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.multipicker.entity + +import android.net.Uri + +data class MultiPickerFileType( + override val displayName: String?, + override val size: Long, + override val mimeType: String?, + override val contentUri: Uri +) : MultiPickerBaseType diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerImageType.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerImageType.kt new file mode 100644 index 0000000000..b1aef171b4 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerImageType.kt @@ -0,0 +1,29 @@ +/* + * 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.multipicker.entity + +import android.net.Uri + +data class MultiPickerImageType( + override val displayName: String?, + override val size: Long, + override val mimeType: String?, + override val contentUri: Uri, + val width: Int, + val height: Int, + val orientation: Int +) : MultiPickerBaseType diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerVideoType.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerVideoType.kt new file mode 100644 index 0000000000..ba9a8d233e --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerVideoType.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.riotx.multipicker.entity + +import android.net.Uri + +data class MultiPickerVideoType( + override val displayName: String?, + override val size: Long, + override val mimeType: String?, + override val contentUri: Uri, + val width: Int, + val height: Int, + val orientation: Int, + val duration: Long +) : MultiPickerBaseType diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/provider/MultiPickerFileProvider.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/provider/MultiPickerFileProvider.kt new file mode 100644 index 0000000000..048b2ca199 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/provider/MultiPickerFileProvider.kt @@ -0,0 +1,21 @@ +/* + * 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.multipicker.provider + +import androidx.core.content.FileProvider + +class MultiPickerFileProvider : FileProvider() diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/utils/ImageUtils.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/utils/ImageUtils.kt new file mode 100644 index 0000000000..c5171e7d84 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/utils/ImageUtils.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.riotx.multipicker.utils + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.ImageDecoder +import android.net.Uri +import android.os.Build +import androidx.exifinterface.media.ExifInterface +import timber.log.Timber + +object ImageUtils { + + fun getBitmap(context: Context, uri: Uri): Bitmap? { + return try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + ImageDecoder.decodeBitmap(ImageDecoder.createSource(context.contentResolver, uri)) + } else { + context.contentResolver.openInputStream(uri)?.use { inputStream -> + BitmapFactory.decodeStream(inputStream) + } + } + } catch (e: Exception) { + Timber.e(e, "Cannot decode Bitmap: %s", uri.toString()) + null + } + } + + fun getOrientation(context: Context, uri: Uri): Int { + var orientation = 0 + context.contentResolver.openInputStream(uri)?.use { inputStream -> + try { + ExifInterface(inputStream).let { + orientation = it.rotationDegrees + } + } catch (e: Exception) { + Timber.e(e, "Cannot read orientation: %s", uri.toString()) + } + } + return orientation + } +} diff --git a/multipicker/src/main/res/xml/multipicker_provider_paths.xml b/multipicker/src/main/res/xml/multipicker_provider_paths.xml new file mode 100644 index 0000000000..ff9b81ce98 --- /dev/null +++ b/multipicker/src/main/res/xml/multipicker_provider_paths.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index d020abade4..04307e89d9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,2 @@ include ':vector', ':matrix-sdk-android', ':matrix-sdk-android-rx', ':diff-match-patch' +include ':multipicker' diff --git a/tools/import_from_riot.sh b/tools/import_from_riot.sh index 7572b73f17..a2b68a347c 100755 --- a/tools/import_from_riot.sh +++ b/tools/import_from_riot.sh @@ -37,6 +37,7 @@ cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-en-rGB/strings.xml ./mat cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-es/strings.xml ./matrix-sdk-android/src/main/res/values-es/strings.xml cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-es-rMX/strings.xml ./matrix-sdk-android/src/main/res/values-es-rMX/strings.xml cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-eu/strings.xml ./matrix-sdk-android/src/main/res/values-eu/strings.xml +cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-fa/strings.xml ./matrix-sdk-android/src/main/res/values-fa/strings.xml cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-fi/strings.xml ./matrix-sdk-android/src/main/res/values-fi/strings.xml cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-fr/strings.xml ./matrix-sdk-android/src/main/res/values-fr/strings.xml cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-gl/strings.xml ./matrix-sdk-android/src/main/res/values-gl/strings.xml @@ -75,6 +76,7 @@ cp ../riot-android/vector/src/main/res/values-bn-rIN/strings.xml ./vector/src cp ../riot-android/vector/src/main/res/values-bs/strings.xml ./vector/src/main/res/values-bs/strings.xml cp ../riot-android/vector/src/main/res/values-ca/strings.xml ./vector/src/main/res/values-ca/strings.xml cp ../riot-android/vector/src/main/res/values-cs/strings.xml ./vector/src/main/res/values-cs/strings.xml +cp ../riot-android/vector/src/main/res/values-cy/strings.xml ./vector/src/main/res/values-cy/strings.xml cp ../riot-android/vector/src/main/res/values-da/strings.xml ./vector/src/main/res/values-da/strings.xml cp ../riot-android/vector/src/main/res/values-de/strings.xml ./vector/src/main/res/values-de/strings.xml cp ../riot-android/vector/src/main/res/values-el/strings.xml ./vector/src/main/res/values-el/strings.xml @@ -96,6 +98,7 @@ cp ../riot-android/vector/src/main/res/values-it/strings.xml ./vector/src cp ../riot-android/vector/src/main/res/values-ja/strings.xml ./vector/src/main/res/values-ja/strings.xml cp ../riot-android/vector/src/main/res/values-ko/strings.xml ./vector/src/main/res/values-ko/strings.xml cp ../riot-android/vector/src/main/res/values-lv/strings.xml ./vector/src/main/res/values-lv/strings.xml +cp ../riot-android/vector/src/main/res/values-nb-rNO/strings.xml ./vector/src/main/res/values-nb-rNO/strings.xml cp ../riot-android/vector/src/main/res/values-nl/strings.xml ./vector/src/main/res/values-nl/strings.xml cp ../riot-android/vector/src/main/res/values-nn/strings.xml ./vector/src/main/res/values-nn/strings.xml cp ../riot-android/vector/src/main/res/values-pl/strings.xml ./vector/src/main/res/values-pl/strings.xml diff --git a/tools/release/sign_apk.sh b/tools/release/sign_apk.sh index 7f421c15f4..866510ba13 100755 --- a/tools/release/sign_apk.sh +++ b/tools/release/sign_apk.sh @@ -17,7 +17,7 @@ PARAM_KEYSTORE_PATH=$1 PARAM_APK=$2 # Other params -BUILD_TOOLS_VERSION="28.0.3" +BUILD_TOOLS_VERSION="29.0.3" MIN_SDK_VERSION=19 echo "Signing APK with build-tools version ${BUILD_TOOLS_VERSION} for min SDK version ${MIN_SDK_VERSION}..." diff --git a/vector/build.gradle b/vector/build.gradle index 2aae593271..2a898f5621 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -104,13 +104,13 @@ ext.abiVersionCodes = ["armeabi-v7a": 1, "arm64-v8a": 2, "x86": 3, "x86_64": 4]. def buildNumber = System.env.BUILDKITE_BUILD_NUMBER as Integer ?: 0 android { - compileSdkVersion 28 + compileSdkVersion 29 defaultConfig { applicationId "im.vector.riotx" // Set to API 19 because motionLayout is min API 18. // In the future we may consider using an alternative of MotionLayout to support API 16. But for security reason, maybe not. minSdkVersion 19 - targetSdkVersion 28 + targetSdkVersion 29 multiDexEnabled true // `develop` branch will have version code from timestamp, to ensure each build from CI has a incremented versionCode. @@ -249,10 +249,12 @@ dependencies { def moshi_version = '1.8.0' def daggerVersion = '2.25.4' def autofill_version = "1.0.0" + def work_version = '2.3.3' implementation project(":matrix-sdk-android") implementation project(":matrix-sdk-android-rx") implementation project(":diff-match-patch") + implementation project(":multipicker") implementation 'com.android.support:multidex:1.0.3' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" @@ -296,7 +298,7 @@ dependencies { implementation 'com.airbnb.android:mvrx:1.3.0' // Work - implementation "androidx.work:work-runtime-ktx:2.3.3" + implementation "androidx.work:work-runtime-ktx:$work_version" // Paging implementation "androidx.paging:paging-runtime-ktx:2.1.1" @@ -322,7 +324,7 @@ dependencies { implementation 'com.nulab-inc:zxcvbn:1.2.7' //Alerter - implementation 'com.tapadoo.android:alerter:4.0.3' + implementation 'com.tapadoo.android:alerter:5.1.2' implementation 'com.otaliastudios:autocomplete:1.1.0' @@ -346,9 +348,6 @@ dependencies { // Badge for compatibility implementation 'me.leolin:ShortcutBadger:1.1.22@aar' - // File picker - implementation 'com.kbeanie:multipicker:1.6@aar' - // DI implementation "com.google.dagger:dagger:$daggerVersion" kapt "com.google.dagger:dagger-compiler:$daggerVersion" @@ -369,7 +368,7 @@ dependencies { implementation "androidx.emoji:emoji-appcompat:1.0.0" - implementation 'com.github.BillCarsonFr:JsonViewer:0.4' + implementation 'com.github.BillCarsonFr:JsonViewer:0.5' // QR-code // Stick to 3.3.3 because of https://github.com/zxing/zxing/issues/1170 diff --git a/vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiItem.kt b/vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiItem.kt index 7403ead43c..cf9ca1032c 100644 --- a/vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiItem.kt +++ b/vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiItem.kt @@ -18,11 +18,14 @@ package im.vector.riotx.features.debug.sas import android.annotation.SuppressLint import android.widget.TextView +import androidx.core.content.ContextCompat import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.matrix.android.api.session.crypto.verification.EmojiRepresentation import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel +import me.gujun.android.span.image +import me.gujun.android.span.span @EpoxyModelClass(layout = im.vector.riotx.R.layout.item_sas_emoji) abstract class SasEmojiItem : VectorEpoxyModel() { @@ -36,7 +39,12 @@ abstract class SasEmojiItem : VectorEpoxyModel() { override fun bind(holder: Holder) { super.bind(holder) holder.indexView.text = "$index:" - holder.emojiView.text = emojiRepresentation.emoji + holder.emojiView.text = span { + +emojiRepresentation.emoji + emojiRepresentation.drawableRes?.let { + image(ContextCompat.getDrawable(holder.view.context, it)!!) + } + } holder.textView.setText(emojiRepresentation.nameResId) holder.idView.text = holder.idView.resources.getResourceEntryName(emojiRepresentation.nameResId) } diff --git a/vector/src/debug/java/im/vector/riotx/receivers/DebugReceiver.kt b/vector/src/debug/java/im/vector/riotx/receivers/DebugReceiver.kt index 887fba364c..914d7923df 100644 --- a/vector/src/debug/java/im/vector/riotx/receivers/DebugReceiver.kt +++ b/vector/src/debug/java/im/vector/riotx/receivers/DebugReceiver.kt @@ -21,7 +21,7 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.SharedPreferences -import android.preference.PreferenceManager +import androidx.preference.PreferenceManager import androidx.core.content.edit import im.vector.riotx.core.utils.lsFiles import timber.log.Timber diff --git a/vector/src/gplay/java/im/vector/riotx/push/fcm/FcmHelper.kt b/vector/src/gplay/java/im/vector/riotx/push/fcm/FcmHelper.kt index 136949d66f..d62aaf0f3d 100755 --- a/vector/src/gplay/java/im/vector/riotx/push/fcm/FcmHelper.kt +++ b/vector/src/gplay/java/im/vector/riotx/push/fcm/FcmHelper.kt @@ -19,7 +19,7 @@ package im.vector.riotx.push.fcm import android.app.Activity import android.content.Context -import android.preference.PreferenceManager +import androidx.preference.PreferenceManager import android.widget.Toast import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 2e56e20ce7..092817a6cc 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ + diff --git a/vector/src/main/assets/open_source_licenses.html b/vector/src/main/assets/open_source_licenses.html index e4fdc2f5ae..3af564aaca 100755 --- a/vector/src/main/assets/open_source_licenses.html +++ b/vector/src/main/assets/open_source_licenses.html @@ -23,6 +23,7 @@ } + @@ -566,5 +567,13 @@ Apache License of your accepting any such warranty or additional liability. +
+    CC-BY 4.0
+    
  • + Twitter/twemoji Graphics +
    +
  • +
    + diff --git a/vector/src/main/java/im/vector/riotx/VectorApplication.kt b/vector/src/main/java/im/vector/riotx/VectorApplication.kt index 81cf1402b0..680550e818 100644 --- a/vector/src/main/java/im/vector/riotx/VectorApplication.kt +++ b/vector/src/main/java/im/vector/riotx/VectorApplication.kt @@ -48,6 +48,7 @@ import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.notifications.NotificationUtils import im.vector.riotx.features.notifications.PushRuleTriggerListener +import im.vector.riotx.features.popup.PopupAlertManager import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler import im.vector.riotx.features.session.SessionListener import im.vector.riotx.features.settings.VectorPreferences @@ -77,6 +78,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration. @Inject lateinit var notificationUtils: NotificationUtils @Inject lateinit var appStateHandler: AppStateHandler @Inject lateinit var rxConfig: RxConfig + @Inject lateinit var popupAlertManager: PopupAlertManager lateinit var vectorComponent: VectorComponent private var fontThreadHandler: Handler? = null @@ -102,7 +104,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration. BigImageViewer.initialize(GlideImageLoader.with(applicationContext)) EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler() EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler() - registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks()) + registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks(popupAlertManager)) val fontRequest = FontRequest( "com.google.android.gms.fonts", "com.google.android.gms", @@ -169,7 +171,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration. MultiDex.install(this) } - override fun onConfigurationChanged(newConfig: Configuration?) { + override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) vectorConfiguration.onConfigurationChanged() } diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index d7e89a62f6..c68972cdd4 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -26,9 +26,18 @@ import im.vector.riotx.features.attachments.preview.AttachmentsPreviewFragment import im.vector.riotx.features.createdirect.CreateDirectRoomDirectoryUsersFragment import im.vector.riotx.features.createdirect.CreateDirectRoomKnownUsersFragment import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment +import im.vector.riotx.features.crypto.recover.BootstrapAccountPasswordFragment +import im.vector.riotx.features.crypto.recover.BootstrapConclusionFragment +import im.vector.riotx.features.crypto.recover.BootstrapConfirmPassphraseFragment +import im.vector.riotx.features.crypto.recover.BootstrapEnterPassphraseFragment +import im.vector.riotx.features.crypto.recover.BootstrapSaveRecoveryKeyFragment +import im.vector.riotx.features.crypto.recover.BootstrapWaitingFragment +import im.vector.riotx.features.crypto.verification.cancel.VerificationCancelFragment +import im.vector.riotx.features.crypto.verification.cancel.VerificationNotMeFragment import im.vector.riotx.features.crypto.verification.choose.VerificationChooseMethodFragment import im.vector.riotx.features.crypto.verification.conclusion.VerificationConclusionFragment import im.vector.riotx.features.crypto.verification.emoji.VerificationEmojiCodeFragment +import im.vector.riotx.features.crypto.verification.qrconfirmation.VerificationQRWaitingFragment import im.vector.riotx.features.crypto.verification.qrconfirmation.VerificationQrScannedByOtherFragment import im.vector.riotx.features.crypto.verification.request.VerificationRequestFragment import im.vector.riotx.features.grouplist.GroupListFragment @@ -74,6 +83,10 @@ import im.vector.riotx.features.settings.VectorSettingsSecurityPrivacyFragment import im.vector.riotx.features.settings.crosssigning.CrossSigningSettingsFragment import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment import im.vector.riotx.features.settings.devtools.AccountDataFragment +import im.vector.riotx.features.settings.devtools.GossipingEventsPaperTrailFragment +import im.vector.riotx.features.settings.devtools.IncomingKeyRequestListFragment +import im.vector.riotx.features.settings.devtools.KeyRequestsFragment +import im.vector.riotx.features.settings.devtools.OutgoingKeyRequestListFragment import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment import im.vector.riotx.features.settings.push.PushGatewaysFragment import im.vector.riotx.features.share.IncomingShareFragment @@ -327,11 +340,26 @@ interface FragmentModule { @FragmentKey(VerificationQrScannedByOtherFragment::class) fun bindVerificationQrScannedByOtherFragment(fragment: VerificationQrScannedByOtherFragment): Fragment + @Binds + @IntoMap + @FragmentKey(VerificationQRWaitingFragment::class) + fun bindVerificationQRWaitingFragment(fragment: VerificationQRWaitingFragment): Fragment + @Binds @IntoMap @FragmentKey(VerificationConclusionFragment::class) fun bindVerificationConclusionFragment(fragment: VerificationConclusionFragment): Fragment + @Binds + @IntoMap + @FragmentKey(VerificationCancelFragment::class) + fun bindVerificationCancelFragment(fragment: VerificationCancelFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(VerificationNotMeFragment::class) + fun bindVerificationNotMeFragment(fragment: VerificationNotMeFragment): Fragment + @Binds @IntoMap @FragmentKey(QrCodeScannerFragment::class) @@ -366,4 +394,54 @@ interface FragmentModule { @IntoMap @FragmentKey(AccountDataFragment::class) fun bindAccountDataFragment(fragment: AccountDataFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(OutgoingKeyRequestListFragment::class) + fun bindOutgoingKeyRequestListFragment(fragment: OutgoingKeyRequestListFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(IncomingKeyRequestListFragment::class) + fun bindIncomingKeyRequestListFragment(fragment: IncomingKeyRequestListFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(KeyRequestsFragment::class) + fun bindKeyRequestsFragment(fragment: KeyRequestsFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(GossipingEventsPaperTrailFragment::class) + fun bindGossipingEventsPaperTrailFragment(fragment: GossipingEventsPaperTrailFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(BootstrapEnterPassphraseFragment::class) + fun bindBootstrapEnterPassphraseFragment(fragment: BootstrapEnterPassphraseFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(BootstrapConfirmPassphraseFragment::class) + fun bindBootstrapConfirmPassphraseFragment(fragment: BootstrapConfirmPassphraseFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(BootstrapWaitingFragment::class) + fun bindBootstrapWaitingFragment(fragment: BootstrapWaitingFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(BootstrapSaveRecoveryKeyFragment::class) + fun bindBootstrapSaveRecoveryKeyFragment(fragment: BootstrapSaveRecoveryKeyFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(BootstrapConclusionFragment::class) + fun bindBootstrapConclusionFragment(fragment: BootstrapConclusionFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(BootstrapAccountPasswordFragment::class) + fun bindBootstrapAccountPasswordFragment(fragment: BootstrapAccountPasswordFragment): Fragment } diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index 5cd54c6c2b..af49b00b59 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -27,6 +27,7 @@ import im.vector.riotx.features.MainActivity import im.vector.riotx.features.createdirect.CreateDirectRoomActivity import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.riotx.features.crypto.quads.SharedSecureStorageActivity +import im.vector.riotx.features.crypto.recover.BootstrapBottomSheet import im.vector.riotx.features.crypto.verification.VerificationBottomSheet import im.vector.riotx.features.debug.DebugMenuActivity import im.vector.riotx.features.home.HomeActivity @@ -128,6 +129,7 @@ interface ScreenComponent { fun inject(bottomSheet: VerificationBottomSheet) fun inject(bottomSheet: DeviceVerificationInfoBottomSheet) fun inject(bottomSheet: DeviceListBottomSheet) + fun inject(bottomSheet: BootstrapBottomSheet) /* ========================================================================================== * Others diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt index 4ae92b29b1..6f864c7f5b 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt @@ -39,12 +39,14 @@ import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.HomeRoomListDataSource import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.html.VectorHtmlCompressor +import im.vector.riotx.features.login.ReAuthHelper import im.vector.riotx.features.navigation.Navigator import im.vector.riotx.features.notifications.NotifiableEventResolver import im.vector.riotx.features.notifications.NotificationBroadcastReceiver import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.notifications.NotificationUtils import im.vector.riotx.features.notifications.PushRuleTriggerListener +import im.vector.riotx.features.popup.PopupAlertManager import im.vector.riotx.features.rageshake.BugReporter import im.vector.riotx.features.rageshake.VectorFileLogger import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler @@ -128,6 +130,10 @@ interface VectorComponent { fun emojiDataSource(): EmojiDataSource + fun alertManager() : PopupAlertManager + + fun reAuthHelper() : ReAuthHelper + @Component.Factory interface Factory { fun create(@BindsInstance context: Context): VectorComponent diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/RecyclerView.kt b/vector/src/main/java/im/vector/riotx/core/extensions/RecyclerView.kt index 51f3ce611a..3b3132229c 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/RecyclerView.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/RecyclerView.kt @@ -16,10 +16,13 @@ package im.vector.riotx.core.extensions +import androidx.core.content.ContextCompat import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.EpoxyController +import im.vector.riotx.R +import im.vector.riotx.features.themes.ThemeUtils /** * Apply a Vertical LinearLayout Manager to the recyclerView and set the adapter from the epoxy controller @@ -40,7 +43,13 @@ fun RecyclerView.configureWith(epoxyController: EpoxyController, itemAnimator?.let { this.itemAnimator = it } } if (showDivider) { - addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) + addItemDecoration( + DividerItemDecoration(context, DividerItemDecoration.VERTICAL).apply { + ContextCompat.getDrawable(context, ThemeUtils.getResourceId(context, R.drawable.divider_horizontal_light))?.let { + setDrawable(it) + } + } + ) } setHasFixedSize(hasFixedSize) adapter = epoxyController.adapter diff --git a/vector/src/main/java/im/vector/riotx/core/files/FileSaver.kt b/vector/src/main/java/im/vector/riotx/core/files/FileSaver.kt index 677f7894e8..f978e20ca9 100644 --- a/vector/src/main/java/im/vector/riotx/core/files/FileSaver.kt +++ b/vector/src/main/java/im/vector/riotx/core/files/FileSaver.kt @@ -17,7 +17,10 @@ package im.vector.riotx.core.files import android.app.DownloadManager +import android.content.ContentValues import android.content.Context +import android.os.Build +import android.provider.MediaStore import androidx.annotation.WorkerThread import arrow.core.Try import okio.buffer @@ -54,10 +57,24 @@ fun addEntryToDownloadManager(context: Context, mimeType: String, title: String = file.name, description: String = file.name) { - val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager? - try { - downloadManager?.addCompletedDownload(title, description, true, mimeType, file.absolutePath, file.length(), true) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val contentValues = ContentValues().apply { + put(MediaStore.Downloads.TITLE, title) + put(MediaStore.Downloads.DISPLAY_NAME, description) + put(MediaStore.Downloads.MIME_TYPE, mimeType) + put(MediaStore.Downloads.SIZE, file.length()) + } + context.contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)?.let { uri -> + context.contentResolver.openOutputStream(uri)?.use { outputStream -> + outputStream.sink().buffer().write(file.inputStream().use { it.readBytes() }) + } + } + } else { + val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager? + @Suppress("DEPRECATION") + downloadManager?.addCompletedDownload(title, description, true, mimeType, file.absolutePath, file.length(), true) + } } catch (e: Exception) { Timber.e(e, "## addEntryToDownloadManager(): Exception") } diff --git a/vector/src/main/java/im/vector/riotx/core/glide/VectorGlideModelLoader.kt b/vector/src/main/java/im/vector/riotx/core/glide/VectorGlideModelLoader.kt index 1a90b0c34f..191ab6d972 100644 --- a/vector/src/main/java/im/vector/riotx/core/glide/VectorGlideModelLoader.kt +++ b/vector/src/main/java/im/vector/riotx/core/glide/VectorGlideModelLoader.kt @@ -95,7 +95,7 @@ class VectorGlideDataFetcher(private val activeSessionHolder: ActiveSessionHolde override fun loadData(priority: Priority, callback: DataFetcher.DataCallback) { Timber.v("Load data: $data") - if (data.isLocalFile()) { + if (data.isLocalFile() && data.url != null) { val initialFile = File(data.url) callback.onDataReady(FileInputStream(initialFile)) return diff --git a/vector/src/main/java/im/vector/riotx/core/images/ImageTools.kt b/vector/src/main/java/im/vector/riotx/core/images/ImageTools.kt deleted file mode 100644 index 84cba7392f..0000000000 --- a/vector/src/main/java/im/vector/riotx/core/images/ImageTools.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.riotx.core.images - -import android.content.Context -import android.net.Uri -import android.provider.MediaStore -import androidx.exifinterface.media.ExifInterface -import timber.log.Timber -import javax.inject.Inject - -class ImageTools @Inject constructor(private val context: Context) { - - /** - * Gets the [ExifInterface] value for the orientation for this local bitmap Uri. - * - * @param uri The URI to find the orientation for. Must be local. - * @return The orientation value, which may be [ExifInterface.ORIENTATION_UNDEFINED]. - */ - fun getOrientationForBitmap(uri: Uri): Int { - var orientation = ExifInterface.ORIENTATION_UNDEFINED - - if (uri.scheme == "content") { - val proj = arrayOf(MediaStore.Images.Media.DATA) - try { - val cursor = context.contentResolver.query(uri, proj, null, null, null) - cursor?.use { - if (it.moveToFirst()) { - val idxData = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATA) - val path = it.getString(idxData) - if (path.isNullOrBlank()) { - Timber.w("Cannot find path in media db for uri $uri") - return orientation - } - val exif = ExifInterface(path) - orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED) - } - } - } catch (e: Exception) { - // eg SecurityException from com.google.android.apps.photos.content.GooglePhotosImageProvider URIs - // eg IOException from trying to parse the returned path as a file when it is an http uri. - Timber.e(e, "Cannot get orientation for bitmap") - } - } else if (uri.scheme == "file") { - try { - val path = uri.path - if (path != null) { - val exif = ExifInterface(path) - orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED) - } - } catch (e: Exception) { - Timber.e(e, "Cannot get EXIF for file uri $uri") - } - } - - return orientation - } -} diff --git a/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt b/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt index 2e5786b57d..f451308c36 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt @@ -333,7 +333,7 @@ class EllipsizingTextView @JvmOverloads constructor(context: Context, attrs: Att * @param workingText text to strip end punctuation from * @return Text without end punctuation. */ - fun stripEndPunctuation(workingText: CharSequence?): String { + fun stripEndPunctuation(workingText: CharSequence): String { return mEndPunctPattern!!.matcher(workingText).replaceFirst("") } } diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt index 2649662ee5..ea954ecf27 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt @@ -134,7 +134,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { restorables.forEach { it.onSaveInstanceState(outState) } } - override fun onRestoreInstanceState(savedInstanceState: Bundle?) { + override fun onRestoreInstanceState(savedInstanceState: Bundle) { restorables.forEach { it.onRestoreInstanceState(savedInstanceState) } super.onRestoreInstanceState(savedInstanceState) } diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt index cbb0e904e4..6eb316456a 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt @@ -171,7 +171,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector { override fun invalidate() { // no-ops by default - Timber.w("invalidate() method has not been implemented") + Timber.v("invalidate() method has not been implemented") } protected fun setArguments(args: Parcelable? = null) { diff --git a/vector/src/main/java/im/vector/riotx/core/services/CallService.kt b/vector/src/main/java/im/vector/riotx/core/services/CallService.kt index f36b289aaa..30ab62d5b2 100644 --- a/vector/src/main/java/im/vector/riotx/core/services/CallService.kt +++ b/vector/src/main/java/im/vector/riotx/core/services/CallService.kt @@ -128,13 +128,13 @@ class CallService : VectorService() { * Display a call in progress notification. */ private fun displayCallInProgressNotification(intent: Intent) { - val callId = intent.getStringExtra(EXTRA_CALL_ID) + val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: "" val notification = notificationUtils.buildPendingCallNotification( intent.getBooleanExtra(EXTRA_IS_VIDEO, false), - intent.getStringExtra(EXTRA_ROOM_NAME), - intent.getStringExtra(EXTRA_ROOM_ID), - intent.getStringExtra(EXTRA_MATRIX_ID), + intent.getStringExtra(EXTRA_ROOM_NAME) ?: "", + intent.getStringExtra(EXTRA_ROOM_ID) ?: "", + intent.getStringExtra(EXTRA_MATRIX_ID) ?: "", callId) startForeground(NOTIFICATION_ID, notification) diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/BottomSheetActionButton.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/BottomSheetActionButton.kt new file mode 100644 index 0000000000..d29982c9e4 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/BottomSheetActionButton.kt @@ -0,0 +1,132 @@ +/* + * 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.ui.views + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.drawable.Drawable +import android.os.Build +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.TextView +import androidx.core.view.isGone +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import butterknife.BindView +import butterknife.ButterKnife +import im.vector.riotx.R +import im.vector.riotx.core.extensions.setTextOrHide +import im.vector.riotx.features.themes.ThemeUtils + +class BottomSheetActionButton @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr) { + + @BindView(R.id.itemVerificationActionTitle) + lateinit var actionTextView: TextView + + @BindView(R.id.itemVerificationActionSubTitle) + lateinit var descriptionTextView: TextView + + @BindView(R.id.itemVerificationLeftIcon) + lateinit var leftIconImageView: ImageView + + @BindView(R.id.itemVerificationActionIcon) + lateinit var rightIconImageView: ImageView + + @BindView(R.id.itemVerificationClickableZone) + lateinit var clickableView: View + + var title: String? = null + set(value) { + field = value + actionTextView.setTextOrHide(value) + } + + var subTitle: String? = null + set(value) { + field = value + descriptionTextView.setTextOrHide(value) + } + + var forceStartPadding: Boolean? = null + set(value) { + field = value + if (leftIcon == null) { + if (forceStartPadding == true) { + leftIconImageView.isInvisible = true + } else { + leftIconImageView.isGone = true + } + } + } + + var leftIcon: Drawable? = null + set(value) { + field = value + if (value == null) { + if (forceStartPadding == true) { + leftIconImageView.isInvisible = true + } else { + leftIconImageView.isGone = true + } + leftIconImageView.setImageDrawable(null) + } else { + leftIconImageView.isVisible = true + leftIconImageView.setImageDrawable(value) + } + } + + var rightIcon: Drawable? = null + set(value) { + field = value + rightIconImageView.setImageDrawable(value) + } + + var tint: Int? = null + set(value) { + field = value + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + leftIconImageView.imageTintList = value?.let { ColorStateList.valueOf(value) } + } else { + leftIcon?.let { + leftIcon = ThemeUtils.tintDrawable(context, it, value ?: ThemeUtils.getColor(context, android.R.attr.textColor)) + } + } + } + + init { + inflate(context, R.layout.item_verification_action, this) + ButterKnife.bind(this) + + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.BottomSheetActionButton, 0, 0) + title = typedArray.getString(R.styleable.BottomSheetActionButton_actionTitle) ?: "" + subTitle = typedArray.getString(R.styleable.BottomSheetActionButton_actionDescription) ?: "" + forceStartPadding = typedArray.getBoolean(R.styleable.BottomSheetActionButton_forceStartPadding, false) + leftIcon = typedArray.getDrawable(R.styleable.BottomSheetActionButton_leftIcon) + + rightIcon = typedArray.getDrawable(R.styleable.BottomSheetActionButton_rightIcon) + + tint = typedArray.getColor(R.styleable.BottomSheetActionButton_tint, ThemeUtils.getColor(context, android.R.attr.textColor)) + + typedArray.recycle() + } +} diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt index 7ce394b954..83a4e88ad5 100755 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt @@ -17,7 +17,7 @@ package im.vector.riotx.core.ui.views import android.content.Context -import android.preference.PreferenceManager +import androidx.preference.PreferenceManager import android.util.AttributeSet import android.view.View import android.view.ViewGroup @@ -123,6 +123,7 @@ class KeysBackupBanner @JvmOverloads constructor( is State.Setup -> { delegate?.setupKeysBackup() } + is State.Update, is State.Recover -> { delegate?.recoverKeysBackup() } diff --git a/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt b/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt index 627d757574..95035f6c96 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt @@ -37,9 +37,10 @@ class Debouncer(private val handler: Handler) { fun cancel(identifier: String) { if (runnables.containsKey(identifier)) { - val old = runnables[identifier] - handler.removeCallbacks(old) - runnables.remove(identifier) + runnables[identifier]?.let { + handler.removeCallbacks(it) + runnables.remove(identifier) + } } } diff --git a/vector/src/main/java/im/vector/riotx/core/utils/FileUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/FileUtils.kt index 1a19b49872..839356a129 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/FileUtils.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/FileUtils.kt @@ -73,7 +73,7 @@ private fun logAction(file: File): Boolean { */ private fun recursiveActionOnFile(file: File, action: ActionOnFile): Boolean { if (file.isDirectory) { - file.list().forEach { + file.list()?.forEach { val result = recursiveActionOnFile(File(file, it), action) if (!result) { diff --git a/vector/src/main/java/im/vector/riotx/core/utils/RingtoneUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/RingtoneUtils.kt index 1d2b97e27e..5345658207 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/RingtoneUtils.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/RingtoneUtils.kt @@ -20,7 +20,7 @@ import android.content.Context import android.media.Ringtone import android.media.RingtoneManager import android.net.Uri -import android.preference.PreferenceManager +import androidx.preference.PreferenceManager import androidx.core.content.edit import im.vector.riotx.features.settings.VectorPreferences diff --git a/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt index 1e005c777b..d82134caf5 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt @@ -84,7 +84,7 @@ fun requestDisablingBatteryOptimization(activity: Activity, fragment: Fragment?, */ fun copyToClipboard(context: Context, text: CharSequence, showToast: Boolean = true, @StringRes toastMessage: Int = R.string.copied_to_clipboard) { val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - clipboard.primaryClip = ClipData.newPlainText("", text) + clipboard.setPrimaryClip(ClipData.newPlainText("", text)) if (showToast) { context.toast(toastMessage) } @@ -153,7 +153,7 @@ fun startAddGoogleAccountIntent(context: AppCompatActivity, requestCode: Int) { } } -fun startSharePlainTextIntent(fragment: Fragment, chooserTitle: String?, text: String, subject: String? = null) { +fun startSharePlainTextIntent(fragment: Fragment, chooserTitle: String?, text: String, subject: String? = null, requestCode: Int? = null) { val share = Intent(Intent.ACTION_SEND) share.type = "text/plain" if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { @@ -165,7 +165,11 @@ fun startSharePlainTextIntent(fragment: Fragment, chooserTitle: String?, text: S share.putExtra(Intent.EXTRA_SUBJECT, subject) share.putExtra(Intent.EXTRA_TEXT, text) try { - fragment.startActivity(Intent.createChooser(share, chooserTitle)) + if (requestCode != null) { + fragment.startActivityForResult(Intent.createChooser(share, chooserTitle), requestCode) + } else { + fragment.startActivity(Intent.createChooser(share, chooserTitle)) + } } catch (activityNotFoundException: ActivityNotFoundException) { fragment.activity?.toast(R.string.error_no_external_application_found) } diff --git a/vector/src/main/java/im/vector/riotx/core/utils/TextUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/TextUtils.kt index 75f6893c7c..9519eb1f9d 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/TextUtils.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/TextUtils.kt @@ -35,8 +35,8 @@ object TextUtils { if (value < 1000) return value.toString() // deal with easy case val e = suffixes.floorEntry(value) - val divideBy = e.key - val suffix = e.value + val divideBy = e?.key + val suffix = e?.value val truncated = value / (divideBy!! / 10) // the number part of the output times 10 val hasDecimal = truncated < 100 && truncated / 10.0 != (truncated / 10).toDouble() diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt index ba1197b787..daea538e12 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt @@ -18,20 +18,13 @@ package im.vector.riotx.features.attachments import android.app.Activity import android.content.Context import android.content.Intent +import android.net.Uri import android.os.Bundle import androidx.fragment.app.Fragment -import com.kbeanie.multipicker.api.Picker.PICK_AUDIO -import com.kbeanie.multipicker.api.Picker.PICK_CONTACT -import com.kbeanie.multipicker.api.Picker.PICK_FILE -import com.kbeanie.multipicker.api.Picker.PICK_IMAGE_CAMERA -import com.kbeanie.multipicker.api.Picker.PICK_IMAGE_DEVICE -import com.kbeanie.multipicker.core.ImagePickerImpl -import com.kbeanie.multipicker.core.PickerManager -import com.kbeanie.multipicker.utils.IntentUtils import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.riotx.core.platform.Restorable -import im.vector.riotx.features.attachments.AttachmentsHelper.Callback +import im.vector.riotx.multipicker.MultiPicker import timber.log.Timber private const val CAPTURE_PATH_KEY = "CAPTURE_PATH_KEY" @@ -39,20 +32,8 @@ private const val PENDING_TYPE_KEY = "PENDING_TYPE_KEY" /** * This class helps to handle attachments by providing simple methods. - * The process is asynchronous and you must implement [Callback] methods to get the data or a failure. */ -class AttachmentsHelper private constructor(private val context: Context, - private val pickerManagerFactory: PickerManagerFactory) : Restorable { - - companion object { - fun create(fragment: Fragment, callback: Callback): AttachmentsHelper { - return AttachmentsHelper(fragment.requireContext(), FragmentPickerManagerFactory(fragment, callback)) - } - - fun create(activity: Activity, callback: Callback): AttachmentsHelper { - return AttachmentsHelper(activity, ActivityPickerManagerFactory(activity, callback)) - } - } +class AttachmentsHelper(val context: Context, val callback: Callback) : Restorable { interface Callback { fun onContactAttachmentReady(contactAttachment: ContactAttachment) { @@ -66,39 +47,15 @@ class AttachmentsHelper private constructor(private val context: Context, } // Capture path allows to handle camera image picking. It must be restored if the activity gets killed. - private var capturePath: String? = null + private var captureUri: Uri? = null // The pending type is set if we have to handle permission request. It must be restored if the activity gets killed. var pendingType: AttachmentTypeSelectorView.Type? = null - private val imagePicker by lazy { - pickerManagerFactory.createImagePicker() - } - - private val videoPicker by lazy { - pickerManagerFactory.createVideoPicker() - } - - private val cameraImagePicker by lazy { - pickerManagerFactory.createCameraImagePicker() - } - - private val filePicker by lazy { - pickerManagerFactory.createFilePicker() - } - - private val audioPicker by lazy { - pickerManagerFactory.createAudioPicker() - } - - private val contactPicker by lazy { - pickerManagerFactory.createContactPicker() - } - // Restorable override fun onSaveInstanceState(outState: Bundle) { - capturePath?.also { - outState.putString(CAPTURE_PATH_KEY, it) + captureUri?.also { + outState.putParcelable(CAPTURE_PATH_KEY, it) } pendingType?.also { outState.putSerializable(PENDING_TYPE_KEY, it) @@ -106,10 +63,7 @@ class AttachmentsHelper private constructor(private val context: Context, } override fun onRestoreInstanceState(savedInstanceState: Bundle?) { - capturePath = savedInstanceState?.getString(CAPTURE_PATH_KEY) - if (capturePath != null) { - cameraImagePicker.reinitialize(capturePath) - } + captureUri = savedInstanceState?.getParcelable(CAPTURE_PATH_KEY) as? Uri pendingType = savedInstanceState?.getSerializable(PENDING_TYPE_KEY) as? AttachmentTypeSelectorView.Type } @@ -118,36 +72,36 @@ class AttachmentsHelper private constructor(private val context: Context, /** * Starts the process for handling file picking */ - fun selectFile() { - filePicker.pickFile() + fun selectFile(fragment: Fragment) { + MultiPicker.get(MultiPicker.FILE).startWith(fragment) } /** * Starts the process for handling image picking */ - fun selectGallery() { - imagePicker.pickImage() + fun selectGallery(fragment: Fragment) { + MultiPicker.get(MultiPicker.IMAGE).startWith(fragment) } /** * Starts the process for handling audio picking */ - fun selectAudio() { - audioPicker.pickAudio() + fun selectAudio(fragment: Fragment) { + MultiPicker.get(MultiPicker.AUDIO).startWith(fragment) } /** * Starts the process for handling capture image picking */ - fun openCamera() { - capturePath = cameraImagePicker.pickImage() + fun openCamera(fragment: Fragment) { + captureUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(fragment) } /** * Starts the process for handling contact picking */ - fun selectContact() { - contactPicker.pickContact() + fun selectContact(fragment: Fragment) { + MultiPicker.get(MultiPicker.CONTACT).startWith(fragment) } /** @@ -157,14 +111,58 @@ class AttachmentsHelper private constructor(private val context: Context, */ fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { if (resultCode == Activity.RESULT_OK) { - val pickerManager = getPickerManagerForRequestCode(requestCode) - if (pickerManager != null) { - if (pickerManager is ImagePickerImpl) { - pickerManager.reinitialize(capturePath) + when (requestCode) { + MultiPicker.REQUEST_CODE_PICK_FILE -> { + callback.onContentAttachmentsReady( + MultiPicker.get(MultiPicker.FILE) + .getSelectedFiles(context, requestCode, resultCode, data) + .map { it.toContentAttachmentData() } + ) } - pickerManager.submit(data) - return true + MultiPicker.REQUEST_CODE_PICK_AUDIO -> { + callback.onContentAttachmentsReady( + MultiPicker.get(MultiPicker.AUDIO) + .getSelectedFiles(context, requestCode, resultCode, data) + .map { it.toContentAttachmentData() } + ) + } + MultiPicker.REQUEST_CODE_PICK_CONTACT -> { + MultiPicker.get(MultiPicker.CONTACT) + .getSelectedFiles(context, requestCode, resultCode, data) + .firstOrNull() + ?.toContactAttachment() + ?.let { + callback.onContactAttachmentReady(it) + } + } + MultiPicker.REQUEST_CODE_PICK_IMAGE -> { + callback.onContentAttachmentsReady( + MultiPicker.get(MultiPicker.IMAGE) + .getSelectedFiles(context, requestCode, resultCode, data) + .map { it.toContentAttachmentData() } + ) + } + MultiPicker.REQUEST_CODE_TAKE_PHOTO -> { + captureUri?.let { captureUri -> + MultiPicker.get(MultiPicker.CAMERA) + .getTakenPhoto(context, requestCode, resultCode, captureUri) + ?.let { + callback.onContentAttachmentsReady( + listOf(it).map { it.toContentAttachmentData() } + ) + } + } + } + MultiPicker.REQUEST_CODE_PICK_VIDEO -> { + callback.onContentAttachmentsReady( + MultiPicker.get(MultiPicker.VIDEO) + .getSelectedFiles(context, requestCode, resultCode, data) + .map { it.toContentAttachmentData() } + ) + } + else -> return false } + return true } return false } @@ -174,30 +172,35 @@ class AttachmentsHelper private constructor(private val context: Context, * * @return true if it can handle the intent data, false otherwise */ - fun handleShareIntent(intent: Intent): Boolean { + fun handleShareIntent(context: Context, intent: Intent): Boolean { val type = intent.resolveType(context) ?: return false if (type.startsWith("image")) { - imagePicker.submit(IntentUtils.getPickerIntentForSharing(intent)) + callback.onContentAttachmentsReady( + MultiPicker.get(MultiPicker.IMAGE).getIncomingFiles(context, intent).map { + it.toContentAttachmentData() + } + ) } else if (type.startsWith("video")) { - videoPicker.submit(IntentUtils.getPickerIntentForSharing(intent)) + callback.onContentAttachmentsReady( + MultiPicker.get(MultiPicker.VIDEO).getIncomingFiles(context, intent).map { + it.toContentAttachmentData() + } + ) } else if (type.startsWith("audio")) { - videoPicker.submit(IntentUtils.getPickerIntentForSharing(intent)) + callback.onContentAttachmentsReady( + MultiPicker.get(MultiPicker.AUDIO).getIncomingFiles(context, intent).map { + it.toContentAttachmentData() + } + ) } else if (type.startsWith("application") || type.startsWith("file") || type.startsWith("*")) { - filePicker.submit(IntentUtils.getPickerIntentForSharing(intent)) + callback.onContentAttachmentsReady( + MultiPicker.get(MultiPicker.FILE).getIncomingFiles(context, intent).map { + it.toContentAttachmentData() + } + ) } else { return false } return true } - - private fun getPickerManagerForRequestCode(requestCode: Int): PickerManager? { - return when (requestCode) { - PICK_IMAGE_DEVICE -> imagePicker - PICK_IMAGE_CAMERA -> cameraImagePicker - PICK_FILE -> filePicker - PICK_CONTACT -> contactPicker - PICK_AUDIO -> audioPicker - else -> null - } - } } diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt index a3de5084de..02b712b8a7 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt @@ -16,51 +16,48 @@ package im.vector.riotx.features.attachments -import com.kbeanie.multipicker.api.entity.ChosenAudio -import com.kbeanie.multipicker.api.entity.ChosenContact -import com.kbeanie.multipicker.api.entity.ChosenFile -import com.kbeanie.multipicker.api.entity.ChosenImage -import com.kbeanie.multipicker.api.entity.ChosenVideo import im.vector.matrix.android.api.session.content.ContentAttachmentData +import im.vector.riotx.multipicker.entity.MultiPickerAudioType +import im.vector.riotx.multipicker.entity.MultiPickerBaseType +import im.vector.riotx.multipicker.entity.MultiPickerContactType +import im.vector.riotx.multipicker.entity.MultiPickerFileType +import im.vector.riotx.multipicker.entity.MultiPickerImageType +import im.vector.riotx.multipicker.entity.MultiPickerVideoType import timber.log.Timber -fun ChosenContact.toContactAttachment(): ContactAttachment { +fun MultiPickerContactType.toContactAttachment(): ContactAttachment { return ContactAttachment( displayName = displayName, photoUri = photoUri, - emails = emails.toList(), - phones = phones.toList() + emails = emailList.toList(), + phones = phoneNumberList.toList() ) } -fun ChosenFile.toContentAttachmentData(): ContentAttachmentData { +fun MultiPickerFileType.toContentAttachmentData(): ContentAttachmentData { if (mimeType == null) Timber.w("No mimeType") return ContentAttachmentData( - path = originalPath, mimeType = mimeType, type = mapType(), size = size, - date = createdAt?.time ?: System.currentTimeMillis(), name = displayName, - queryUri = queryUri + queryUri = contentUri ) } -fun ChosenAudio.toContentAttachmentData(): ContentAttachmentData { +fun MultiPickerAudioType.toContentAttachmentData(): ContentAttachmentData { if (mimeType == null) Timber.w("No mimeType") return ContentAttachmentData( - path = originalPath, mimeType = mimeType, type = mapType(), size = size, - date = createdAt?.time ?: System.currentTimeMillis(), name = displayName, duration = duration, - queryUri = queryUri + queryUri = contentUri ) } -private fun ChosenFile.mapType(): ContentAttachmentData.Type { +private fun MultiPickerBaseType.mapType(): ContentAttachmentData.Type { return when { mimeType?.startsWith("image/") == true -> ContentAttachmentData.Type.IMAGE mimeType?.startsWith("video/") == true -> ContentAttachmentData.Type.VIDEO @@ -69,10 +66,9 @@ private fun ChosenFile.mapType(): ContentAttachmentData.Type { } } -fun ChosenImage.toContentAttachmentData(): ContentAttachmentData { +fun MultiPickerImageType.toContentAttachmentData(): ContentAttachmentData { if (mimeType == null) Timber.w("No mimeType") return ContentAttachmentData( - path = originalPath, mimeType = mimeType, type = mapType(), name = displayName, @@ -80,23 +76,20 @@ fun ChosenImage.toContentAttachmentData(): ContentAttachmentData { height = height.toLong(), width = width.toLong(), exifOrientation = orientation, - date = createdAt?.time ?: System.currentTimeMillis(), - queryUri = queryUri + queryUri = contentUri ) } -fun ChosenVideo.toContentAttachmentData(): ContentAttachmentData { +fun MultiPickerVideoType.toContentAttachmentData(): ContentAttachmentData { if (mimeType == null) Timber.w("No mimeType") return ContentAttachmentData( - path = originalPath, mimeType = mimeType, type = ContentAttachmentData.Type.VIDEO, size = size, - date = createdAt?.time ?: System.currentTimeMillis(), height = height.toLong(), width = width.toLong(), duration = duration, name = displayName, - queryUri = queryUri + queryUri = contentUri ) } diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsPickerCallback.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsPickerCallback.kt deleted file mode 100644 index 62956e08c8..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsPickerCallback.kt +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.riotx.features.attachments - -import com.kbeanie.multipicker.api.callbacks.AudioPickerCallback -import com.kbeanie.multipicker.api.callbacks.ContactPickerCallback -import com.kbeanie.multipicker.api.callbacks.FilePickerCallback -import com.kbeanie.multipicker.api.callbacks.ImagePickerCallback -import com.kbeanie.multipicker.api.callbacks.VideoPickerCallback -import com.kbeanie.multipicker.api.entity.ChosenAudio -import com.kbeanie.multipicker.api.entity.ChosenContact -import com.kbeanie.multipicker.api.entity.ChosenFile -import com.kbeanie.multipicker.api.entity.ChosenImage -import com.kbeanie.multipicker.api.entity.ChosenVideo - -/** - * This class delegates the PickerManager callbacks to an [AttachmentsHelper.Callback] - */ -class AttachmentsPickerCallback(private val callback: AttachmentsHelper.Callback) - : ImagePickerCallback, - FilePickerCallback, - VideoPickerCallback, - AudioPickerCallback, - ContactPickerCallback { - - override fun onContactChosen(contact: ChosenContact?) { - if (contact == null) { - callback.onAttachmentsProcessFailed() - } else { - val contactAttachment = contact.toContactAttachment() - callback.onContactAttachmentReady(contactAttachment) - } - } - - override fun onAudiosChosen(audios: MutableList?) { - if (audios.isNullOrEmpty()) { - callback.onAttachmentsProcessFailed() - } else { - val attachments = audios.map { - it.toContentAttachmentData() - } - callback.onContentAttachmentsReady(attachments) - } - } - - override fun onFilesChosen(files: MutableList?) { - if (files.isNullOrEmpty()) { - callback.onAttachmentsProcessFailed() - } else { - val attachments = files.map { - it.toContentAttachmentData() - } - callback.onContentAttachmentsReady(attachments) - } - } - - override fun onImagesChosen(images: MutableList?) { - if (images.isNullOrEmpty()) { - callback.onAttachmentsProcessFailed() - } else { - val attachments = images.map { - it.toContentAttachmentData() - } - callback.onContentAttachmentsReady(attachments) - } - } - - override fun onVideosChosen(videos: MutableList?) { - if (videos.isNullOrEmpty()) { - callback.onAttachmentsProcessFailed() - } else { - val attachments = videos.map { - it.toContentAttachmentData() - } - callback.onContentAttachmentsReady(attachments) - } - } - - override fun onError(error: String?) { - callback.onAttachmentsProcessFailed() - } -} diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/PickerManagerFactory.kt b/vector/src/main/java/im/vector/riotx/features/attachments/PickerManagerFactory.kt deleted file mode 100644 index 6c03f21ab3..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/attachments/PickerManagerFactory.kt +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.riotx.features.attachments - -import android.app.Activity -import androidx.fragment.app.Fragment -import com.kbeanie.multipicker.api.AudioPicker -import com.kbeanie.multipicker.api.CameraImagePicker -import com.kbeanie.multipicker.api.ContactPicker -import com.kbeanie.multipicker.api.FilePicker -import com.kbeanie.multipicker.api.ImagePicker -import com.kbeanie.multipicker.api.VideoPicker - -/** - * Factory for creating different pickers. It allows to use with fragment or activity builders. - */ -interface PickerManagerFactory { - - fun createImagePicker(): ImagePicker - - fun createCameraImagePicker(): CameraImagePicker - - fun createVideoPicker(): VideoPicker - - fun createFilePicker(): FilePicker - - fun createAudioPicker(): AudioPicker - - fun createContactPicker(): ContactPicker -} - -class ActivityPickerManagerFactory(private val activity: Activity, callback: AttachmentsHelper.Callback) : PickerManagerFactory { - - private val attachmentsPickerCallback = AttachmentsPickerCallback(callback) - - override fun createImagePicker(): ImagePicker { - return ImagePicker(activity).also { - it.setImagePickerCallback(attachmentsPickerCallback) - it.allowMultiple() - } - } - - override fun createCameraImagePicker(): CameraImagePicker { - return CameraImagePicker(activity).also { - it.setImagePickerCallback(attachmentsPickerCallback) - } - } - - override fun createVideoPicker(): VideoPicker { - return VideoPicker(activity).also { - it.setVideoPickerCallback(attachmentsPickerCallback) - it.allowMultiple() - } - } - - override fun createFilePicker(): FilePicker { - return FilePicker(activity).also { - it.allowMultiple() - it.setFilePickerCallback(attachmentsPickerCallback) - } - } - - override fun createAudioPicker(): AudioPicker { - return AudioPicker(activity).also { - it.allowMultiple() - it.setAudioPickerCallback(attachmentsPickerCallback) - } - } - - override fun createContactPicker(): ContactPicker { - return ContactPicker(activity).also { - it.setContactPickerCallback(attachmentsPickerCallback) - } - } -} - -class FragmentPickerManagerFactory(private val fragment: Fragment, callback: AttachmentsHelper.Callback) : PickerManagerFactory { - - private val attachmentsPickerCallback = AttachmentsPickerCallback(callback) - - override fun createImagePicker(): ImagePicker { - return ImagePicker(fragment).also { - it.setImagePickerCallback(attachmentsPickerCallback) - it.allowMultiple() - } - } - - override fun createCameraImagePicker(): CameraImagePicker { - return CameraImagePicker(fragment).also { - it.setImagePickerCallback(attachmentsPickerCallback) - } - } - - override fun createVideoPicker(): VideoPicker { - return VideoPicker(fragment).also { - it.setVideoPickerCallback(attachmentsPickerCallback) - it.allowMultiple() - } - } - - override fun createFilePicker(): FilePicker { - return FilePicker(fragment).also { - it.allowMultiple() - it.setFilePickerCallback(attachmentsPickerCallback) - } - } - - override fun createAudioPicker(): AudioPicker { - return AudioPicker(fragment).also { - it.allowMultiple() - it.setAudioPickerCallback(attachmentsPickerCallback) - } - } - - override fun createContactPicker(): ContactPicker { - return ContactPicker(fragment).also { - it.setContactPickerCallback(attachmentsPickerCallback) - } - } -} diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewControllers.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewControllers.kt index 34f018aaf9..60ee722116 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewControllers.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewControllers.kt @@ -25,7 +25,7 @@ class AttachmentBigPreviewController @Inject constructor() : TypedEpoxyControlle override fun buildModels(data: AttachmentsPreviewViewState) { data.attachments.forEach { attachmentBigPreviewItem { - id(it.path) + id(it.queryUri.toString()) attachment(it) } } @@ -43,7 +43,7 @@ class AttachmentMiniaturePreviewController @Inject constructor() : TypedEpoxyCon override fun buildModels(data: AttachmentsPreviewViewState) { data.attachments.forEachIndexed { index, contentAttachmentData -> attachmentMiniaturePreviewItem { - id(contentAttachmentData.path) + id(contentAttachmentData.queryUri.toString()) attachment(contentAttachmentData) checked(data.currentAttachmentIndex == index) clickListener { _ -> diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewItems.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewItems.kt index 3b43fa6e20..373298bf31 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewItems.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewItems.kt @@ -33,11 +33,10 @@ abstract class AttachmentPreviewItem : VectorE abstract val attachment: ContentAttachmentData override fun bind(holder: H) { - val path = attachment.path if (attachment.type == ContentAttachmentData.Type.VIDEO || attachment.type == ContentAttachmentData.Type.IMAGE) { Glide.with(holder.view.context) .asBitmap() - .load(path) + .load(attachment.queryUri) .apply(RequestOptions().frame(0)) .into(holder.imageView) } else { diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewAction.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewAction.kt index 5acc59b035..aef724331f 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewAction.kt @@ -17,10 +17,11 @@ package im.vector.riotx.features.attachments.preview +import android.net.Uri import im.vector.riotx.core.platform.VectorViewModelAction sealed class AttachmentsPreviewAction : VectorViewModelAction { object RemoveCurrentAttachment : AttachmentsPreviewAction() data class SetCurrentAttachment(val index: Int): AttachmentsPreviewAction() - data class UpdatePathOfCurrentAttachment(val newPath: String): AttachmentsPreviewAction() + data class UpdatePathOfCurrentAttachment(val newUri: Uri): AttachmentsPreviewAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewActivity.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewActivity.kt index 46a90803ca..6c91f70131 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewActivity.kt @@ -43,7 +43,7 @@ class AttachmentsPreviewActivity : VectorBaseActivity(), ToolbarConfigurable { } fun getOutput(intent: Intent): List { - return intent.getParcelableArrayListExtra(ATTACHMENTS_PREVIEW_RESULT) + return intent.getParcelableArrayListExtra(ATTACHMENTS_PREVIEW_RESULT) ?: emptyList() } fun getKeepOriginalSize(intent: Intent): Boolean { diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt index e52b497df4..3b1972ffbc 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt @@ -172,9 +172,9 @@ class AttachmentsPreviewFragment @Inject constructor( } private fun handleCropResult(result: Intent) { - val resultPath = UCrop.getOutput(result)?.path - if (resultPath != null) { - viewModel.handle(AttachmentsPreviewAction.UpdatePathOfCurrentAttachment(resultPath)) + val resultUri = UCrop.getOutput(result) + if (resultUri != null) { + viewModel.handle(AttachmentsPreviewAction.UpdatePathOfCurrentAttachment(resultUri)) } else { Toast.makeText(requireContext(), "Cannot retrieve cropped value", Toast.LENGTH_SHORT).show() } @@ -202,8 +202,7 @@ class AttachmentsPreviewFragment @Inject constructor( private fun doHandleEditAction() = withState(viewModel) { val currentAttachment = it.attachments.getOrNull(it.currentAttachmentIndex) ?: return@withState val destinationFile = File(requireContext().cacheDir, "${currentAttachment.name}_edited_image_${System.currentTimeMillis()}") - // Note: using currentAttachment.queryUri.toUri() make the app crash when sharing from Google Photos - val uri = File(currentAttachment.path).toUri() + val uri = currentAttachment.queryUri UCrop.of(uri, destinationFile.toUri()) .withOptions( UCrop.Options() diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewModel.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewModel.kt index 1f6c8c2f8b..d1e44fa963 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewModel.kt @@ -62,7 +62,7 @@ class AttachmentsPreviewViewModel @AssistedInject constructor(@Assisted initialS private fun handleUpdatePathOfCurrentAttachment(action: AttachmentsPreviewAction.UpdatePathOfCurrentAttachment) = withState { val attachments = it.attachments.mapIndexed { index, contentAttachmentData -> if (index == it.currentAttachmentIndex) { - contentAttachmentData.copy(path = action.newPath) + contentAttachmentData.copy(queryUri = action.newUri) } else { contentAttachmentData } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt index cae8b50523..b9b75588f1 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt @@ -40,7 +40,7 @@ class KeysExporter(private val session: Session) { runCatching { val data = awaitCallback { session.cryptoService().exportRoomKeys(password, it) } withContext(Dispatchers.IO) { - val parentDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + val parentDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) val file = File(parentDir, "riotx-keys-" + System.currentTimeMillis() + ".txt") writeToFile(data, file) diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseFragment.kt index a9bdeee2d6..8dc8855583 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseFragment.kt @@ -95,7 +95,7 @@ class KeysBackupRestoreFromPassphraseFragment @Inject constructor(): VectorBaseF // used just to have default link representation val clickableSpan = object : ClickableSpan() { - override fun onClick(widget: View?) {} + override fun onClick(widget: View) {} } val start = helperText.indexOf(clickableText) val end = start + clickableText.length diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt index 4f2d806ce3..1fec404f7d 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt @@ -31,10 +31,12 @@ import im.vector.riotx.core.epoxy.loadingItem import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.ui.list.GenericItem import im.vector.riotx.core.ui.list.genericItem +import im.vector.riotx.features.settings.VectorPreferences import java.util.UUID import javax.inject.Inject class KeysBackupSettingsRecyclerViewController @Inject constructor(private val stringProvider: StringProvider, + private val vectorPreferences: VectorPreferences, private val session: Session) : TypedEpoxyController() { var listener: Listener? = null @@ -149,7 +151,9 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(private val s description(keyVersionResult?.algorithm ?: "") } - buildKeysBackupTrust(data.keysBackupVersionTrust) + if (vectorPreferences.developerMode()) { + buildKeysBackupTrust(data.keysBackupVersionTrust) + } } // Footer diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt index 924e25a4d7..fbc69505fa 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt @@ -153,7 +153,7 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() { } override fun onFailure(failure: Throwable) { - toast(failure.localizedMessage) + toast(failure.localizedMessage ?: getString(R.string.unexpected_error)) hideWaitingView() } }) diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt index a224cfb387..9175d6c081 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt @@ -167,7 +167,7 @@ class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment() GlobalScope.launch(Dispatchers.Main) { Try { withContext(Dispatchers.IO) { - val parentDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + val parentDir = context?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) val file = File(parentDir, "recovery-key-" + System.currentTimeMillis() + ".txt") writeToFile(data, file) diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt index 534de09ce0..ddb50628d6 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt @@ -22,19 +22,21 @@ package im.vector.riotx.features.crypto.keysrequest import android.content.Context import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.Session -import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener +import im.vector.matrix.android.api.session.crypto.keyshare.GossipingRequestListener import im.vector.matrix.android.api.session.crypto.verification.SasVerificationTransaction import im.vector.matrix.android.api.session.crypto.verification.VerificationService import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest -import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequestCancellation +import im.vector.matrix.android.internal.crypto.IncomingRequestCancellation +import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel 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.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse import im.vector.riotx.R +import im.vector.riotx.features.popup.DefaultVectorAlert import im.vector.riotx.features.popup.PopupAlertManager import timber.log.Timber import java.text.DateFormat @@ -53,8 +55,8 @@ import javax.inject.Singleton */ @Singleton -class KeyRequestHandler @Inject constructor(private val context: Context) - : RoomKeysRequestListener, +class KeyRequestHandler @Inject constructor(private val context: Context, private val popupAlertManager: PopupAlertManager) + : GossipingRequestListener, VerificationService.Listener { private val alertsToRequests = HashMap>() @@ -73,6 +75,13 @@ class KeyRequestHandler @Inject constructor(private val context: Context) session = null } + override fun onSecretShareRequest(request: IncomingSecretShareRequest) : Boolean { + // By default riotX will not prompt if the SDK has decided that the request should not be fulfilled + Timber.v("## onSecretShareRequest() : Ignoring $request") + request.ignore?.run() + return true + } + /** * Handle incoming key request. * @@ -110,9 +119,9 @@ class KeyRequestHandler @Inject constructor(private val context: Context) } if (deviceInfo.isUnknown) { - session?.cryptoService()?.setDeviceVerification(DeviceTrustLevel(false, false), userId, deviceId) + session?.cryptoService()?.setDeviceVerification(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false), userId, deviceId) - deviceInfo.trustLevel = DeviceTrustLevel(false, false) + deviceInfo.trustLevel = DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false) // can we get more info on this device? session?.cryptoService()?.getDevicesList(object : MatrixCallback { @@ -180,7 +189,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context) } } - val alert = PopupAlertManager.VectorAlert( + val alert = DefaultVectorAlert( alertManagerId(userId, deviceId), context.getString(R.string.key_share_request), dialogText, @@ -194,20 +203,6 @@ class KeyRequestHandler @Inject constructor(private val context: Context) denyAllRequests(mappingKey) } - // TODO send to the new profile page -// alert.addButton( -// context.getString(R.string.start_verification_short_label), -// Runnable { -// alert.weakCurrentActivity?.get()?.let { -// val intent = SASVerificationActivity.outgoingIntent(it, -// session?.myUserId ?: "", -// userId, deviceId) -// it.startActivity(intent) -// } -// }, -// false -// ) - alert.addButton(context.getString(R.string.share_without_verifying_short_label), Runnable { shareAllSessions(mappingKey) }) @@ -216,7 +211,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context) denyAllRequests(mappingKey) }) - PopupAlertManager.postVectorAlert(alert) + popupAlertManager.postVectorAlert(alert) } private fun denyAllRequests(mappingKey: String) { @@ -238,7 +233,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context) * * @param request the cancellation request. */ - override fun onRoomKeyRequestCancellation(request: IncomingRoomKeyRequestCancellation) { + override fun onRoomKeyRequestCancellation(request: IncomingRequestCancellation) { // see if we can find the request in the queue val userId = request.userId val deviceId = request.deviceId @@ -256,7 +251,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context) && it.requestId == request.requestId } if (alertsToRequests[alertMgrUniqueKey]?.isEmpty() == true) { - PopupAlertManager.cancelAlert(alertMgrUniqueKey) + popupAlertManager.cancelAlert(alertMgrUniqueKey) alertsToRequests.remove(keyForMap(userId, deviceId)) } } @@ -267,7 +262,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context) if (state == VerificationTxState.Verified) { // ok it's verified, see if we have key request for that shareAllSessions("${tx.otherDeviceId}${tx.otherUserId}") - PopupAlertManager.cancelAlert("ikr_${tx.otherDeviceId}${tx.otherUserId}") + popupAlertManager.cancelAlert("ikr_${tx.otherDeviceId}${tx.otherUserId}") } } // should do it with QR tx also @@ -277,7 +272,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context) override fun markedAsManuallyVerified(userId: String, deviceId: String) { // accept related requests shareAllSessions(keyForMap(userId, deviceId)) - PopupAlertManager.cancelAlert(alertManagerId(userId, deviceId)) + popupAlertManager.cancelAlert(alertManagerId(userId, deviceId)) } private fun keyForMap(userId: String, deviceId: String) = "$deviceId$userId" diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecureStorageViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecureStorageViewModel.kt index a9f5d33888..530e0934cb 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecureStorageViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecureStorageViewModel.kt @@ -16,6 +16,7 @@ package im.vector.riotx.features.crypto.quads +import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxViewModelFactory @@ -34,9 +35,9 @@ import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.WaitingViewData import im.vector.riotx.core.resources.StringProvider import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import timber.log.Timber import java.io.ByteArrayOutputStream data class SharedSecureStorageViewState( @@ -77,7 +78,7 @@ class SharedSecureStorageViewModel @AssistedInject constructor( private fun handleSubmitPassphrase(action: SharedSecureStorageAction.SubmitPassphrase) { val decryptedSecretMap = HashMap() - GlobalScope.launch(Dispatchers.IO) { + viewModelScope.launch(Dispatchers.IO) { runCatching { _viewEvents.post(SharedSecureStorageViewEvent.ShowModalLoading) val passphrase = action.passphrase @@ -116,14 +117,18 @@ class SharedSecureStorageViewModel @AssistedInject constructor( withContext(Dispatchers.IO) { args.requestedSecrets.forEach { - val res = awaitCallback { callback -> - session.sharedSecretStorageService.getSecret( - name = it, - keyId = keyInfo.id, - secretKey = keySpec, - callback = callback) + if (session.getAccountDataEvent(it) != null) { + val res = awaitCallback { callback -> + session.sharedSecretStorageService.getSecret( + name = it, + keyId = keyInfo.id, + secretKey = keySpec, + callback = callback) + } + decryptedSecretMap[it] = res + } else { + Timber.w("## Cannot find secret $it in SSSS, skip") } - decryptedSecretMap[it] = res } } }.fold({ @@ -158,8 +163,8 @@ class SharedSecureStorageViewModel @AssistedInject constructor( @JvmStatic override fun create(viewModelContext: ViewModelContext, state: SharedSecureStorageViewState): SharedSecureStorageViewModel? { val activity: SharedSecureStorageActivity = viewModelContext.activity() - val args: SharedSecureStorageActivity.Args = activity.intent.getParcelableExtra(MvRx.KEY_ARG) - return activity.viewModelFactory.create(state, args) + val args: SharedSecureStorageActivity.Args? = activity.intent.getParcelableExtra(MvRx.KEY_ARG) + return args?.let { activity.viewModelFactory.create(state, it) } } } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapAccountPasswordFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapAccountPasswordFragment.kt new file mode 100644 index 0000000000..abe6e54092 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapAccountPasswordFragment.kt @@ -0,0 +1,123 @@ +/* + * 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.crypto.recover + +import android.os.Bundle +import android.view.View +import android.view.inputmethod.EditorInfo +import androidx.core.text.toSpannable +import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.withState +import com.jakewharton.rxbinding3.view.clicks +import com.jakewharton.rxbinding3.widget.editorActionEvents +import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.riotx.R +import im.vector.riotx.core.extensions.hideKeyboard +import im.vector.riotx.core.extensions.showPassword +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.utils.colorizeMatchingText +import io.reactivex.android.schedulers.AndroidSchedulers +import kotlinx.android.synthetic.main.fragment_bootstrap_enter_account_password.* +import kotlinx.android.synthetic.main.fragment_bootstrap_enter_passphrase.bootstrapDescriptionText +import kotlinx.android.synthetic.main.fragment_bootstrap_enter_passphrase.ssss_view_show_password +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class BootstrapAccountPasswordFragment @Inject constructor( + private val colorProvider: ColorProvider +) : VectorBaseFragment() { + + override fun getLayoutResId() = R.layout.fragment_bootstrap_enter_account_password + + val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val recPassPhrase = getString(R.string.account_password) + bootstrapDescriptionText.text = getString(R.string.enter_account_password, recPassPhrase) + .toSpannable() + .colorizeMatchingText(recPassPhrase, colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) + + bootstrapAccountPasswordEditText.hint = getString(R.string.account_password) + + bootstrapAccountPasswordEditText.editorActionEvents() + .debounce(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + if (it.actionId == EditorInfo.IME_ACTION_DONE) { + submit() + } + } + .disposeOnDestroyView() + + bootstrapAccountPasswordEditText.textChanges() + .distinct() + .subscribe { + if (!it.isNullOrBlank()) { + bootstrapAccountPasswordTil.error = null + } + } + .disposeOnDestroyView() + + ssss_view_show_password.clicks() + .debounce(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) + } + .disposeOnDestroyView() + + bootstrapPasswordButton.clicks() + .debounce(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + submit() + } + .disposeOnDestroyView() + + withState(sharedViewModel) { state -> + (state.step as? BootstrapStep.AccountPassword)?.failure?.let { + bootstrapAccountPasswordTil.error = it + } + } + } + + private fun submit() = withState(sharedViewModel) { state -> + if (state.step !is BootstrapStep.AccountPassword) { + return@withState + } + val accountPassword = bootstrapAccountPasswordEditText.text?.toString() + if (accountPassword.isNullOrBlank()) { + bootstrapAccountPasswordTil.error = getString(R.string.error_empty_field_your_password) + } else { + view?.hideKeyboard() + sharedViewModel.handle(BootstrapActions.ReAuth(accountPassword)) + } + } + + override fun invalidate() = withState(sharedViewModel) { state -> + super.invalidate() + + if (state.step is BootstrapStep.AccountPassword) { + val isPasswordVisible = state.step.isPasswordVisible + bootstrapAccountPasswordEditText.showPassword(isPasswordVisible, updateCursor = false) + ssss_view_show_password.setImageResource(if (isPasswordVisible) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapActions.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapActions.kt new file mode 100644 index 0000000000..7c0f2c1c46 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapActions.kt @@ -0,0 +1,43 @@ +/* + * 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.crypto.recover + +import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth +import im.vector.riotx.core.platform.VectorViewModelAction +import java.io.OutputStream + +sealed class BootstrapActions : VectorViewModelAction { + + // Navigation + + object GoBack : BootstrapActions() + data class GoToConfirmPassphrase(val passphrase: String) : BootstrapActions() + object GoToCompleted : BootstrapActions() + object GoToEnterAccountPassword : BootstrapActions() + + data class DoInitialize(val passphrase: String, val auth: UserPasswordAuth? = null) : BootstrapActions() + data class DoInitializeGeneratedKey(val auth: UserPasswordAuth? = null) : BootstrapActions() + object TogglePasswordVisibility : BootstrapActions() + data class UpdateCandidatePassphrase(val pass: String) : BootstrapActions() + data class UpdateConfirmCandidatePassphrase(val pass: String) : BootstrapActions() + data class ReAuth(val pass: String) : BootstrapActions() + object RecoveryKeySaved : BootstrapActions() + object Completed : BootstrapActions() + object SaveReqQueryStarted : BootstrapActions() + data class SaveKeyToUri(val os: OutputStream) : BootstrapActions() + object SaveReqFailed : BootstrapActions() +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapBottomSheet.kt new file mode 100644 index 0000000000..6305f161e3 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapBottomSheet.kt @@ -0,0 +1,161 @@ +/* + * 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.crypto.recover + +import android.app.Dialog +import android.os.Bundle +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.extensions.commitTransaction +import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment +import kotlinx.android.synthetic.main.bottom_sheet_bootstrap.* +import javax.inject.Inject +import kotlin.reflect.KClass + +class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() { + + override val showExpanded = true + + @Inject + lateinit var bootstrapViewModelFactory: BootstrapSharedViewModel.Factory + + private val viewModel by fragmentViewModel(BootstrapSharedViewModel::class) + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun getLayoutResId() = R.layout.bottom_sheet_bootstrap + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewModel.observeViewEvents { event -> + when (event) { + is BootstrapViewEvents.Dismiss -> dismiss() + is BootstrapViewEvents.ModalError -> { + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.dialog_title_error) + .setMessage(event.error) + .setPositiveButton(R.string.ok, null) + .show() + } + BootstrapViewEvents.RecoveryKeySaved -> { + KeepItSafeDialog().show(requireActivity()) + } + is BootstrapViewEvents.SkipBootstrap -> { + promptSkip(event.genKeyOption) + } + } + } + } + + private fun promptSkip(genKeyOption: Boolean) { + AlertDialog.Builder(requireContext()) + .setTitle(R.string.are_you_sure) + .setMessage(if (genKeyOption) R.string.bootstrap_skip_text else R.string.bootstrap_skip_text_no_gen_key) + .setPositiveButton(R.string._continue, null) + .apply { + if (genKeyOption) { + setNeutralButton(R.string.generate_message_key) { _, _ -> + viewModel.handle(BootstrapActions.DoInitializeGeneratedKey()) + } + } + } + .setNegativeButton(R.string.skip) { _, _ -> + dismiss() + } + .show() + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val rootView = super.onCreateView(inflater, container, savedInstanceState) + dialog?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) + return rootView + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return super.onCreateDialog(savedInstanceState).apply { + setOnKeyListener { _, keyCode, keyEvent -> + if (keyCode == KeyEvent.KEYCODE_BACK && keyEvent.action == KeyEvent.ACTION_UP) { + viewModel.handle(BootstrapActions.GoBack) + true + } else { + false + } + } + } + } + + override fun invalidate() = withState(viewModel) { state -> + + when (state.step) { + is BootstrapStep.SetupPassphrase -> { + bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_password)) + bootstrapTitleText.text = getString(R.string.set_recovery_passphrase, getString(R.string.recovery_passphrase)) + showFragment(BootstrapEnterPassphraseFragment::class, Bundle()) + } + is BootstrapStep.ConfirmPassphrase -> { + bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_password)) + bootstrapTitleText.text = getString(R.string.confirm_recovery_passphrase, getString(R.string.recovery_passphrase)) + showFragment(BootstrapConfirmPassphraseFragment::class, Bundle()) + } + is BootstrapStep.AccountPassword -> { + bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_user)) + bootstrapTitleText.text = getString(R.string.account_password) + showFragment(BootstrapAccountPasswordFragment::class, Bundle()) + } + is BootstrapStep.Initializing -> { + bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_key)) + bootstrapTitleText.text = getString(R.string.bootstrap_loading_title) + showFragment(BootstrapWaitingFragment::class, Bundle()) + } + is BootstrapStep.SaveRecoveryKey -> { + bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_key)) + bootstrapTitleText.text = getString(R.string.keys_backup_setup_step3_please_make_copy) + showFragment(BootstrapSaveRecoveryKeyFragment::class, Bundle()) + } + is BootstrapStep.DoneSuccess -> { + bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_key)) + bootstrapTitleText.text = getString(R.string.bootstrap_finish_title) + showFragment(BootstrapConclusionFragment::class, Bundle()) + } + } + super.invalidate() + } + + private fun showFragment(fragmentClass: KClass, bundle: Bundle) { + if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) { + childFragmentManager.commitTransaction { + replace(R.id.bottomSheetFragmentContainer, + fragmentClass.java, + bundle, + fragmentClass.simpleName + ) + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConclusionFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConclusionFragment.kt new file mode 100644 index 0000000000..d84283b14c --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConclusionFragment.kt @@ -0,0 +1,66 @@ +/* + * 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.crypto.recover + +import android.os.Bundle +import android.view.View +import androidx.core.text.toSpannable +import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.withState +import com.jakewharton.rxbinding3.view.clicks +import im.vector.riotx.R +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.utils.colorizeMatchingText +import io.reactivex.android.schedulers.AndroidSchedulers +import kotlinx.android.synthetic.main.fragment_bootstrap_conclusion.* +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class BootstrapConclusionFragment @Inject constructor( + private val colorProvider: ColorProvider +) : VectorBaseFragment() { + + override fun getLayoutResId() = R.layout.fragment_bootstrap_conclusion + + val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + bootstrapConclusionContinue.clickableView.clicks() + .debounce(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + sharedViewModel.handle(BootstrapActions.Completed) + } + .disposeOnDestroyView() + } + + override fun invalidate() = withState(sharedViewModel) { state -> + if (state.step !is BootstrapStep.DoneSuccess) return@withState + + bootstrapConclusionText.text = getString( + R.string.bootstrap_cross_signing_success, + getString(R.string.recovery_passphrase), + getString(R.string.message_key) + ) + .toSpannable() + .colorizeMatchingText(getString(R.string.recovery_passphrase), colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) + .colorizeMatchingText(getString(R.string.message_key), colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt new file mode 100644 index 0000000000..34a49e852e --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt @@ -0,0 +1,124 @@ +/* + * 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.crypto.recover + +import android.os.Bundle +import android.view.View +import android.view.inputmethod.EditorInfo +import androidx.core.text.toSpannable +import androidx.core.view.isGone +import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.withState +import com.jakewharton.rxbinding3.view.clicks +import com.jakewharton.rxbinding3.widget.editorActionEvents +import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.riotx.R +import im.vector.riotx.core.extensions.hideKeyboard +import im.vector.riotx.core.extensions.showPassword +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.utils.colorizeMatchingText +import io.reactivex.android.schedulers.AndroidSchedulers +import kotlinx.android.synthetic.main.fragment_bootstrap_enter_passphrase.* +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class BootstrapConfirmPassphraseFragment @Inject constructor( + private val colorProvider: ColorProvider +) : VectorBaseFragment() { + + override fun getLayoutResId() = R.layout.fragment_bootstrap_enter_passphrase + + val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + ssss_passphrase_security_progress.isGone = true + + val recPassPhrase = getString(R.string.recovery_passphrase) + bootstrapDescriptionText.text = getString(R.string.bootstrap_info_confirm_text, recPassPhrase) + .toSpannable() + .colorizeMatchingText(recPassPhrase, colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) + + ssss_passphrase_enter_edittext.hint = getString(R.string.passphrase_confirm_passphrase) + + withState(sharedViewModel) { + // set initial value (useful when coming back) + ssss_passphrase_enter_edittext.setText(it.passphraseRepeat ?: "") + ssss_passphrase_enter_edittext.requestFocus() + } + + ssss_passphrase_enter_edittext.editorActionEvents() + .debounce(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + if (it.actionId == EditorInfo.IME_ACTION_DONE) { + submit() + } + } + .disposeOnDestroyView() + + ssss_passphrase_enter_edittext.textChanges() + .subscribe { + ssss_passphrase_enter_til.error = null + sharedViewModel.handle(BootstrapActions.UpdateConfirmCandidatePassphrase(it?.toString() ?: "")) + } + .disposeOnDestroyView() + + sharedViewModel.observeViewEvents { + // when (it) { +// is SharedSecureStorageViewEvent.InlineError -> { +// ssss_passphrase_enter_til.error = it.message +// } +// } + } + + ssss_view_show_password.clicks() + .debounce(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) + } + .disposeOnDestroyView() + } + + private fun submit() = withState(sharedViewModel) { state -> + if (state.step !is BootstrapStep.ConfirmPassphrase) { + return@withState + } + val passphrase = ssss_passphrase_enter_edittext.text?.toString() + if (passphrase.isNullOrBlank()) { + ssss_passphrase_enter_til.error = getString(R.string.passphrase_empty_error_message) + } else if (passphrase != state.passphrase) { + ssss_passphrase_enter_til.error = getString(R.string.passphrase_passphrase_does_not_match) + } else { + view?.hideKeyboard() + sharedViewModel.handle(BootstrapActions.DoInitialize(passphrase)) + } + } + + override fun invalidate() = withState(sharedViewModel) { state -> + super.invalidate() + + if (state.step is BootstrapStep.ConfirmPassphrase) { + val isPasswordVisible = state.step.isPasswordVisible + ssss_passphrase_enter_edittext.showPassword(isPasswordVisible, updateCursor = false) + ssss_view_show_password.setImageResource(if (isPasswordVisible) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt new file mode 100644 index 0000000000..a19604d78e --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt @@ -0,0 +1,242 @@ +/* + * 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.crypto.recover + +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.failure.MatrixError +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME +import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME +import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME +import im.vector.matrix.android.api.session.securestorage.EmptyKeySigner +import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService +import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo +import im.vector.matrix.android.internal.auth.data.LoginFlowTypes +import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse +import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion +import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth +import im.vector.matrix.android.internal.di.MoshiProvider +import im.vector.matrix.android.internal.util.awaitCallback +import im.vector.riotx.R +import im.vector.riotx.core.platform.WaitingViewData +import im.vector.riotx.core.resources.StringProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import timber.log.Timber +import java.util.UUID +import javax.inject.Inject + +sealed class BootstrapResult { + + data class Success(val keyInfo: SsssKeyCreationInfo) : BootstrapResult() + + abstract class Failure(val error: String?) : BootstrapResult() + + class UnsupportedAuthFlow : Failure(null) + + data class GenericError(val failure: Throwable) : Failure(failure.localizedMessage) + data class InvalidPasswordError(val matrixError: MatrixError) : Failure(null) + class FailedToCreateSSSSKey(failure: Throwable) : Failure(failure.localizedMessage) + class FailedToSetDefaultSSSSKey(failure: Throwable) : Failure(failure.localizedMessage) + class FailedToStorePrivateKeyInSSSS(failure: Throwable) : Failure(failure.localizedMessage) + object MissingPrivateKey : Failure(null) + + data class PasswordAuthFlowMissing(val sessionId: String, val userId: String) : Failure(null) +} + +interface BootstrapProgressListener { + fun onProgress(data: WaitingViewData) +} + +data class Params( + val userPasswordAuth: UserPasswordAuth? = null, + val progressListener: BootstrapProgressListener? = null, + val passphrase: String? +) + +class BootstrapCrossSigningTask @Inject constructor( + private val session: Session, + private val stringProvider: StringProvider +) { + + operator fun invoke( + scope: CoroutineScope, + params: Params, + onResult: (BootstrapResult) -> Unit = {} + ) { + val backgroundJob = scope.async { execute(params) } + scope.launch { onResult(backgroundJob.await()) } + } + + suspend fun execute(params: Params): BootstrapResult { + params.progressListener?.onProgress( + WaitingViewData( + stringProvider.getString(R.string.bootstrap_crosssigning_progress_initializing), + isIndeterminate = true + ) + ) + val crossSigningService = session.cryptoService().crossSigningService() + + try { + awaitCallback { + crossSigningService.initializeCrossSigning(params.userPasswordAuth, it) + } + } catch (failure: Throwable) { + return handleInitializeXSigningError(failure) + } + + val keyInfo: SsssKeyCreationInfo + + val ssssService = session.sharedSecretStorageService + + params.progressListener?.onProgress( + WaitingViewData( + stringProvider.getString(R.string.bootstrap_crosssigning_progress_pbkdf2), + isIndeterminate = true) + ) + try { + keyInfo = awaitCallback { + params.passphrase?.let { passphrase -> + ssssService.generateKeyWithPassphrase( + UUID.randomUUID().toString(), + "ssss_key", + passphrase, + EmptyKeySigner(), + null, + it + ) + } ?: kotlin.run { + ssssService.generateKey( + UUID.randomUUID().toString(), + "ssss_key", + EmptyKeySigner(), + it + ) + } + } + } catch (failure: Failure) { + return BootstrapResult.FailedToCreateSSSSKey(failure) + } + + params.progressListener?.onProgress( + WaitingViewData( + stringProvider.getString(R.string.bootstrap_crosssigning_progress_default_key), + isIndeterminate = true) + ) + try { + awaitCallback { + ssssService.setDefaultKey(keyInfo.keyId, it) + } + } catch (failure: Failure) { + // Maybe we could just ignore this error? + return BootstrapResult.FailedToSetDefaultSSSSKey(failure) + } + + val xKeys = crossSigningService.getCrossSigningPrivateKeys() + val mskPrivateKey = xKeys?.master ?: return BootstrapResult.MissingPrivateKey + val sskPrivateKey = xKeys.selfSigned ?: return BootstrapResult.MissingPrivateKey + val uskPrivateKey = xKeys.user ?: return BootstrapResult.MissingPrivateKey + + try { + params.progressListener?.onProgress( + WaitingViewData( + stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_msk), + isIndeterminate = true + ) + ) + awaitCallback { + ssssService.storeSecret( + MASTER_KEY_SSSS_NAME, + mskPrivateKey, + listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)), it + ) + } + params.progressListener?.onProgress( + WaitingViewData( + stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_usk), + isIndeterminate = true + ) + ) + awaitCallback { + ssssService.storeSecret( + USER_SIGNING_KEY_SSSS_NAME, + uskPrivateKey, + listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)), + it + ) + } + params.progressListener?.onProgress( + WaitingViewData( + stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_ssk), isIndeterminate = true + ) + ) + awaitCallback { + ssssService.storeSecret( + SELF_SIGNING_KEY_SSSS_NAME, + sskPrivateKey, + listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)), it + ) + } + } catch (failure: Failure) { + // Maybe we could just ignore this error? + return BootstrapResult.FailedToStorePrivateKeyInSSSS(failure) + } + + params.progressListener?.onProgress( + WaitingViewData( + stringProvider.getString(R.string.bootstrap_crosssigning_progress_key_backup), + isIndeterminate = true + ) + ) + try { + val creationInfo = awaitCallback { + session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it) + } + val version = awaitCallback { + session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it) + } + // Save it for gossiping + session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version) + } catch (failure: Throwable) { + Timber.e("## BootstrapCrossSigningTask: Failed to init keybackup") + } + + return BootstrapResult.Success(keyInfo) + } + + private fun handleInitializeXSigningError(failure: Throwable): BootstrapResult { + if (failure is Failure.ServerError && failure.error.code == MatrixError.M_FORBIDDEN) { + return BootstrapResult.InvalidPasswordError(failure.error) + } else if (failure is Failure.OtherServerError && failure.httpCode == 401) { + try { + MoshiProvider.providesMoshi() + .adapter(RegistrationFlowResponse::class.java) + .fromJson(failure.errorBody) + } catch (e: Exception) { + null + }?.let { flowResponse -> + if (flowResponse.flows?.any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true } != true) { + // can't do this from here + return BootstrapResult.UnsupportedAuthFlow() + } + } + } + return BootstrapResult.GenericError(failure) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapEnterPassphraseFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapEnterPassphraseFragment.kt new file mode 100644 index 0000000000..35e2e1373c --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapEnterPassphraseFragment.kt @@ -0,0 +1,134 @@ +/* + * 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.crypto.recover + +import android.os.Bundle +import android.view.View +import android.view.inputmethod.EditorInfo +import androidx.core.text.toSpannable +import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.withState +import com.jakewharton.rxbinding3.view.clicks +import com.jakewharton.rxbinding3.widget.editorActionEvents +import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.riotx.R +import im.vector.riotx.core.extensions.showPassword +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.utils.colorizeMatchingText +import im.vector.riotx.features.settings.VectorLocale +import io.reactivex.android.schedulers.AndroidSchedulers +import kotlinx.android.synthetic.main.fragment_bootstrap_enter_passphrase.* +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class BootstrapEnterPassphraseFragment @Inject constructor( + private val colorProvider: ColorProvider +) : VectorBaseFragment() { + + override fun getLayoutResId() = R.layout.fragment_bootstrap_enter_passphrase + + val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val recPassPhrase = getString(R.string.recovery_passphrase) + bootstrapDescriptionText.text = getString(R.string.bootstrap_info_text, recPassPhrase) + .toSpannable() + .colorizeMatchingText(recPassPhrase, colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) + + ssss_passphrase_enter_edittext.hint = getString(R.string.passphrase_enter_passphrase) + withState(sharedViewModel) { + // set initial value (usefull when coming back) + ssss_passphrase_enter_edittext.setText(it.passphrase ?: "") + } + ssss_passphrase_enter_edittext.editorActionEvents() + .debounce(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + if (it.actionId == EditorInfo.IME_ACTION_DONE) { + submit() + } + } + .disposeOnDestroyView() + + ssss_passphrase_enter_edittext.textChanges() + .subscribe { + // ssss_passphrase_enter_til.error = null + sharedViewModel.handle(BootstrapActions.UpdateCandidatePassphrase(it?.toString() ?: "")) +// ssss_passphrase_submit.isEnabled = it.isNotBlank() + } + .disposeOnDestroyView() + + sharedViewModel.observeViewEvents { + // when (it) { +// is SharedSecureStorageViewEvent.InlineError -> { +// ssss_passphrase_enter_til.error = it.message +// } +// } + } + + ssss_view_show_password.clicks() + .debounce(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) + } + .disposeOnDestroyView() + } + + private fun submit() = withState(sharedViewModel) { state -> + if (state.step !is BootstrapStep.SetupPassphrase) { + return@withState + } + val score = state.passphraseStrength.invoke()?.score + val passphrase = ssss_passphrase_enter_edittext.text?.toString() + if (passphrase.isNullOrBlank()) { + ssss_passphrase_enter_til.error = getString(R.string.passphrase_empty_error_message) + } else if (score != 4) { + ssss_passphrase_enter_til.error = getString(R.string.passphrase_passphrase_too_weak) + } else { + sharedViewModel.handle(BootstrapActions.GoToConfirmPassphrase(passphrase)) + } + } + + override fun invalidate() = withState(sharedViewModel) { state -> + super.invalidate() + + if (state.step is BootstrapStep.SetupPassphrase) { + val isPasswordVisible = state.step.isPasswordVisible + ssss_passphrase_enter_edittext.showPassword(isPasswordVisible, updateCursor = false) + ssss_view_show_password.setImageResource(if (isPasswordVisible) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black) + + state.passphraseStrength.invoke()?.let { strength -> + val score = strength.score + ssss_passphrase_security_progress.strength = score + if (score in 1..3) { + val hint = + strength.feedback?.getWarning(VectorLocale.applicationLocale)?.takeIf { it.isNotBlank() } + ?: strength.feedback?.getSuggestions(VectorLocale.applicationLocale)?.firstOrNull() + if (hint != null && hint != ssss_passphrase_enter_til.error.toString()) { + ssss_passphrase_enter_til.error = hint + } + } else { + ssss_passphrase_enter_til.error = null + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt new file mode 100644 index 0000000000..05c6f7a53f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt @@ -0,0 +1,144 @@ +/* + * 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.crypto.recover + +import android.app.Activity.RESULT_OK +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.core.text.toSpannable +import androidx.core.view.isVisible +import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.withState +import com.jakewharton.rxbinding3.view.clicks +import im.vector.riotx.R +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.utils.colorizeMatchingText +import im.vector.riotx.core.utils.startSharePlainTextIntent +import im.vector.riotx.core.utils.toast +import io.reactivex.android.schedulers.AndroidSchedulers +import kotlinx.android.synthetic.main.fragment_bootstrap_save_key.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class BootstrapSaveRecoveryKeyFragment @Inject constructor( + private val colorProvider: ColorProvider +) : VectorBaseFragment() { + + override fun getLayoutResId() = R.layout.fragment_bootstrap_save_key + + val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + bootstrapSaveText.text = getString(R.string.bootstrap_save_key_description, getString(R.string.message_key), getString(R.string.recovery_passphrase)) + .toSpannable() + .colorizeMatchingText(getString(R.string.recovery_passphrase), colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) + .colorizeMatchingText(getString(R.string.message_key), colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) + + recoverySave.clickableView.clicks() + .debounce(600, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + downloadRecoveryKey() + } + .disposeOnDestroyView() + + recoveryCopy.clickableView.clicks() + .debounce(600, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + shareRecoveryKey() + } + .disposeOnDestroyView() + + recoveryContinue.clickableView.clicks() + .debounce(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + sharedViewModel.handle(BootstrapActions.GoToCompleted) + } + .disposeOnDestroyView() + } + + private fun downloadRecoveryKey() = withState(sharedViewModel) { _ -> + + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.type = "text/plain" + intent.putExtra(Intent.EXTRA_TITLE, "riot-recovery-key.txt") + + try { + sharedViewModel.handle(BootstrapActions.SaveReqQueryStarted) + startActivityForResult(Intent.createChooser(intent, getString(R.string.keys_backup_setup_step3_please_make_copy)), REQUEST_CODE_SAVE) + } catch (activityNotFoundException: ActivityNotFoundException) { + requireActivity().toast(R.string.error_no_external_application_found) + sharedViewModel.handle(BootstrapActions.SaveReqFailed) + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == REQUEST_CODE_SAVE) { + val uri = data?.data + if (resultCode == RESULT_OK && uri != null) { + GlobalScope.launch(Dispatchers.IO) { + try { + sharedViewModel.handle(BootstrapActions.SaveKeyToUri(context!!.contentResolver!!.openOutputStream(uri)!!)) + } catch (failure: Throwable) { + sharedViewModel.handle(BootstrapActions.SaveReqFailed) + } + } + } else { + // result code seems to be always cancelled here.. so act as if it was saved + sharedViewModel.handle(BootstrapActions.SaveReqFailed) + } + return + } else if (requestCode == REQUEST_CODE_COPY) { + sharedViewModel.handle(BootstrapActions.RecoveryKeySaved) + } + super.onActivityResult(requestCode, resultCode, data) + } + + private fun shareRecoveryKey() = withState(sharedViewModel) { state -> + val recoveryKey = state.recoveryKeyCreationInfo?.recoveryKey?.formatRecoveryKey() + ?: return@withState + + startSharePlainTextIntent(this, + context?.getString(R.string.keys_backup_setup_step3_share_intent_chooser_title), + recoveryKey, + context?.getString(R.string.recovery_key), REQUEST_CODE_COPY) + } + + override fun invalidate() = withState(sharedViewModel) { state -> + val step = state.step + if (step !is BootstrapStep.SaveRecoveryKey) return@withState + + recoveryContinue.isVisible = step.isSaved + bootstrapRecoveryKeyText.text = state.recoveryKeyCreationInfo?.recoveryKey?.formatRecoveryKey() + } + + companion object { + const val REQUEST_CODE_SAVE = 123 + const val REQUEST_CODE_COPY = 124 + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt new file mode 100644 index 0000000000..998899374c --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt @@ -0,0 +1,350 @@ +/* + * 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.crypto.recover + +import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.ViewModelContext +import com.nulabinc.zxcvbn.Strength +import com.nulabinc.zxcvbn.Zxcvbn +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo +import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth +import im.vector.riotx.R +import im.vector.riotx.core.extensions.exhaustive +import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.platform.WaitingViewData +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.features.login.ReAuthHelper +import kotlinx.coroutines.launch +import java.io.OutputStream + +data class BootstrapViewState( + val step: BootstrapStep = BootstrapStep.SetupPassphrase(false), + val passphrase: String? = null, + val passphraseRepeat: String? = null, + val crossSigningInitialization: Async = Uninitialized, + val passphraseStrength: Async = Uninitialized, + val passphraseConfirmMatch: Async = Uninitialized, + val recoveryKeyCreationInfo: SsssKeyCreationInfo? = null, + val initializationWaitingViewData: WaitingViewData? = null, + val currentReAuth: UserPasswordAuth? = null, + val recoverySaveFileProcess: Async = Uninitialized +) : MvRxState + +sealed class BootstrapStep { + data class SetupPassphrase(val isPasswordVisible: Boolean) : BootstrapStep() + data class ConfirmPassphrase(val isPasswordVisible: Boolean) : BootstrapStep() + data class AccountPassword(val isPasswordVisible: Boolean, val failure: String? = null) : BootstrapStep() + object Initializing : BootstrapStep() + data class SaveRecoveryKey(val isSaved: Boolean) : BootstrapStep() + object DoneSuccess : BootstrapStep() +} + +class BootstrapSharedViewModel @AssistedInject constructor( + @Assisted initialState: BootstrapViewState, + private val stringProvider: StringProvider, + private val session: Session, + private val bootstrapTask: BootstrapCrossSigningTask, + private val reAuthHelper: ReAuthHelper +) : VectorViewModel(initialState) { + + private val zxcvbn = Zxcvbn() + + @AssistedInject.Factory + interface Factory { + fun create(initialState: BootstrapViewState): BootstrapSharedViewModel + } + + override fun handle(action: BootstrapActions) = withState { state -> + when (action) { + is BootstrapActions.GoBack -> queryBack() + BootstrapActions.TogglePasswordVisibility -> { + when (state.step) { + is BootstrapStep.SetupPassphrase -> { + setState { + copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible)) + } + } + is BootstrapStep.ConfirmPassphrase -> { + setState { + copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible)) + } + } + + is BootstrapStep.AccountPassword -> { + setState { + copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible)) + } + } + else -> { + } + } + } + + is BootstrapActions.UpdateCandidatePassphrase -> { + val strength = zxcvbn.measure(action.pass) + setState { + copy( + passphrase = action.pass, + passphraseStrength = Success(strength) + ) + } + } + is BootstrapActions.GoToConfirmPassphrase -> { + setState { + copy( + passphrase = action.passphrase, + step = BootstrapStep.ConfirmPassphrase( + isPasswordVisible = (state.step as? BootstrapStep.SetupPassphrase)?.isPasswordVisible ?: false + ) + ) + } + } + is BootstrapActions.UpdateConfirmCandidatePassphrase -> { + setState { + copy( + passphraseRepeat = action.pass + ) + } + } + is BootstrapActions.DoInitialize -> { + if (state.passphrase == state.passphraseRepeat) { + val auth = action.auth ?: reAuthHelper.rememberedAuth() + if (auth == null) { + setState { + copy( + step = BootstrapStep.AccountPassword(false) + ) + } + } else { + startInitializeFlow(action.auth) + } + } else { + setState { + copy( + passphraseConfirmMatch = Fail(Throwable(stringProvider.getString(R.string.passphrase_passphrase_does_not_match))) + ) + } + } + } + is BootstrapActions.DoInitializeGeneratedKey -> { + val auth = action.auth ?: reAuthHelper.rememberedAuth() + if (auth == null) { + setState { + copy( + passphrase = null, + passphraseRepeat = null, + step = BootstrapStep.AccountPassword(false) + ) + } + } else { + startInitializeFlow(action.auth) + } + } + BootstrapActions.RecoveryKeySaved -> { + _viewEvents.post(BootstrapViewEvents.RecoveryKeySaved) + setState { + copy(step = BootstrapStep.SaveRecoveryKey(true)) + } + } + BootstrapActions.Completed -> { + _viewEvents.post(BootstrapViewEvents.Dismiss) + } + BootstrapActions.GoToCompleted -> { + setState { + copy(step = BootstrapStep.DoneSuccess) + } + } + BootstrapActions.SaveReqQueryStarted -> { + setState { + copy(recoverySaveFileProcess = Loading()) + } + } + is BootstrapActions.SaveKeyToUri -> { + saveRecoveryKeyToUri(action.os) + } + BootstrapActions.SaveReqFailed -> { + setState { + copy(recoverySaveFileProcess = Uninitialized) + } + } + BootstrapActions.GoToEnterAccountPassword -> { + setState { + copy(step = BootstrapStep.AccountPassword(false)) + } + } + is BootstrapActions.ReAuth -> { + startInitializeFlow( + state.currentReAuth?.copy(password = action.pass) + ?: UserPasswordAuth(user = session.myUserId, password = action.pass) + ) + } + }.exhaustive + } + + // ======================================= + // Business Logic + // ======================================= + private fun saveRecoveryKeyToUri(os: OutputStream) = withState { state -> + viewModelScope.launch { + kotlin.runCatching { + os.use { + os.write((state.recoveryKeyCreationInfo?.recoveryKey?.formatRecoveryKey() ?: "").toByteArray()) + } + }.fold({ + setState { + _viewEvents.post(BootstrapViewEvents.RecoveryKeySaved) + copy( + recoverySaveFileProcess = Success(Unit), + step = BootstrapStep.SaveRecoveryKey(isSaved = true) + ) + } + }, { + setState { + copy(recoverySaveFileProcess = Fail(it)) + } + }) + } + } + + private fun startInitializeFlow(auth: UserPasswordAuth?) { + setState { + copy(step = BootstrapStep.Initializing) + } + + val progressListener = object : BootstrapProgressListener { + override fun onProgress(data: WaitingViewData) { + setState { + copy( + initializationWaitingViewData = data + ) + } + } + } + + withState { state -> + viewModelScope.launch { + bootstrapTask.invoke(this, Params( + userPasswordAuth = auth ?: reAuthHelper.rememberedAuth(), + progressListener = progressListener, + passphrase = state.passphrase + )) { + when (it) { + is BootstrapResult.Success -> { + setState { + copy( + recoveryKeyCreationInfo = it.keyInfo, + step = BootstrapStep.SaveRecoveryKey(false) + ) + } + } + is BootstrapResult.PasswordAuthFlowMissing -> { + setState { + copy( + currentReAuth = UserPasswordAuth(session = it.sessionId, user = it.userId), + step = BootstrapStep.AccountPassword(false) + ) + } + } + is BootstrapResult.UnsupportedAuthFlow -> { + _viewEvents.post(BootstrapViewEvents.ModalError(stringProvider.getString(R.string.auth_flow_not_supported))) + _viewEvents.post(BootstrapViewEvents.Dismiss) + } + is BootstrapResult.InvalidPasswordError -> { + // it's a bad password + setState { + copy( + // We clear the auth session, to avoid 'Requested operation has changed during the UI authentication session' error + currentReAuth = UserPasswordAuth(session = null, user = session.myUserId), + step = BootstrapStep.AccountPassword(false, stringProvider.getString(R.string.auth_invalid_login_param)) + ) + } + } + is BootstrapResult.Failure -> { + if (it is BootstrapResult.GenericError + && it.failure is im.vector.matrix.android.api.failure.Failure.OtherServerError + && it.failure.httpCode == 401) { + } else { + _viewEvents.post(BootstrapViewEvents.ModalError(it.error ?: stringProvider.getString(R.string.matrix_error))) + setState { + copy( + step = BootstrapStep.ConfirmPassphrase(false) + ) + } + } + } + } + } + } + } + } + + // ======================================= + // Fragment interaction + // ======================================= + + private fun queryBack() = withState { state -> + when (state.step) { + is BootstrapStep.SetupPassphrase -> { + // do we let you cancel from here? + _viewEvents.post(BootstrapViewEvents.SkipBootstrap()) + } + is BootstrapStep.ConfirmPassphrase -> { + setState { + copy( + step = BootstrapStep.SetupPassphrase( + isPasswordVisible = (state.step as? BootstrapStep.ConfirmPassphrase)?.isPasswordVisible ?: false + ) + ) + } + } + is BootstrapStep.AccountPassword -> { + _viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null)) + } + BootstrapStep.Initializing -> { + // do we let you cancel from here? + _viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null)) + } + is BootstrapStep.SaveRecoveryKey, + BootstrapStep.DoneSuccess -> { + // nop + } + } + } + + // ====================================== + // Companion, view model assisted creation + // ====================================== + + companion object : MvRxViewModelFactory { + + override fun create(viewModelContext: ViewModelContext, state: BootstrapViewState): BootstrapSharedViewModel? { + val fragment: BootstrapBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.bootstrapViewModelFactory.create(state) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapViewEvents.kt new file mode 100644 index 0000000000..679eabd561 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapViewEvents.kt @@ -0,0 +1,26 @@ +/* + * 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.crypto.recover + +import im.vector.riotx.core.platform.VectorViewEvents + +sealed class BootstrapViewEvents : VectorViewEvents { + object Dismiss : BootstrapViewEvents() + data class ModalError(val error: String) : BootstrapViewEvents() + object RecoveryKeySaved: BootstrapViewEvents() + data class SkipBootstrap(val genKeyOption: Boolean = true): BootstrapViewEvents() +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapWaitingFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapWaitingFragment.kt new file mode 100644 index 0000000000..ff79fa6a4b --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapWaitingFragment.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.riotx.features.crypto.recover + +import android.os.Bundle +import android.view.View +import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.riotx.R +import im.vector.riotx.core.platform.VectorBaseFragment +import kotlinx.android.synthetic.main.fragment_bootstrap_waiting.* +import javax.inject.Inject + +class BootstrapWaitingFragment @Inject constructor() : VectorBaseFragment() { + + override fun getLayoutResId() = R.layout.fragment_bootstrap_waiting + + val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + } + + override fun invalidate() = withState(sharedViewModel) { state -> + if (state.step !is BootstrapStep.Initializing) return@withState + bootstrapLoadingStatusText.text = state.initializationWaitingViewData?.message + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/KeepItSafeDialog.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/KeepItSafeDialog.kt new file mode 100644 index 0000000000..c8348ea7e9 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/KeepItSafeDialog.kt @@ -0,0 +1,68 @@ +/* + * 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.crypto.recover + +import android.app.Activity +import android.content.DialogInterface +import android.view.KeyEvent +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat +import im.vector.riotx.R +import me.gujun.android.span.image +import me.gujun.android.span.span + +class KeepItSafeDialog { + + fun show(activity: Activity) { + val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_recovery_key_saved_info, null) + + val descriptionText = dialogLayout.findViewById(R.id.keepItSafeText) + + descriptionText.text = span { + span { + image(ContextCompat.getDrawable(activity, R.drawable.ic_check_on)!!) + +" " + +activity.getString(R.string.bootstrap_crosssigning_print_it) + +"\n\n" + image(ContextCompat.getDrawable(activity, R.drawable.ic_check_on)!!) + +" " + +activity.getString(R.string.bootstrap_crosssigning_save_usb) + +"\n\n" + image(ContextCompat.getDrawable(activity, R.drawable.ic_check_on)!!) + +" " + +activity.getString(R.string.bootstrap_crosssigning_save_cloud) + +"\n\n" + } + } + + AlertDialog.Builder(activity) +// .setIcon(android.R.drawable.ic_dialog_alert) +// .setTitle(R.string.devices_delete_dialog_title) + .setView(dialogLayout) + .setPositiveButton(R.string.ok, null) + .setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event -> + if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { + dialog.cancel() + return@OnKeyListener true + } + false + }) + .create() + .show() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/RecoveryKeyExt.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/RecoveryKeyExt.kt new file mode 100644 index 0000000000..29453af932 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/RecoveryKeyExt.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.riotx.features.crypto.recover + +fun String.formatRecoveryKey(): String { + return this.replace(" ", "") + .chunked(16) + .joinToString("\n") { + it + .chunked(4) + .joinToString(" ") + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/IncomingVerificationRequestHandler.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/IncomingVerificationRequestHandler.kt index e7e26f52a4..ccd3e6578a 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/IncomingVerificationRequestHandler.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/IncomingVerificationRequestHandler.kt @@ -17,15 +17,17 @@ package im.vector.riotx.features.crypto.verification import android.content.Context import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.crypto.verification.PendingVerificationRequest import im.vector.matrix.android.api.session.crypto.verification.VerificationService import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState -import im.vector.matrix.android.api.session.crypto.verification.PendingVerificationRequest +import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.R import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.features.home.room.detail.RoomDetailActivity import im.vector.riotx.features.home.room.detail.RoomDetailArgs import im.vector.riotx.features.popup.PopupAlertManager +import im.vector.riotx.features.popup.VerificationVectorAlert import im.vector.riotx.features.themes.ThemeUtils import javax.inject.Inject import javax.inject.Singleton @@ -34,7 +36,9 @@ import javax.inject.Singleton * Listens to the VerificationManager and add a new notification when an incoming request is detected. */ @Singleton -class IncomingVerificationRequestHandler @Inject constructor(private val context: Context) : VerificationService.Listener { +class IncomingVerificationRequestHandler @Inject constructor( + private val context: Context, + private val popupAlertManager: PopupAlertManager) : VerificationService.Listener { private var session: Session? = null @@ -58,7 +62,7 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context val name = session?.getUser(tx.otherUserId)?.displayName ?: tx.otherUserId - val alert = PopupAlertManager.VectorAlert( + val alert = VerificationVectorAlert( uid, context.getString(R.string.sas_incoming_request_notif_title), context.getString(R.string.sas_incoming_request_notif_content, name), @@ -68,12 +72,14 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context // TODO a bit too hugly :/ activity.supportFragmentManager.findFragmentByTag(VerificationBottomSheet.WAITING_SELF_VERIF_TAG)?.let { false.also { - PopupAlertManager.cancelAlert(uid) + popupAlertManager.cancelAlert(uid) } } ?: true } else true }) .apply { + matrixItem = session?.getUser(tx.otherUserId)?.toMatrixItem() + contentAction = Runnable { (weakCurrentActivity?.get() as? VectorBaseActivity)?.let { it.navigator.performDeviceVerification(it, tx.otherUserId, tx.transactionId) @@ -99,11 +105,11 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context // 10mn expiration expirationTimestamp = System.currentTimeMillis() + (10 * 60 * 1000L) } - PopupAlertManager.postVectorAlert(alert) + popupAlertManager.postVectorAlert(alert) } is VerificationTxState.TerminalTxState -> { // cancel related notification - PopupAlertManager.cancelAlert(uid) + popupAlertManager.cancelAlert(uid) } else -> Unit } @@ -115,7 +121,7 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context val name = session?.getUser(pr.otherUserId)?.displayName ?: pr.otherUserId - val alert = PopupAlertManager.VectorAlert( + val alert = VerificationVectorAlert( uniqueIdForVerificationRequest(pr), context.getString(R.string.sas_incoming_request_notif_title), "$name(${pr.otherUserId})", @@ -128,6 +134,8 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context } else true }) .apply { + matrixItem = session?.getUser(pr.otherUserId)?.toMatrixItem() + contentAction = Runnable { (weakCurrentActivity?.get() as? VectorBaseActivity)?.let { val roomId = pr.roomId @@ -148,14 +156,14 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context // 5mn expiration expirationTimestamp = System.currentTimeMillis() + (5 * 60 * 1000L) } - PopupAlertManager.postVectorAlert(alert) + popupAlertManager.postVectorAlert(alert) } } override fun verificationRequestUpdated(pr: PendingVerificationRequest) { // If an incoming request is readied (by another device?) we should discard the alert if (pr.isIncoming && (pr.isReady || pr.handledByOtherSession)) { - PopupAlertManager.cancelAlert(uniqueIdForVerificationRequest(pr)) + popupAlertManager.cancelAlert(uniqueIdForVerificationRequest(pr)) } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt index e1218ec4a9..695716d386 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt @@ -16,14 +16,15 @@ package im.vector.riotx.features.crypto.verification import android.app.Activity +import android.app.Dialog import android.content.Intent import android.os.Bundle import android.os.Parcelable +import android.view.KeyEvent import android.view.View import android.widget.ImageView import android.widget.TextView import androidx.appcompat.app.AlertDialog -import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.view.isVisible import androidx.fragment.app.Fragment import butterknife.BindView @@ -31,22 +32,29 @@ import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME +import im.vector.matrix.android.api.session.crypto.verification.CancelCode import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.commitTransaction import im.vector.riotx.core.extensions.exhaustive +import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.riotx.features.crypto.quads.SharedSecureStorageActivity +import im.vector.riotx.features.crypto.verification.cancel.VerificationCancelFragment +import im.vector.riotx.features.crypto.verification.cancel.VerificationNotMeFragment import im.vector.riotx.features.crypto.verification.choose.VerificationChooseMethodFragment import im.vector.riotx.features.crypto.verification.conclusion.VerificationConclusionFragment import im.vector.riotx.features.crypto.verification.emoji.VerificationEmojiCodeFragment +import im.vector.riotx.features.crypto.verification.qrconfirmation.VerificationQRWaitingFragment import im.vector.riotx.features.crypto.verification.qrconfirmation.VerificationQrScannedByOtherFragment import im.vector.riotx.features.crypto.verification.request.VerificationRequestFragment import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.settings.VectorSettingsActivity import kotlinx.android.parcel.Parcelize import timber.log.Timber import javax.inject.Inject @@ -58,6 +66,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { data class VerificationArgs( val otherUserId: String, val verificationId: String? = null, + val verificationLocalId: String? = null, val roomId: String? = null, // Special mode where UX should show loading wheel until other session sends a request/tx val selfVerificationMode: Boolean = false @@ -80,13 +89,17 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { lateinit var otherUserNameText: TextView @BindView(R.id.verificationRequestShield) - lateinit var otherUserShield: View + lateinit var otherUserShield: ImageView @BindView(R.id.verificationRequestAvatar) lateinit var otherUserAvatarImageView: ImageView override fun getLayoutResId() = R.layout.bottom_sheet_verification + init { + isCancelable = false + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -97,7 +110,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { startActivityForResult(SharedSecureStorageActivity.newIntent( requireContext(), null, // use default key - listOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME), + listOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME), SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS ), SECRET_REQUEST_CODE) } @@ -110,10 +123,27 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { .show() Unit } + VerificationBottomSheetViewEvents.GoToSettings -> { + dismiss() + (activity as? VectorBaseActivity)?.navigator?.openSettings(requireContext(), VectorSettingsActivity.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY) + } }.exhaustive } } + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return super.onCreateDialog(savedInstanceState).apply { + setOnKeyListener { _, keyCode, keyEvent -> + if (keyCode == KeyEvent.KEYCODE_BACK && keyEvent.action == KeyEvent.ACTION_UP) { + viewModel.queryCancel() + true + } else { + false + } + } + } + } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (resultCode == Activity.RESULT_OK && requestCode == SECRET_REQUEST_CODE) { data?.getStringExtra(SharedSecureStorageActivity.EXTRA_DATA_RESULT)?.let { @@ -127,15 +157,16 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { state.otherUserMxItem?.let { matrixItem -> if (state.isMe) { + avatarRenderer.render(matrixItem, otherUserAvatarImageView) if (state.sasTransactionState == VerificationTxState.Verified || state.qrTransactionState == VerificationTxState.Verified || state.verifiedFromPrivateKeys) { - otherUserAvatarImageView.setImageResource(R.drawable.ic_shield_trusted) + otherUserShield.setImageResource(R.drawable.ic_shield_trusted) } else { - otherUserAvatarImageView.setImageResource(R.drawable.ic_shield_warning) + otherUserShield.setImageResource(R.drawable.ic_shield_warning) } otherUserNameText.text = getString(R.string.complete_security) - otherUserShield.isVisible = false + otherUserShield.isVisible = true } else { avatarRenderer.render(matrixItem, otherUserAvatarImageView) @@ -149,6 +180,18 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { } } + if (state.userThinkItsNotHim) { + otherUserNameText.text = getString(R.string.dialog_title_warning) + showFragment(VerificationNotMeFragment::class, Bundle()) + return@withState + } + + if (state.userWantsToCancel) { + otherUserNameText.text = getString(R.string.are_you_sure) + showFragment(VerificationCancelFragment::class, Bundle()) + return@withState + } + if (state.selfVerificationMode && state.verifiedFromPrivateKeys) { showFragment(VerificationConclusionFragment::class, Bundle().apply { putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(true, null, state.isMe)) @@ -202,6 +245,13 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { showFragment(VerificationQrScannedByOtherFragment::class, Bundle()) return@withState } + is VerificationTxState.Started, + is VerificationTxState.WaitingOtherReciprocateConfirm -> { + showFragment(VerificationQRWaitingFragment::class, Bundle().apply { + putParcelable(MvRx.KEY_ARG, VerificationQRWaitingFragment.Args(state.isMe, state.otherUserMxItem?.getBestName() ?: "")) + }) + return@withState + } is VerificationTxState.Verified -> { showFragment(VerificationConclusionFragment::class, Bundle().apply { putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(true, null, state.isMe)) @@ -222,7 +272,14 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { // Transaction has not yet started if (state.pendingRequest.invoke()?.cancelConclusion != null) { // The request has been declined, we should dismiss - dismiss() + otherUserNameText.text = getString(R.string.verification_cancelled) + showFragment(VerificationConclusionFragment::class, Bundle().apply { + putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args( + false, + state.pendingRequest.invoke()?.cancelConclusion?.value ?: CancelCode.User.value, + state.isMe)) + }) + return@withState } // If it's an outgoing @@ -267,6 +324,10 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { } } + override fun dismiss() { + super.dismiss() + } + companion object { const val SECRET_REQUEST_CODE = 101 @@ -299,11 +360,11 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { } } -fun View.getParentCoordinatorLayout(): CoordinatorLayout? { - var current = this as? View - while (current != null) { - if (current is CoordinatorLayout) return current - current = current.parent as? View - } - return null -} +// fun View.getParentCoordinatorLayout(): CoordinatorLayout? { +// var current = this as? View +// while (current != null) { +// if (current is CoordinatorLayout) return current +// current = current.parent as? View +// } +// return null +// } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewEvents.kt index d7c02a8d3b..7e3a5441de 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewEvents.kt @@ -24,5 +24,6 @@ import im.vector.riotx.core.platform.VectorViewEvents sealed class VerificationBottomSheetViewEvents : VectorViewEvents { object Dismiss : VerificationBottomSheetViewEvents() object AccessSecretStore : VerificationBottomSheetViewEvents() + object GoToSettings : VerificationBottomSheetViewEvents() data class ModalError(val errorMessage: CharSequence) : VerificationBottomSheetViewEvents() } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt index db8dd895b4..f5f92c381d 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt @@ -15,6 +15,7 @@ */ package im.vector.riotx.features.crypto.verification +import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Async import com.airbnb.mvrx.Fail import com.airbnb.mvrx.FragmentViewModelContext @@ -28,10 +29,13 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME +import im.vector.matrix.android.api.session.crypto.verification.CancelCode import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction +import im.vector.matrix.android.api.session.crypto.verification.PendingVerificationRequest import im.vector.matrix.android.api.session.crypto.verification.QrCodeVerificationTransaction import im.vector.matrix.android.api.session.crypto.verification.SasVerificationTransaction import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod @@ -44,9 +48,15 @@ import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64 import im.vector.matrix.android.internal.crypto.crosssigning.isVerified -import im.vector.matrix.android.api.session.crypto.verification.PendingVerificationRequest +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult +import im.vector.matrix.android.internal.crypto.keysbackup.util.computeRecoveryKey +import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult +import im.vector.matrix.android.internal.util.awaitCallback +import im.vector.riotx.R import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.resources.StringProvider +import kotlinx.coroutines.launch import timber.log.Timber data class VerificationBottomSheetViewState( @@ -60,14 +70,18 @@ data class VerificationBottomSheetViewState( // true when we display the loading and we wait for the other (incoming request) val selfVerificationMode: Boolean = false, val verifiedFromPrivateKeys: Boolean = false, - val isMe: Boolean = false + val isMe: Boolean = false, + val currentDeviceCanCrossSign: Boolean = false, + val userWantsToCancel: Boolean = false, + val userThinkItsNotHim: Boolean = false ) : MvRxState class VerificationBottomSheetViewModel @AssistedInject constructor( @Assisted initialState: VerificationBottomSheetViewState, - @Assisted args: VerificationBottomSheet.VerificationArgs, + @Assisted val args: VerificationBottomSheet.VerificationArgs, private val session: Session, - private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider) + private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider, + private val stringProvider: StringProvider) : VectorViewModel(initialState), VerificationService.Listener { @@ -111,7 +125,8 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( pendingRequest = if (pr != null) Success(pr) else Uninitialized, selfVerificationMode = selfVerificationMode, roomId = args.roomId, - isMe = args.otherUserId == session.myUserId + isMe = args.otherUserId == session.myUserId, + currentDeviceCanCrossSign = session.cryptoService().crossSigningService().canCrossSign() ) } @@ -137,6 +152,62 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( args: VerificationBottomSheet.VerificationArgs): VerificationBottomSheetViewModel } + fun queryCancel() = withState { state -> + if (state.userThinkItsNotHim) { + setState { + copy(userThinkItsNotHim = false) + } + } else { + // if the verification is already done you can't cancel anymore + if (state.pendingRequest.invoke()?.cancelConclusion != null || state.sasTransactionState is VerificationTxState.TerminalTxState) { + // you cannot cancel anymore + } else { + setState { + copy(userWantsToCancel = true) + } + } + } + } + + fun confirmCancel() = withState { state -> + cancelAllPendingVerifications(state) + _viewEvents.post(VerificationBottomSheetViewEvents.Dismiss) + } + + private fun cancelAllPendingVerifications(state: VerificationBottomSheetViewState) { + session.cryptoService() + .verificationService().getExistingVerificationRequest(state.otherUserMxItem?.id ?: "", state.transactionId)?.let { + session.cryptoService().verificationService().cancelVerificationRequest(it) + } + session.cryptoService() + .verificationService() + .getExistingTransaction(state.otherUserMxItem?.id ?: "", state.transactionId ?: "") + ?.cancel(CancelCode.User) + } + + fun continueFromCancel() { + setState { + copy(userWantsToCancel = false) + } + } + + fun continueFromWasNotMe() { + setState { + copy(userThinkItsNotHim = false) + } + } + + fun itWasNotMe() { + setState { + copy(userThinkItsNotHim = true) + } + } + + fun goToSettings() = withState { state -> + cancelAllPendingVerifications(state) + _viewEvents.post(VerificationBottomSheetViewEvents.GoToSettings) + } + companion object : MvRxViewModelFactory { override fun create(viewModelContext: ViewModelContext, state: VerificationBottomSheetViewState): VerificationBottomSheetViewModel? { @@ -273,40 +344,83 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( _viewEvents.post(VerificationBottomSheetViewEvents.AccessSecretStore) } is VerificationAction.GotResultFromSsss -> { - try { - action.cypherData.fromBase64().inputStream().use { ins -> - val res = session.loadSecureSecret>(ins, action.alias) - val trustResult = session.cryptoService().crossSigningService().checkTrustFromPrivateKeys( - res?.get(MASTER_KEY_SSSS_NAME), - res?.get(USER_SIGNING_KEY_SSSS_NAME), - res?.get(SELF_SIGNING_KEY_SSSS_NAME) - ) - if (trustResult.isVerified()) { - // Sign this device and upload the signature - session.sessionParams.credentials.deviceId?.let { deviceId -> - session.cryptoService() - .crossSigningService().trustDevice(deviceId, object : MatrixCallback { - override fun onFailure(failure: Throwable) { - Timber.w(failure, "Failed to sign my device after recovery") - } - }) - } - - setState { - copy(verifiedFromPrivateKeys = true) - } - } else { - // POP UP something - _viewEvents.post(VerificationBottomSheetViewEvents.ModalError("Failed to import keys")) - } - } - } catch (failure: Throwable) { - _viewEvents.post(VerificationBottomSheetViewEvents.ModalError(failure.localizedMessage)) - } + handleSecretBackFromSSSS(action) } }.exhaustive } + private fun handleSecretBackFromSSSS(action: VerificationAction.GotResultFromSsss) { + try { + action.cypherData.fromBase64().inputStream().use { ins -> + val res = session.loadSecureSecret>(ins, action.alias) + val trustResult = session.cryptoService().crossSigningService().checkTrustFromPrivateKeys( + res?.get(MASTER_KEY_SSSS_NAME), + res?.get(USER_SIGNING_KEY_SSSS_NAME), + res?.get(SELF_SIGNING_KEY_SSSS_NAME) + ) + if (trustResult.isVerified()) { + // Sign this device and upload the signature + session.sessionParams.credentials.deviceId?.let { deviceId -> + session.cryptoService() + .crossSigningService().trustDevice(deviceId, object : MatrixCallback { + override fun onFailure(failure: Throwable) { + Timber.w(failure, "Failed to sign my device after recovery") + } + }) + } + + setState { + copy(verifiedFromPrivateKeys = true) + } + + // try to get keybackup key + } else { + // POP UP something + _viewEvents.post(VerificationBottomSheetViewEvents.ModalError(stringProvider.getString(R.string.error_failed_to_import_keys))) + } + + // try the keybackup + tentativeRestoreBackup(res) + Unit + } + } catch (failure: Throwable) { + _viewEvents.post( + VerificationBottomSheetViewEvents.ModalError(failure.localizedMessage ?: stringProvider.getString(R.string.unexpected_error))) + } + } + + private fun tentativeRestoreBackup(res: Map?) { + viewModelScope.launch { + try { + val secret = res?.get(KEYBACKUP_SECRET_SSSS_NAME) ?: return@launch Unit.also { + Timber.v("## Keybackup secret not restored from SSSS") + } + + val version = awaitCallback { + session.cryptoService().keysBackupService().getCurrentVersion(it) + } ?: return@launch + + awaitCallback { + session.cryptoService().keysBackupService().restoreKeysWithRecoveryKey( + version, + computeRecoveryKey(secret.fromBase64()), + null, + null, + null, + it + ) + } + + awaitCallback { + session.cryptoService().keysBackupService().trustKeysBackupVersion(version, true, it) + } + } catch (failure: Throwable) { + // Just ignore for now + Timber.v("## Failed to restore backup after SSSS recovery") + } + } + } + override fun transactionCreated(tx: VerificationTransaction) { transactionUpdated(tx) } @@ -391,7 +505,10 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( || pr.localId == state.pendingRequest.invoke()?.localId || state.pendingRequest.invoke()?.transactionId == pr.transactionId) { setState { - copy(pendingRequest = Success(pr)) + copy( + transactionId = args.verificationId ?: pr.transactionId, + pendingRequest = Success(pr) + ) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationCancelController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationCancelController.kt new file mode 100644 index 0000000000..49b2f7dce1 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationCancelController.kt @@ -0,0 +1,104 @@ +/* + * 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.crypto.verification.cancel + +import androidx.core.text.toSpannable +import com.airbnb.epoxy.EpoxyController +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.dividerItem +import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.core.utils.colorizeMatchingText +import im.vector.riotx.features.crypto.verification.VerificationBottomSheetViewState +import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem +import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem +import javax.inject.Inject + +class VerificationCancelController @Inject constructor( + private val stringProvider: StringProvider, + private val colorProvider: ColorProvider +) : EpoxyController() { + + var listener: Listener? = null + + private var viewState: VerificationBottomSheetViewState? = null + + fun update(viewState: VerificationBottomSheetViewState) { + this.viewState = viewState + requestModelBuild() + } + + override fun buildModels() { + val state = viewState ?: return + + if (state.isMe) { + if (state.currentDeviceCanCrossSign) { + bottomSheetVerificationNoticeItem { + id("notice") + notice(stringProvider.getString(R.string.verify_cancel_self_verification_from_trusted)) + } + } else { + bottomSheetVerificationNoticeItem { + id("notice") + notice(stringProvider.getString(R.string.verify_cancel_self_verification_from_untrusted)) + } + } + } else { + val otherUserID = state.otherUserMxItem?.id ?: "" + val otherDisplayName = state.otherUserMxItem?.displayName ?: "" + bottomSheetVerificationNoticeItem { + id("notice") + notice( + stringProvider.getString(R.string.verify_cancel_other, otherDisplayName, otherUserID) + .toSpannable() + .colorizeMatchingText(otherUserID, colorProvider.getColorFromAttribute(R.attr.vctr_notice_text_color)) + ) + } + } + + dividerItem { + id("sep0") + } + + bottomSheetVerificationActionItem { + id("cancel") + title(stringProvider.getString(R.string.cancel)) + titleColor(colorProvider.getColor(R.color.riotx_destructive_accent)) + iconRes(R.drawable.ic_arrow_right) + iconColor(colorProvider.getColor(R.color.riotx_destructive_accent)) + listener { listener?.onTapCancel() } + } + + dividerItem { + id("sep1") + } + + bottomSheetVerificationActionItem { + id("continue") + title(stringProvider.getString(R.string._continue)) + titleColor(colorProvider.getColor(R.color.riotx_positive_accent)) + iconRes(R.drawable.ic_arrow_right) + iconColor(colorProvider.getColor(R.color.riotx_positive_accent)) + listener { listener?.onTapContinue() } + } + } + + interface Listener { + fun onTapCancel() + fun onTapContinue() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationCancelFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationCancelFragment.kt new file mode 100644 index 0000000000..0c5c070156 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationCancelFragment.kt @@ -0,0 +1,66 @@ +/* + * 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.crypto.verification.cancel + +import android.os.Bundle +import android.view.View +import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.riotx.R +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.configureWith +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.features.crypto.verification.VerificationBottomSheetViewModel +import kotlinx.android.synthetic.main.bottom_sheet_verification_child_fragment.* +import javax.inject.Inject + +class VerificationCancelFragment @Inject constructor( + val controller: VerificationCancelController +) : VectorBaseFragment(), VerificationCancelController.Listener { + + private val viewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class) + + override fun getLayoutResId() = R.layout.bottom_sheet_verification_child_fragment + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupRecyclerView() + } + + override fun onDestroyView() { + bottomSheetVerificationRecyclerView.cleanup() + controller.listener = null + super.onDestroyView() + } + + private fun setupRecyclerView() { + bottomSheetVerificationRecyclerView.configureWith(controller, hasFixedSize = false, disableItemAnimation = true) + controller.listener = this + } + + override fun invalidate() = withState(viewModel) { state -> + controller.update(state) + } + + override fun onTapCancel() { + viewModel.confirmCancel() + } + + override fun onTapContinue() { + viewModel.continueFromCancel() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationNotMeController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationNotMeController.kt new file mode 100644 index 0000000000..3978ab8ba5 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationNotMeController.kt @@ -0,0 +1,82 @@ +/* + * 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.crypto.verification.cancel + +import com.airbnb.epoxy.EpoxyController +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.dividerItem +import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.features.crypto.verification.VerificationBottomSheetViewState +import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem +import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem +import im.vector.riotx.features.html.EventHtmlRenderer +import javax.inject.Inject + +class VerificationNotMeController @Inject constructor( + private val stringProvider: StringProvider, + private val colorProvider: ColorProvider, + private val eventHtmlRenderer: EventHtmlRenderer +) : EpoxyController() { + + var listener: Listener? = null + + private var viewState: VerificationBottomSheetViewState? = null + + fun update(viewState: VerificationBottomSheetViewState) { + this.viewState = viewState + requestModelBuild() + } + + override fun buildModels() { + bottomSheetVerificationNoticeItem { + id("notice") + notice(eventHtmlRenderer.render(stringProvider.getString(R.string.verify_not_me_self_verification))) + } + + dividerItem { + id("sep0") + } + + bottomSheetVerificationActionItem { + id("skip") + title(stringProvider.getString(R.string.skip)) + titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) + iconRes(R.drawable.ic_arrow_right) + iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) + listener { listener?.onTapSkip() } + } + + dividerItem { + id("sep1") + } + + bottomSheetVerificationActionItem { + id("settings") + title(stringProvider.getString(R.string.settings)) + titleColor(colorProvider.getColor(R.color.riotx_positive_accent)) + iconRes(R.drawable.ic_arrow_right) + iconColor(colorProvider.getColor(R.color.riotx_positive_accent)) + listener { listener?.onTapSettings() } + } + } + + interface Listener { + fun onTapSkip() + fun onTapSettings() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationNotMeFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationNotMeFragment.kt new file mode 100644 index 0000000000..b764639078 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationNotMeFragment.kt @@ -0,0 +1,66 @@ +/* + * 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.crypto.verification.cancel + +import android.os.Bundle +import android.view.View +import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.riotx.R +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.configureWith +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.features.crypto.verification.VerificationBottomSheetViewModel +import kotlinx.android.synthetic.main.bottom_sheet_verification_child_fragment.* +import javax.inject.Inject + +class VerificationNotMeFragment @Inject constructor( + val controller: VerificationNotMeController +) : VectorBaseFragment(), VerificationNotMeController.Listener { + + private val viewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class) + + override fun getLayoutResId() = R.layout.bottom_sheet_verification_child_fragment + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupRecyclerView() + } + + override fun onDestroyView() { + bottomSheetVerificationRecyclerView.cleanup() + controller.listener = null + super.onDestroyView() + } + + private fun setupRecyclerView() { + bottomSheetVerificationRecyclerView.configureWith(controller, hasFixedSize = false, disableItemAnimation = true) + controller.listener = this + } + + override fun invalidate() = withState(viewModel) { state -> + controller.update(state) + } + + override fun onTapSkip() { + viewModel.continueFromWasNotMe() + } + + override fun onTapSettings() { + viewModel.goToSettings() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodController.kt index 87bb843291..919869500f 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodController.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodController.kt @@ -95,10 +95,27 @@ class VerificationChooseMethodController @Inject constructor( listener { listener?.doVerifyBySas() } } } + + if (state.isMe && state.canCrossSign) { + dividerItem { + id("sep_notMe") + } + + bottomSheetVerificationActionItem { + id("wasnote") + title(stringProvider.getString(R.string.verify_new_session_was_not_me)) + titleColor(colorProvider.getColor(R.color.riotx_destructive_accent)) + subTitle(stringProvider.getString(R.string.verify_new_session_compromized)) + iconRes(R.drawable.ic_arrow_right) + iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) + listener { listener?.onClickOnWasNotMe() } + } + } } interface Listener { fun openCamera() fun doVerifyBySas() + fun onClickOnWasNotMe() } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodFragment.kt index e0b7f97383..eb32f5b0e3 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodFragment.kt @@ -89,6 +89,10 @@ class VerificationChooseMethodFragment @Inject constructor( } } + override fun onClickOnWasNotMe() { + sharedViewModel.itWasNotMe() + } + private fun doOpenQRCodeScanner() { QrCodeScannerActivity.startForResult(this) } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt index c7fdf77123..3c3009ed01 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt @@ -39,7 +39,9 @@ data class VerificationChooseMethodViewState( val otherCanShowQrCode: Boolean = false, val otherCanScanQrCode: Boolean = false, val qrCodeText: String? = null, - val SASModeAvailable: Boolean = false + val SASModeAvailable: Boolean = false, + val isMe: Boolean = false, + val canCrossSign: Boolean = false ) : MvRxState class VerificationChooseMethodViewModel @AssistedInject constructor( @@ -61,6 +63,10 @@ class VerificationChooseMethodViewModel @AssistedInject constructor( } } + override fun verificationRequestCreated(pr: PendingVerificationRequest) { + verificationRequestUpdated(pr) + } + override fun verificationRequestUpdated(pr: PendingVerificationRequest) = withState { state -> val pvr = session.cryptoService().verificationService().getExistingVerificationRequest(state.otherUserId, state.transactionId) @@ -103,6 +109,8 @@ class VerificationChooseMethodViewModel @AssistedInject constructor( val qrCodeVerificationTransaction = verificationService.getExistingTransaction(args.otherUserId, args.verificationId ?: "") return VerificationChooseMethodViewState(otherUserId = args.otherUserId, + isMe = session.myUserId == pvr?.otherUserId, + canCrossSign = session.cryptoService().crossSigningService().canCrossSign(), transactionId = args.verificationId ?: "", otherCanShowQrCode = pvr?.otherCanShowQrCode().orFalse(), otherCanScanQrCode = pvr?.otherCanScanQrCode().orFalse(), diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/conclusion/VerificationConclusionController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/conclusion/VerificationConclusionController.kt index 9719651bd4..bd40bec210 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/conclusion/VerificationConclusionController.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/conclusion/VerificationConclusionController.kt @@ -58,6 +58,8 @@ class VerificationConclusionController @Inject constructor( id("image") imageRes(R.drawable.ic_shield_trusted) } + + bottomDone() } ConclusionState.WARNING -> { bottomSheetVerificationNoticeItem { @@ -74,10 +76,32 @@ class VerificationConclusionController @Inject constructor( id("warning_notice") notice(eventHtmlRenderer.render(stringProvider.getString(R.string.verification_conclusion_compromised))) } - } - else -> Unit - } + bottomDone() + } + ConclusionState.CANCELLED -> { + bottomSheetVerificationNoticeItem { + id("notice_cancelled") + notice(stringProvider.getString(R.string.verify_cancelled_notice)) + } + + dividerItem { + id("sep0") + } + + bottomSheetVerificationActionItem { + id("got_it") + title(stringProvider.getString(R.string.sas_got_it)) + titleColor(colorProvider.getColor(R.color.riotx_accent)) + iconRes(R.drawable.ic_arrow_right) + iconColor(colorProvider.getColor(R.color.riotx_accent)) + listener { listener?.onButtonTapped() } + } + } + } + } + + private fun bottomDone() { dividerItem { id("sep0") } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/conclusion/VerificationConclusionFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/conclusion/VerificationConclusionFragment.kt index 854809084e..7405722c04 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/conclusion/VerificationConclusionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/conclusion/VerificationConclusionFragment.kt @@ -66,12 +66,7 @@ class VerificationConclusionFragment @Inject constructor( } override fun invalidate() = withState(viewModel) { state -> - if (state.conclusionState == ConclusionState.CANCELLED) { - // Just dismiss in this case - sharedViewModel.handle(VerificationAction.GotItConclusion) - } else { - controller.update(state) - } + controller.update(state) } override fun onButtonTapped() { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/epoxy/BottomSheetVerificationEmojisItem.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/epoxy/BottomSheetVerificationEmojisItem.kt index 7d72486ccd..be14c7df3d 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/epoxy/BottomSheetVerificationEmojisItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/epoxy/BottomSheetVerificationEmojisItem.kt @@ -16,14 +16,21 @@ */ package im.vector.riotx.features.crypto.verification.epoxy +import android.content.Context import android.view.ViewGroup +import android.widget.ImageView import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.matrix.android.api.session.crypto.verification.EmojiRepresentation import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel +import me.gujun.android.span.Span +import me.gujun.android.span.image +import me.gujun.android.span.span /** * A emoji list for bottom sheet. @@ -40,26 +47,36 @@ abstract class BottomSheetVerificationEmojisItem : VectorEpoxyModel(R.id.item_emoji_tv).text = emojiRepresentation0.emoji - holder.emoji0View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation0.nameResId) + bindEmojiView(holder.emoji0View, emojiRepresentation0) + bindEmojiView(holder.emoji1View, emojiRepresentation1) + bindEmojiView(holder.emoji2View, emojiRepresentation2) + bindEmojiView(holder.emoji3View, emojiRepresentation3) + bindEmojiView(holder.emoji4View, emojiRepresentation4) + bindEmojiView(holder.emoji5View, emojiRepresentation5) + bindEmojiView(holder.emoji6View, emojiRepresentation6) + } - holder.emoji1View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation1.emoji - holder.emoji1View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation1.nameResId) + private fun spanForRepresentation(context: Context, rep: EmojiRepresentation): Span { + return span { + if (rep.drawableRes != null) { + ContextCompat.getDrawable(context, rep.drawableRes!!)?.let { image(it) } + } else { + +rep.emoji + } + } + } - holder.emoji2View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation2.emoji - holder.emoji2View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation2.nameResId) - - holder.emoji3View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation3.emoji - holder.emoji3View.findViewById(R.id.item_emoji_name_tv)?.setText(emojiRepresentation3.nameResId) - - holder.emoji4View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation4.emoji - holder.emoji4View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation4.nameResId) - - holder.emoji5View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation5.emoji - holder.emoji5View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation5.nameResId) - - holder.emoji6View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation6.emoji - holder.emoji6View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation6.nameResId) + private fun bindEmojiView(view: ViewGroup, rep: EmojiRepresentation) { + rep.drawableRes?.let { + view.findViewById(R.id.item_emoji_tv).isVisible = false + view.findViewById(R.id.item_emoji_image).isVisible = true + view.findViewById(R.id.item_emoji_image).setImageDrawable(ContextCompat.getDrawable(view.context, it)) + } ?: kotlin.run { + view.findViewById(R.id.item_emoji_tv).isVisible = true + view.findViewById(R.id.item_emoji_image).isVisible = false + view.findViewById(R.id.item_emoji_tv).text = rep.emoji + } + view.findViewById(R.id.item_emoji_name_tv).setText(rep.nameResId) } class Holder : VectorEpoxyHolder() { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/qrconfirmation/VerificationQRWaitingController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/qrconfirmation/VerificationQRWaitingController.kt new file mode 100644 index 0000000000..2214774882 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/qrconfirmation/VerificationQRWaitingController.kt @@ -0,0 +1,60 @@ +/* + * 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.crypto.verification.qrconfirmation + +import com.airbnb.epoxy.EpoxyController +import im.vector.riotx.R +import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationBigImageItem +import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem +import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationWaitingItem +import javax.inject.Inject + +class VerificationQRWaitingController @Inject constructor( + private val stringProvider: StringProvider, + private val colorProvider: ColorProvider +) : EpoxyController() { + + private var args: VerificationQRWaitingFragment.Args? = null + + fun update(args: VerificationQRWaitingFragment.Args) { + this.args = args + requestModelBuild() + } + + override fun buildModels() { + val params = args ?: return + + bottomSheetVerificationNoticeItem { + id("notice") + apply { + notice(stringProvider.getString(R.string.qr_code_scanned_verif_waiting_notice)) + } + } + + bottomSheetVerificationBigImageItem { + id("image") + imageRes(R.drawable.ic_shield_trusted) + } + + bottomSheetVerificationWaitingItem { + id("waiting") + title(stringProvider.getString(R.string.qr_code_scanned_verif_waiting, params.otherUserName)) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/qrconfirmation/VerificationQRWaitingFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/qrconfirmation/VerificationQRWaitingFragment.kt new file mode 100644 index 0000000000..77de3997cb --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/qrconfirmation/VerificationQRWaitingFragment.kt @@ -0,0 +1,59 @@ +/* + * 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.crypto.verification.qrconfirmation + +import android.os.Bundle +import android.os.Parcelable +import android.view.View +import com.airbnb.mvrx.MvRx +import im.vector.riotx.R +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.configureWith +import im.vector.riotx.core.platform.VectorBaseFragment +import kotlinx.android.parcel.Parcelize +import kotlinx.android.synthetic.main.bottom_sheet_verification_child_fragment.* +import javax.inject.Inject + +class VerificationQRWaitingFragment @Inject constructor( + val controller: VerificationQRWaitingController +) : VectorBaseFragment() { + + @Parcelize + data class Args( + val isMe: Boolean, + val otherUserName: String + ) : Parcelable + + override fun getLayoutResId() = R.layout.bottom_sheet_verification_child_fragment + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupRecyclerView() + (arguments?.getParcelable(MvRx.KEY_ARG) as? Args)?.let { + controller.update(it) + } + } + + override fun onDestroyView() { + bottomSheetVerificationRecyclerView.cleanup() + super.onDestroyView() + } + + private fun setupRecyclerView() { + bottomSheetVerificationRecyclerView.configureWith(controller, hasFixedSize = false, disableItemAnimation = true) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherController.kt index f775ac7941..dd1d3d0f90 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherController.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherController.kt @@ -21,7 +21,9 @@ import im.vector.riotx.R import im.vector.riotx.core.epoxy.dividerItem import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.features.crypto.verification.VerificationBottomSheetViewState import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem +import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationBigImageItem import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem import javax.inject.Inject @@ -32,33 +34,37 @@ class VerificationQrScannedByOtherController @Inject constructor( var listener: Listener? = null - init { + private var viewState: VerificationBottomSheetViewState? = null + + fun update(viewState: VerificationBottomSheetViewState) { + this.viewState = viewState requestModelBuild() } override fun buildModels() { + val state = viewState ?: return + bottomSheetVerificationNoticeItem { id("notice") - notice(stringProvider.getString(R.string.qr_code_scanned_by_other_notice)) + apply { + if (state.isMe) { + notice(stringProvider.getString(R.string.qr_code_scanned_self_verif_notice)) + } else { + val name = state.otherUserMxItem?.getBestName() ?: "" + notice(stringProvider.getString(R.string.qr_code_scanned_by_other_notice, name)) + } + } + } + + bottomSheetVerificationBigImageItem { + id("image") + imageRes(R.drawable.ic_shield_trusted) } dividerItem { id("sep0") } - bottomSheetVerificationActionItem { - id("confirm") - title(stringProvider.getString(R.string.qr_code_scanned_by_other_yes)) - titleColor(colorProvider.getColor(R.color.riotx_accent)) - iconRes(R.drawable.ic_check_on) - iconColor(colorProvider.getColor(R.color.riotx_accent)) - listener { listener?.onUserConfirmsQrCodeScanned() } - } - - dividerItem { - id("sep1") - } - bottomSheetVerificationActionItem { id("deny") title(stringProvider.getString(R.string.qr_code_scanned_by_other_no)) @@ -67,6 +73,19 @@ class VerificationQrScannedByOtherController @Inject constructor( iconColor(colorProvider.getColor(R.color.vector_error_color)) listener { listener?.onUserDeniesQrCodeScanned() } } + + dividerItem { + id("sep1") + } + + bottomSheetVerificationActionItem { + id("confirm") + title(stringProvider.getString(R.string.qr_code_scanned_by_other_yes)) + titleColor(colorProvider.getColor(R.color.riotx_accent)) + iconRes(R.drawable.ic_check_on) + iconColor(colorProvider.getColor(R.color.riotx_accent)) + listener { listener?.onUserConfirmsQrCodeScanned() } + } } interface Listener { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherFragment.kt index 14d294a27a..a8a16f8006 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherFragment.kt @@ -18,6 +18,7 @@ package im.vector.riotx.features.crypto.verification.qrconfirmation import android.os.Bundle import android.view.View import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.withState import im.vector.riotx.R import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.configureWith @@ -37,10 +38,13 @@ class VerificationQrScannedByOtherFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - setupRecyclerView() } + override fun invalidate() = withState(sharedViewModel) { state -> + controller.update(state) + } + override fun onDestroyView() { bottomSheetVerificationRecyclerView.cleanup() controller.listener = null diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt index 05ed2f1799..56c76bc2b0 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt @@ -74,21 +74,17 @@ class VerificationRequestController @Inject constructor( iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) listener { listener?.onClickRecoverFromPassphrase() } } - bottomSheetVerificationActionItem { - id("skip") - title(stringProvider.getString(R.string.skip)) - titleColor(colorProvider.getColor(R.color.riotx_destructive_accent)) -// subTitle(stringProvider.getString(R.string.verification_use_passphrase)) - iconRes(R.drawable.ic_arrow_right) - iconColor(colorProvider.getColor(R.color.riotx_destructive_accent)) - listener { listener?.onClickDismiss() } - } } else { - val styledText = matrixItem.let { - stringProvider.getString(R.string.verification_request_notice, it.id) - .toSpannable() - .colorizeMatchingText(it.id, colorProvider.getColorFromAttribute(R.attr.vctr_notice_text_color)) - } + val styledText = + if (state.isMe) { + stringProvider.getString(R.string.verify_new_session_notice) + } else { + matrixItem.let { + stringProvider.getString(R.string.verification_request_notice, it.id) + .toSpannable() + .colorizeMatchingText(it.id, colorProvider.getColorFromAttribute(R.attr.vctr_notice_text_color)) + } + } bottomSheetVerificationNoticeItem { id("notice") @@ -119,18 +115,42 @@ class VerificationRequestController @Inject constructor( } is Success -> { if (!pr.invoke().isReady) { - bottomSheetVerificationWaitingItem { - id("waiting") - title(stringProvider.getString(R.string.verification_request_waiting_for, matrixItem.getBestName())) + if (state.isMe) { + bottomSheetVerificationWaitingItem { + id("waiting") + title(stringProvider.getString(R.string.verification_request_waiting)) + } + } else { + bottomSheetVerificationWaitingItem { + id("waiting") + title(stringProvider.getString(R.string.verification_request_waiting_for, matrixItem.getBestName())) + } } } } } } + + if (state.isMe && state.currentDeviceCanCrossSign) { + dividerItem { + id("sep_notMe") + } + + bottomSheetVerificationActionItem { + id("wasnote") + title(stringProvider.getString(R.string.verify_new_session_was_not_me)) + titleColor(colorProvider.getColor(R.color.riotx_destructive_accent)) + subTitle(stringProvider.getString(R.string.verify_new_session_compromized)) + iconRes(R.drawable.ic_arrow_right) + iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) + listener { listener?.onClickOnWasNotMe() } + } + } } interface Listener { fun onClickOnVerificationStart() + fun onClickOnWasNotMe() fun onClickRecoverFromPassphrase() fun onClickDismiss() } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestFragment.kt index 64000d07a1..b6c3659988 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestFragment.kt @@ -69,4 +69,8 @@ class VerificationRequestFragment @Inject constructor( override fun onClickDismiss() { viewModel.handle(VerificationAction.SkipVerification) } + + override fun onClickOnWasNotMe() { + viewModel.itWasNotMe() + } } diff --git a/vector/src/main/java/im/vector/riotx/features/disclaimer/DisclaimerDialog.kt b/vector/src/main/java/im/vector/riotx/features/disclaimer/DisclaimerDialog.kt index ac6b23a109..86b7fd9fc8 100644 --- a/vector/src/main/java/im/vector/riotx/features/disclaimer/DisclaimerDialog.kt +++ b/vector/src/main/java/im/vector/riotx/features/disclaimer/DisclaimerDialog.kt @@ -17,7 +17,7 @@ package im.vector.riotx.features.disclaimer import android.app.Activity -import android.preference.PreferenceManager +import androidx.preference.PreferenceManager import android.view.ViewGroup import android.widget.TextView import androidx.appcompat.app.AlertDialog diff --git a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt index ac4d29dd96..6d85dd8a3e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt @@ -96,8 +96,8 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active // PRIVATE API ********************************************************************************* private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest { - val resolvedUrl = activeSessionHolder.getActiveSession().contentUrlResolver() - .resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE) + val resolvedUrl = activeSessionHolder.getSafeActiveSession()?.contentUrlResolver() + ?.resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE) return glideRequest .load(resolvedUrl) diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt index dfe80de9de..e286c82532 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt @@ -29,6 +29,7 @@ import androidx.drawerlayout.widget.DrawerLayout import androidx.lifecycle.Observer import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.riotx.R @@ -39,9 +40,11 @@ import im.vector.riotx.core.extensions.replaceFragment import im.vector.riotx.core.platform.ToolbarConfigurable import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.pushers.PushersManager +import im.vector.riotx.features.crypto.recover.BootstrapBottomSheet import im.vector.riotx.features.disclaimer.showDisclaimerDialog import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.popup.PopupAlertManager +import im.vector.riotx.features.popup.VerificationVectorAlert import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.workers.signout.SignOutViewModel @@ -60,6 +63,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { @Inject lateinit var pushManager: PushersManager @Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var vectorPreferences: VectorPreferences + @Inject lateinit var popupAlertManager: PopupAlertManager private val drawerListener = object : DrawerLayout.SimpleDrawerListener() { override fun onDrawerStateChanged(newState: Int) { @@ -92,6 +96,9 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { drawerLayout.closeDrawer(GravityCompat.START) replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java) } + is HomeActivitySharedAction.PromptForSecurityBootstrap -> { + BootstrapBottomSheet().apply { isCancelable = false }.show(supportFragmentManager, "BootstrapBottomSheet") + } } } .disposeOnDestroy() @@ -100,6 +107,10 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { notificationDrawerManager.clearAllEvents() intent.removeExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION) } + if (intent.getBooleanExtra(EXTRA_ACCOUNT_CREATION, false)) { + sharedActionViewModel.post(HomeActivitySharedAction.PromptForSecurityBootstrap) + intent.removeExtra(EXTRA_ACCOUNT_CREATION) + } activeSessionHolder.getSafeActiveSession()?.getInitialSyncProgressStatus()?.observe(this, Observer { status -> if (status == null) { @@ -124,6 +135,12 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { waiting_view.isVisible = true } }) + + // Ask again if the app is relaunched + if (!sharedActionViewModel.hasDisplayedCompleteSecurityPrompt + && activeSessionHolder.getSafeActiveSession()?.hasAlreadySynced() == true) { + promptCompleteSecurityIfNeeded() + } } private fun promptCompleteSecurityIfNeeded() { @@ -149,14 +166,15 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { if (crossSigningEnabledOnAccount && myCrossSigningKeys?.isTrusted() == false) { // We need to ask sharedActionViewModel.hasDisplayedCompleteSecurityPrompt = true - PopupAlertManager.postVectorAlert( - PopupAlertManager.VectorAlert( + popupAlertManager.postVectorAlert( + VerificationVectorAlert( uid = "completeSecurity", - title = getString(R.string.new_signin), - description = getString(R.string.complete_security), + title = getString(R.string.complete_security), + description = getString(R.string.crosssigning_verify_this_session), iconId = R.drawable.ic_shield_warning ).apply { - colorInt = ContextCompat.getColor(this@HomeActivity, R.color.riotx_destructive_accent) + matrixItem = session.getUser(session.myUserId)?.toMatrixItem() + colorInt = ContextCompat.getColor(this@HomeActivity, R.color.riotx_positive_accent) contentAction = Runnable { (weakCurrentActivity?.get() as? VectorBaseActivity)?.let { it.navigator.waitSessionVerification(it) @@ -236,11 +254,13 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { companion object { private const val EXTRA_CLEAR_EXISTING_NOTIFICATION = "EXTRA_CLEAR_EXISTING_NOTIFICATION" + private const val EXTRA_ACCOUNT_CREATION = "EXTRA_ACCOUNT_CREATION" - fun newIntent(context: Context, clearNotification: Boolean = false): Intent { + fun newIntent(context: Context, clearNotification: Boolean = false, accountCreation: Boolean = false): Intent { return Intent(context, HomeActivity::class.java) .apply { putExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION, clearNotification) + putExtra(EXTRA_ACCOUNT_CREATION, accountCreation) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivitySharedAction.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivitySharedAction.kt index 493a14512d..902ea93588 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivitySharedAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivitySharedAction.kt @@ -24,4 +24,5 @@ import im.vector.riotx.core.platform.VectorSharedAction sealed class HomeActivitySharedAction : VectorSharedAction { object OpenDrawer : HomeActivitySharedAction() object OpenGroup : HomeActivitySharedAction() + object PromptForSecurityBootstrap : HomeActivitySharedAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt index 85f14e99a8..47338f6335 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt @@ -1,3 +1,4 @@ + /* * Copyright 2019 New Vector Ltd * @@ -19,8 +20,10 @@ package im.vector.riotx.features.home import android.os.Bundle import android.view.LayoutInflater import android.view.View +import androidx.core.content.ContextCompat import androidx.core.view.forEachIndexed import androidx.lifecycle.Observer +import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.bottomnavigation.BottomNavigationItemView @@ -32,11 +35,14 @@ import im.vector.riotx.R import im.vector.riotx.core.extensions.commitTransactionNow import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.platform.ToolbarConfigurable +import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.ui.views.KeysBackupBanner import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.home.room.list.RoomListParams import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView +import im.vector.riotx.features.popup.PopupAlertManager +import im.vector.riotx.features.popup.VerificationVectorAlert import im.vector.riotx.features.workers.signout.SignOutViewModel import kotlinx.android.synthetic.main.fragment_home_detail.* import timber.log.Timber @@ -48,12 +54,15 @@ private const val INDEX_ROOMS = 2 class HomeDetailFragment @Inject constructor( val homeDetailViewModelFactory: HomeDetailViewModel.Factory, - private val avatarRenderer: AvatarRenderer + private val avatarRenderer: AvatarRenderer, + private val alertManager: PopupAlertManager ) : VectorBaseFragment(), KeysBackupBanner.Delegate { private val unreadCounterBadgeViews = arrayListOf() private val viewModel: HomeDetailViewModel by fragmentViewModel() + private val unknownDeviceDetectorSharedViewModel : UnknownDeviceDetectorSharedViewModel by activityViewModel() + private lateinit var sharedActionViewModel: HomeSharedActionViewModel override fun getLayoutResId() = R.layout.fragment_home_detail @@ -77,6 +86,38 @@ class HomeDetailFragment @Inject constructor( viewModel.selectSubscribe(this, HomeDetailViewState::displayMode) { displayMode -> switchDisplayMode(displayMode) } + + unknownDeviceDetectorSharedViewModel.subscribe { + it.unknownSessions.invoke()?.let { unknownDevices -> + Timber.v("## Detector - ${unknownDevices.size} Unknown sessions") + unknownDevices.forEachIndexed { index, deviceInfo -> + Timber.v("## Detector - #$index deviceId:${deviceInfo.second.deviceId} lastSeenTs:${deviceInfo.second.lastSeenTs}") + } + val uid = "Newest_Device" + alertManager.cancelAlert(uid) + if (it.canCrossSign && unknownDevices.isNotEmpty()) { + val newest = unknownDevices.first().second + val user = unknownDevices.first().first + alertManager.postVectorAlert( + VerificationVectorAlert( + uid = uid, + title = getString(R.string.new_session), + description = getString(R.string.new_session_review), + iconId = R.drawable.ic_shield_warning + ).apply { + matrixItem = user + colorInt = ContextCompat.getColor(requireActivity(), R.color.riotx_accent) + contentAction = Runnable { + (weakCurrentActivity?.get() as? VectorBaseActivity) + ?.navigator + ?.requestSessionVerification(requireContext(), newest.deviceId ?: "") + } + dismissedAction = Runnable {} + } + ) + } + } + } } private fun onGroupChange(groupSummary: GroupSummary?) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt index 6b322a2b48..3824ba7922 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt @@ -22,6 +22,7 @@ import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.rx.rx import im.vector.riotx.core.di.HasScreenInjector import im.vector.riotx.core.platform.EmptyViewEvents @@ -116,10 +117,14 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho .observeOn(Schedulers.computation()) .map { it.asSequence() } .subscribe { summaries -> + val invites = summaries + .filter { it.membership == Membership.INVITE } + .count() + val peopleNotifications = summaries .filter { it.isDirect } .map { it.notificationCount } - .sumBy { i -> i } + .sum() val peopleHasHighlight = summaries .filter { it.isDirect } .any { it.highlightCount > 0 } @@ -127,14 +132,14 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho val roomsNotifications = summaries .filter { !it.isDirect } .map { it.notificationCount } - .sumBy { i -> i } + .sum() val roomsHasHighlight = summaries .filter { !it.isDirect } .any { it.highlightCount > 0 } setState { copy( - notificationCountCatchup = peopleNotifications + roomsNotifications, + notificationCountCatchup = peopleNotifications + roomsNotifications + invites, notificationHighlightCatchup = peopleHasHighlight || roomsHasHighlight, notificationCountPeople = peopleNotifications, notificationHighlightPeople = peopleHasHighlight, diff --git a/vector/src/main/java/im/vector/riotx/features/home/UnknwonDeviceDetectorSharedViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/UnknwonDeviceDetectorSharedViewModel.kt new file mode 100644 index 0000000000..180a989858 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/UnknwonDeviceDetectorSharedViewModel.kt @@ -0,0 +1,86 @@ +/* + * 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 + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.ViewModelContext +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.util.MatrixItem +import im.vector.matrix.android.api.util.NoOpCancellable +import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo +import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse +import im.vector.matrix.rx.rx +import im.vector.matrix.rx.singleBuilder +import im.vector.riotx.core.di.HasScreenInjector +import im.vector.riotx.core.platform.EmptyAction +import im.vector.riotx.core.platform.EmptyViewEvents +import im.vector.riotx.core.platform.VectorViewModel +import io.reactivex.android.schedulers.AndroidSchedulers + +data class UnknownDevicesState( + val unknownSessions: Async>> = Uninitialized, + val canCrossSign: Boolean = false +) : MvRxState + +class UnknownDeviceDetectorSharedViewModel(session: Session, initialState: UnknownDevicesState) + : VectorViewModel(initialState) { + + init { + session.rx().liveUserCryptoDevices(session.myUserId) + .observeOn(AndroidSchedulers.mainThread()) + .switchMap { deviceList -> + // Timber.v("## Detector - ============================") +// Timber.v("## Detector - Crypto device update ${deviceList.map { "${it.deviceId} : ${it.isVerified}" }}") + singleBuilder { + session.cryptoService().getDevicesList(it) + NoOpCancellable + }.map { resp -> + // Timber.v("## Detector - Device Infos ${resp.devices?.map { "${it.deviceId} : lastSeen:${it.lastSeenTs}" }}") + resp.devices?.filter { info -> + deviceList.firstOrNull { info.deviceId == it.deviceId }?.let { + !it.isVerified + } ?: false + }?.sortedByDescending { it.lastSeenTs } + ?.map { + session.getUser(it.user_id ?: "")?.toMatrixItem() to it + } ?: emptyList() + } + .toObservable() + } + .execute { async -> + copy(unknownSessions = async) + } + + session.rx().liveCrossSigningInfo(session.myUserId) + .execute { + copy(canCrossSign = session.cryptoService().crossSigningService().canCrossSign()) + } + } + + override fun handle(action: EmptyAction) {} + + companion object : MvRxViewModelFactory { + override fun create(viewModelContext: ViewModelContext, state: UnknownDevicesState): UnknownDeviceDetectorSharedViewModel? { + val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession() + return UnknownDeviceDetectorSharedViewModel(session, state) + } + } +} 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 2ef7b11b0e..3230686d58 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 @@ -71,4 +71,5 @@ 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 ReRequestKeys(val eventId: String) : 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 ad4e9694db..86f4847ff7 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 @@ -124,6 +124,7 @@ import im.vector.riotx.features.attachments.preview.AttachmentsPreviewActivity import im.vector.riotx.features.attachments.preview.AttachmentsPreviewArgs import im.vector.riotx.features.attachments.toGroupedContentAttachmentData import im.vector.riotx.features.command.Command +import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreActivity import im.vector.riotx.features.crypto.util.toImageRes import im.vector.riotx.features.crypto.verification.VerificationBottomSheet import im.vector.riotx.features.home.AvatarRenderer @@ -249,7 +250,7 @@ class RoomDetailFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java) - attachmentsHelper = AttachmentsHelper.create(this, this).register() + attachmentsHelper = AttachmentsHelper(requireContext(), this).register() keyboardStateUtils = KeyboardStateUtils(requireActivity()) setupToolbar(roomToolbar) setupRecyclerView() @@ -289,19 +290,26 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.observeViewEvents { when (it) { - is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable) - is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds) - is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it) - is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(it) - is RoomDetailViewEvents.ShowMessage -> showSnackWithMessage(it.message, Snackbar.LENGTH_LONG) - is RoomDetailViewEvents.NavigateToEvent -> navigateToEvent(it) - is RoomDetailViewEvents.FileTooBigError -> displayFileTooBigError(it) - is RoomDetailViewEvents.DownloadFileState -> handleDownloadFileState(it) - is RoomDetailViewEvents.SendMessageResult -> renderSendMessageResult(it) + is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable) + is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds) + is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it) + is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(it) + is RoomDetailViewEvents.ShowMessage -> showSnackWithMessage(it.message, Snackbar.LENGTH_LONG) + is RoomDetailViewEvents.NavigateToEvent -> navigateToEvent(it) + is RoomDetailViewEvents.FileTooBigError -> displayFileTooBigError(it) + is RoomDetailViewEvents.DownloadFileState -> handleDownloadFileState(it) + is RoomDetailViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it) + is RoomDetailViewEvents.SendMessageResult -> renderSendMessageResult(it) }.exhaustive } } + private fun handleJoinedToAnotherRoom(action: RoomDetailViewEvents.JoinRoomCommandSuccess) { + updateComposerText("") + lockSendButton = false + navigator.openRoom(vectorBaseActivity, action.roomId) + } + override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) if (savedInstanceState == null) { @@ -657,7 +665,7 @@ class RoomDetailFragment @Inject constructor( private fun sendUri(uri: Uri): Boolean { roomDetailViewModel.preventAttachmentPreview = true val shareIntent = Intent(Intent.ACTION_SEND, uri) - val isHandled = attachmentsHelper.handleShareIntent(shareIntent) + val isHandled = attachmentsHelper.handleShareIntent(requireContext(), shareIntent) if (!isHandled) { roomDetailViewModel.preventAttachmentPreview = false Toast.makeText(requireContext(), R.string.error_handling_incoming_share, Toast.LENGTH_SHORT).show() @@ -692,7 +700,7 @@ class RoomDetailFragment @Inject constructor( val isRoomEncrypted = summary?.isEncrypted ?: false if (state.tombstoneEvent == null) { composerLayout.visibility = View.VISIBLE - composerLayout.setRoomEncrypted(isRoomEncrypted) + composerLayout.setRoomEncrypted(isRoomEncrypted, state.asyncRoomSummary.invoke()?.roomEncryptionTrustLevel) notificationAreaView.render(NotificationAreaView.State.Hidden) } else { composerLayout.visibility = View.GONE @@ -774,7 +782,7 @@ class RoomDetailFragment @Inject constructor( updateComposerText("") } is RoomDetailViewEvents.SlashCommandResultError -> { - displayCommandError(sendMessageResult.throwable.localizedMessage) + displayCommandError(sendMessageResult.throwable.localizedMessage ?: getString(R.string.unexpected_error)) } is RoomDetailViewEvents.SlashCommandNotImplemented -> { displayCommandError(getString(R.string.not_implemented)) @@ -1229,6 +1237,14 @@ class RoomDetailFragment @Inject constructor( is EventSharedAction.OnUrlLongClicked -> { onUrlLongClicked(action.url) } + is EventSharedAction.ReRequestKey -> { + roomDetailViewModel.handle(RoomDetailAction.ReRequestKeys(action.eventId)) + } + is EventSharedAction.UseKeyBackup -> { + context?.let { + startActivity(KeysBackupRestoreActivity.intent(it)) + } + } else -> { Toast.makeText(context, "Action $action is not implemented yet", Toast.LENGTH_LONG).show() } @@ -1334,11 +1350,11 @@ class RoomDetailFragment @Inject constructor( private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) { when (type) { - AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera() - AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile() - AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery() - AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio() - AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact() + AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera(this) + AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile(this) + AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(this) + AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio(this) + AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact(this) AttachmentTypeSelectorView.Type.STICKER -> vectorBaseActivity.notImplemented("Adding stickers") }.exhaustive } 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 fcbe7f37c0..b24c2ea23e 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 @@ -50,6 +50,7 @@ sealed class RoomDetailViewEvents : VectorViewEvents { abstract class SendMessageResult : RoomDetailViewEvents() object MessageSent : SendMessageResult() + data class JoinRoomCommandSuccess(val roomId: String) : SendMessageResult() class SlashCommandError(val command: Command) : SendMessageResult() class SlashCommandUnknown(val command: String) : SendMessageResult() data class SlashCommandHandled(@StringRes val messageRes: Int? = null) : SendMessageResult() 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 4ab9125c4f..cef172da73 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 @@ -209,6 +209,7 @@ class RoomDetailViewModel @AssistedInject constructor( is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action) is RoomDetailAction.RequestVerification -> handleRequestVerification(action) is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action) + is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action) } } @@ -379,8 +380,8 @@ class RoomDetailViewModel @AssistedInject constructor( _viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented) } is ParsedCommand.JoinRoom -> { - // TODO - _viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented) + handleJoinToAnotherRoomSlashCommand(slashCommandResult) + popDraft() } is ParsedCommand.PartRoom -> { // TODO @@ -512,6 +513,22 @@ class RoomDetailViewModel @AssistedInject constructor( room.deleteDraft(NoOpMatrixCallback()) } + private fun handleJoinToAnotherRoomSlashCommand(command: ParsedCommand.JoinRoom) { + session.joinRoom(command.roomAlias, command.reason, object : MatrixCallback { + override fun onSuccess(data: Unit) { + session.getRoomSummary(command.roomAlias) + ?.roomId + ?.let { + _viewEvents.post(RoomDetailViewEvents.JoinRoomCommandSuccess(it)) + } + } + + override fun onFailure(failure: Throwable) { + _viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure)) + } + }) + } + private fun legacyRiotQuoteText(quotedText: String?, myText: String): String { val messageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray() return buildString { @@ -593,7 +610,7 @@ class RoomDetailViewModel @AssistedInject constructor( when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) { null -> room.sendMedias(attachments, action.compressBeforeSending, emptySet()) else -> _viewEvents.post(RoomDetailViewEvents.FileTooBigError( - tooBigFile.name ?: tooBigFile.path, + tooBigFile.name ?: tooBigFile.queryUri.toString(), tooBigFile.size, maxUploadFileSize )) @@ -870,6 +887,14 @@ class RoomDetailViewModel @AssistedInject constructor( } } + private fun handleReRequestKeys(action: RoomDetailAction.ReRequestKeys) { + // Check if this request is still active and handled by me + room.getTimeLineEvent(action.eventId)?.let { + session.cryptoService().reRequestRoomKeyForEvent(it.root) + _viewEvents.post(RoomDetailViewEvents.ShowMessage(stringProvider.getString(R.string.e2e_re_request_encryption_key_dialog_content))) + } + } + 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/composer/TextComposerView.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt index 593ce1a8f6..4391009b08 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt @@ -26,12 +26,15 @@ import android.widget.ImageView import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet +import androidx.core.content.ContextCompat import androidx.core.text.toSpannable +import androidx.core.view.isVisible import androidx.transition.AutoTransition import androidx.transition.Transition import androidx.transition.TransitionManager import butterknife.BindView import butterknife.ButterKnife +import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel import im.vector.riotx.R import kotlinx.android.synthetic.main.merge_composer_layout.view.* @@ -64,6 +67,8 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib lateinit var composerEditText: ComposerEditText @BindView(R.id.composer_avatar_view) lateinit var composerAvatarImageView: ImageView + @BindView(R.id.composer_shield) + lateinit var composerShieldImageView: ImageView private var currentConstraintSetId: Int = -1 @@ -158,12 +163,19 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib } } - fun setRoomEncrypted(isEncrypted: Boolean) { - composerEditText.setHint( - if (isEncrypted) { - R.string.room_message_placeholder_encrypted - } else { - R.string.room_message_placeholder_not_encrypted - }) + fun setRoomEncrypted(isEncrypted: Boolean, roomEncryptionTrustLevel: RoomEncryptionTrustLevel?) { + if (isEncrypted) { + composerEditText.setHint(R.string.room_message_placeholder) + composerShieldImageView.isVisible = true + val shieldRes = when (roomEncryptionTrustLevel) { + RoomEncryptionTrustLevel.Trusted -> R.drawable.ic_shield_trusted + RoomEncryptionTrustLevel.Warning -> R.drawable.ic_shield_warning + else -> R.drawable.ic_shield_black + } + composerShieldImageView.setImageDrawable(ContextCompat.getDrawable(context, shieldRes)) + } else { + composerEditText.setHint(R.string.room_message_placeholder) + composerShieldImageView.isVisible = false + } } } 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 a8c9cf679b..48e92ca438 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 @@ -47,9 +47,9 @@ import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineEventVi import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider import im.vector.riotx.features.home.room.detail.timeline.helper.nextOrNull import im.vector.riotx.features.home.room.detail.timeline.item.BaseEventItem +import im.vector.riotx.features.home.room.detail.timeline.item.BasedMergedItem import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem_ -import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.riotx.features.home.room.detail.timeline.item.TimelineReadMarkerItem_ @@ -373,7 +373,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec val localId: Long, val eventId: String?, val eventModel: EpoxyModel<*>? = null, - val mergedHeaderModel: MergedHeaderItem? = null, + val mergedHeaderModel: BasedMergedItem<*>? = null, val formattedDayModel: DaySeparatorItem? = null ) { fun shouldTriggerBuild(): Boolean { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/EventSharedAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/EventSharedAction.kt index b9e2ab2093..e38c055d52 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/EventSharedAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/EventSharedAction.kt @@ -102,4 +102,10 @@ sealed class EventSharedAction(@StringRes val titleRes: Int, // An url in the event preview has been long clicked data class OnUrlLongClicked(val url: String) : EventSharedAction(0, 0) + + data class ReRequestKey(val eventId: String) : + EventSharedAction(R.string.e2e_re_request_encryption_key, R.drawable.key_small) + + object UseKeyBackup : + EventSharedAction(R.string.e2e_use_keybackup, R.drawable.shield) } 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 1fe1db27d7..5212e1469d 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 @@ -25,6 +25,7 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import dagger.Lazy import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.isTextMessage import im.vector.matrix.android.api.session.events.model.toModel @@ -209,6 +210,31 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } ?: "" } + private fun getRedactionReason(timelineEvent: TimelineEvent): String { + return (timelineEvent + .root + .unsignedData + ?.redactedEvent + ?.content + ?.get("reason") as? String) + ?.takeIf { it.isNotBlank() } + .let { reason -> + if (reason == null) { + if (timelineEvent.root.isRedactedBySameUser()) { + stringProvider.getString(R.string.event_redacted_by_user_reason) + } else { + stringProvider.getString(R.string.event_redacted_by_admin_reason) + } + } else { + if (timelineEvent.root.isRedactedBySameUser()) { + stringProvider.getString(R.string.event_redacted_by_user_reason_with_reason, reason) + } else { + stringProvider.getString(R.string.event_redacted_by_admin_reason_with_reason, reason) + } + } + } + } + private fun actionsForEvent(timelineEvent: TimelineEvent): List { val messageContent: MessageContent? = timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() ?: timelineEvent.root.getClearContent().toModel() @@ -272,8 +298,21 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } if (vectorPreferences.developerMode()) { + if (timelineEvent.isEncrypted() && timelineEvent.root.mCryptoError != null) { + val keysBackupService = session.cryptoService().keysBackupService() + if (keysBackupService.state == KeysBackupState.NotTrusted + || (keysBackupService.state == KeysBackupState.ReadyToBackUp + && keysBackupService.canRestoreKeys()) + ) { + add(EventSharedAction.UseKeyBackup) + } + if (session.cryptoService().getCryptoDeviceInfo(session.myUserId).size > 1) { + add(EventSharedAction.ReRequestKey(timelineEvent.eventId)) + } + } + add(EventSharedAction.ViewSource(timelineEvent.root.toContentStringWithIndent())) - if (timelineEvent.isEncrypted()) { + if (timelineEvent.isEncrypted() && timelineEvent.root.mxDecryptionResult != null) { val decryptedContent = timelineEvent.root.toClearContentStringWithIndent() ?: stringProvider.getString(R.string.encryption_information_decryption_error) add(EventSharedAction.ViewDecryptedSource(decryptedContent)) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt new file mode 100644 index 0000000000..ff65b0e656 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt @@ -0,0 +1,80 @@ +/* + * 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.factory + +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.MXCRYPTO_ALGORITHM_MEGOLM +import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent +import im.vector.riotx.R +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.features.home.room.detail.timeline.MessageColorProvider +import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController +import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider +import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory +import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory +import im.vector.riotx.features.home.room.detail.timeline.item.StatusTileTimelineItem +import im.vector.riotx.features.home.room.detail.timeline.item.StatusTileTimelineItem_ +import javax.inject.Inject + +class EncryptionItemFactory @Inject constructor( + private val messageItemAttributesFactory: MessageItemAttributesFactory, + private val messageColorProvider: MessageColorProvider, + private val stringProvider: StringProvider, + private val informationDataFactory: MessageInformationDataFactory, + private val avatarSizeProvider: AvatarSizeProvider) { + + fun create(event: TimelineEvent, + highlight: Boolean, + callback: TimelineEventController.Callback?): StatusTileTimelineItem? { + val algorithm = event.root.getClearContent().toModel()?.algorithm + val informationData = informationDataFactory.create(event, null) + val attributes = messageItemAttributesFactory.create(null, informationData, callback) + + val isSafeAlgorithm = algorithm == MXCRYPTO_ALGORITHM_MEGOLM + val title: String + val description: String + val shield: StatusTileTimelineItem.ShieldUIState + if (isSafeAlgorithm) { + title = stringProvider.getString(R.string.encryption_enabled) + description = stringProvider.getString(R.string.encryption_enabled_tile_description) + shield = StatusTileTimelineItem.ShieldUIState.BLACK + } else { + title = stringProvider.getString(R.string.encryption_not_enabled) + description = stringProvider.getString(R.string.encryption_unknown_algorithm_tile_description) + shield = StatusTileTimelineItem.ShieldUIState.RED + } + return StatusTileTimelineItem_() + .attributes( + StatusTileTimelineItem.Attributes( + title = title, + description = description, + shieldUIState = shield, + informationData = informationData, + avatarRenderer = attributes.avatarRenderer, + messageColorProvider = messageColorProvider, + emojiTypeFace = attributes.emojiTypeFace, + itemClickListener = attributes.itemClickListener, + itemLongClickListener = attributes.itemLongClickListener, + reactionPillCallback = attributes.reactionPillCallback, + readReceiptsCallback = attributes.readReceiptsCallback + ) + ) + .highlighted(highlight) + .leftGuideline(avatarSizeProvider.leftGuideline) + } +} 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 42dc4e07eb..377fc5ab4a 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,16 +16,24 @@ package im.vector.riotx.features.home.room.detail.timeline.factory +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.MXCRYPTO_ALGORITHM_MEGOLM +import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.riotx.features.home.room.detail.timeline.helper.MergedTimelineEventVisibilityStateChangedListener import im.vector.riotx.features.home.room.detail.timeline.helper.canBeMerged +import im.vector.riotx.features.home.room.detail.timeline.helper.isRoomConfiguration import im.vector.riotx.features.home.room.detail.timeline.helper.prevSameTypeEvents -import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem -import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem_ +import im.vector.riotx.features.home.room.detail.timeline.item.BasedMergedItem +import im.vector.riotx.features.home.room.detail.timeline.item.MergedMembershipEventsItem +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 javax.inject.Inject class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: ActiveSessionHolder, @@ -43,8 +51,12 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act eventIdToHighlight: String?, callback: TimelineEventController.Callback?, requestModelBuild: () -> Unit) - : MergedHeaderItem? { - return if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) { + : BasedMergedItem<*>? { + return if (nextEvent?.root?.getClearType() == EventType.STATE_ROOM_CREATE && event.isRoomConfiguration()) { + // It's the first item before room.create + // Collapse all room configuration events + buildRoomCreationMergedSummary(currentPosition, items, event, eventIdToHighlight, requestModelBuild, callback) + } else if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) { null } else { val prevSameTypeEvents = items.prevSameTypeEvents(currentPosition, 2) @@ -53,14 +65,14 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act } else { var highlighted = false val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed() - val mergedData = ArrayList(mergedEvents.size) + val mergedData = ArrayList(mergedEvents.size) mergedEvents.forEach { mergedEvent -> if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) { highlighted = true } val senderAvatar = mergedEvent.senderAvatar val senderName = mergedEvent.getDisambiguatedDisplayName() - val data = MergedHeaderItem.Data( + val data = BasedMergedItem.Data( userId = mergedEvent.root.senderId ?: "", avatarUrl = senderAvatar, memberName = senderName, @@ -82,7 +94,7 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act collapsedEventIds.removeAll(mergedEventIds) } val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() } - val attributes = MergedHeaderItem.Attributes( + val attributes = MergedMembershipEventsItem.Attributes( isCollapsed = isCollapsed, mergeData = mergedData, avatarRenderer = avatarRenderer, @@ -92,7 +104,7 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act }, readReceiptsCallback = callback ) - MergedHeaderItem_() + MergedMembershipEventsItem_() .id(mergeId) .leftGuideline(avatarSizeProvider.leftGuideline) .highlighted(isCollapsed && highlighted) @@ -104,6 +116,81 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act } } + private fun buildRoomCreationMergedSummary(currentPosition: Int, + items: List, + event: TimelineEvent, + eventIdToHighlight: String?, + requestModelBuild: () -> Unit, + callback: TimelineEventController.Callback?): MergedRoomCreationItem_? { + var prevEvent = if (currentPosition > 0) items[currentPosition - 1] else null + var tmpPos = currentPosition - 1 + val mergedEvents = ArrayList().also { it.add(event) } + var hasEncryption = false + var encryptionAlgorithm: String? = null + while (prevEvent != null && prevEvent.isRoomConfiguration()) { + if (prevEvent.root.getClearType() == EventType.STATE_ROOM_ENCRYPTION) { + hasEncryption = true + encryptionAlgorithm = prevEvent.root.getClearContent()?.toModel()?.algorithm + } + mergedEvents.add(prevEvent) + tmpPos-- + prevEvent = if (tmpPos >= 0) items[tmpPos] else null + } + return if (mergedEvents.size > 2) { + 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 = MergedRoomCreationItem.Attributes( + isCollapsed = isCollapsed, + mergeData = mergedData, + avatarRenderer = avatarRenderer, + onCollapsedStateChanged = { + mergeItemCollapseStates[event.localId] = it + requestModelBuild() + }, + hasEncryptionEvent = hasEncryption, + isEncryptionAlgorithmSecure = encryptionAlgorithm == MXCRYPTO_ALGORITHM_MEGOLM, + readReceiptsCallback = callback + ) + MergedRoomCreationItem_() + .id(mergeId) + .leftGuideline(avatarSizeProvider.leftGuideline) + .highlighted(isCollapsed && highlighted) + .attributes(attributes) + .also { + it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents)) + } + } else null + } + fun isCollapsed(localId: Long): Boolean { return collapsedEventIds.contains(localId) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 2b221b9fef..f34a578832 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -440,11 +440,11 @@ class MessageItemFactory @Inject constructor( Spanned.SPAN_INCLUSIVE_EXCLUSIVE) spannable.setSpan(object : ClickableSpan() { - override fun onClick(widget: View?) { + override fun onClick(widget: View) { callback?.onEditedDecorationClicked(informationData) } - override fun updateDrawState(ds: TextPaint?) { + override fun updateDrawState(ds: TextPaint) { // nop } }, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 1462f5fe0d..7e6c387934 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -29,6 +29,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me private val encryptedItemFactory: EncryptedItemFactory, private val noticeItemFactory: NoticeItemFactory, private val defaultItemFactory: DefaultItemFactory, + private val encryptionItemFactory: EncryptionItemFactory, private val roomCreateItemFactory: RoomCreateItemFactory, private val verificationConclusionItemFactory: VerificationItemFactory, private val userPreferencesProvider: UserPreferencesProvider) { @@ -57,8 +58,10 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.CALL_HANGUP, EventType.CALL_ANSWER, EventType.REACTION, - EventType.REDACTION, - EventType.STATE_ROOM_ENCRYPTION -> noticeItemFactory.create(event, highlight, callback) + EventType.REDACTION -> noticeItemFactory.create(event, highlight, callback) + EventType.STATE_ROOM_ENCRYPTION -> { + encryptionItemFactory.create(event, highlight, callback) + } // State room create EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback) // Crypto diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt index 890612c04c..837d0ad571 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt @@ -23,18 +23,19 @@ import im.vector.matrix.android.api.session.events.model.RelationType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent import im.vector.matrix.android.api.session.room.model.message.MessageVerificationCancelContent -import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.session.room.VerificationState +import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyModel +import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.resources.UserPreferencesProvider import im.vector.riotx.features.home.room.detail.timeline.MessageColorProvider import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory -import im.vector.riotx.features.home.room.detail.timeline.item.VerificationRequestConclusionItem -import im.vector.riotx.features.home.room.detail.timeline.item.VerificationRequestConclusionItem_ +import im.vector.riotx.features.home.room.detail.timeline.item.StatusTileTimelineItem +import im.vector.riotx.features.home.room.detail.timeline.item.StatusTileTimelineItem_ import javax.inject.Inject /** @@ -49,6 +50,7 @@ class VerificationItemFactory @Inject constructor( private val avatarSizeProvider: AvatarSizeProvider, private val noticeItemFactory: NoticeItemFactory, private val userPreferencesProvider: UserPreferencesProvider, + private val stringProvider: StringProvider, private val session: Session ) { @@ -71,7 +73,7 @@ class VerificationItemFactory @Inject constructor( ?: return ignoredConclusion(event, highlight, callback) // If it's not a request ignore this event - if (refEvent.root.getClearContent().toModel() == null) return ignoredConclusion(event, highlight, callback) + // if (refEvent.root.getClearContent().toModel() == null) return ignoredConclusion(event, highlight, callback) val referenceInformationData = messageInformationDataFactory.create(refEvent, null) @@ -89,12 +91,12 @@ class VerificationItemFactory @Inject constructor( CancelCode.MismatchedKeys, CancelCode.MismatchedSas -> { // We should display these bad conclusions - return VerificationRequestConclusionItem_() + return StatusTileTimelineItem_() .attributes( - VerificationRequestConclusionItem.Attributes( - toUserId = informationData.senderId, - toUserName = informationData.memberName.toString(), - isPositive = false, + StatusTileTimelineItem.Attributes( + title = stringProvider.getString(R.string.verification_conclusion_warning), + description = "${informationData.memberName} (${informationData.senderId})", + shieldUIState = StatusTileTimelineItem.ShieldUIState.RED, informationData = informationData, avatarRenderer = attributes.avatarRenderer, messageColorProvider = messageColorProvider, @@ -122,12 +124,12 @@ class VerificationItemFactory @Inject constructor( // We only display the done sent by the other user, the done send by me is ignored return ignoredConclusion(event, highlight, callback) } - return VerificationRequestConclusionItem_() + return StatusTileTimelineItem_() .attributes( - VerificationRequestConclusionItem.Attributes( - toUserId = informationData.senderId, - toUserName = informationData.memberName.toString(), - isPositive = true, + StatusTileTimelineItem.Attributes( + title = stringProvider.getString(R.string.sas_verified), + description = "${informationData.memberName} (${informationData.senderId})", + shieldUIState = StatusTileTimelineItem.ShieldUIState.GREEN, informationData = informationData, avatarRenderer = attributes.avatarRenderer, messageColorProvider = messageColorProvider, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 2a7261665a..0758e34495 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -22,7 +22,9 @@ import im.vector.matrix.android.api.session.Session 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.ReferencesAggregatedContent +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited import im.vector.matrix.android.internal.session.room.VerificationState import im.vector.riotx.core.date.VectorDateFormatter @@ -61,6 +63,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses || event.getDisambiguatedDisplayName() != nextEvent?.getDisambiguatedDisplayName() || (nextEvent.root.getClearType() != EventType.MESSAGE && nextEvent.root.getClearType() != EventType.ENCRYPTED) || isNextMessageReceivedMoreThanOneHourAgo + || isTileTypeMessage(nextEvent) val time = dateFormatter.formatMessageHour(date) val avatarUrl = event.senderAvatar @@ -88,7 +91,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses myVote = it.aggregatedContent?.myVote, isClosed = it.closedTime ?: Long.MAX_VALUE > System.currentTimeMillis(), votes = it.aggregatedContent?.votes - ?.groupBy({ it.optionIndex }, { it.userId }) + ?.groupBy({ it.optionIndex }, { it.userId }) ?.mapValues { it.value.size } ) }, @@ -111,4 +114,19 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses sentByMe = event.root.senderId == session.myUserId ) } + + /** + * Tiles type message never show the sender information (like verification request), so we should repeat it for next message + * even if same sender + */ + private fun isTileTypeMessage(event: TimelineEvent?): Boolean { + return when (event?.root?.getClearType()) { + EventType.KEY_VERIFICATION_DONE, + EventType.KEY_VERIFICATION_CANCEL -> true + EventType.MESSAGE -> { + event.getLastMessageContent() is MessageVerificationRequestContent + } + else -> false + } + } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index 5c763cb114..1ea3cd64ac 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -50,6 +50,18 @@ fun TimelineEvent.canBeMerged(): Boolean { return root.getClearType() == EventType.STATE_ROOM_MEMBER } +fun TimelineEvent.isRoomConfiguration(): Boolean { + return when (root.getClearType()) { + EventType.STATE_ROOM_GUEST_ACCESS, + EventType.STATE_ROOM_HISTORY_VISIBILITY, + EventType.STATE_ROOM_JOIN_RULES, + EventType.STATE_ROOM_MEMBER, + EventType.STATE_ROOM_NAME, + EventType.STATE_ROOM_ENCRYPTION -> true + else -> false + } +} + fun List.nextSameTypeEvents(index: Int, minSize: Int): List { if (index >= size - 1) { return emptyList() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BasedMergedItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BasedMergedItem.kt new file mode 100644 index 0000000000..adc9b1442f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BasedMergedItem.kt @@ -0,0 +1,82 @@ +/* + * 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.View +import android.widget.TextView +import androidx.annotation.IdRes +import androidx.core.view.isVisible +import im.vector.matrix.android.api.util.MatrixItem +import im.vector.riotx.R +import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController + +abstract class BasedMergedItem : BaseEventItem() { + + abstract val attributes: Attributes + + override fun bind(holder: H) { + super.bind(holder) + holder.expandView.setOnClickListener { + attributes.onCollapsedStateChanged(!attributes.isCollapsed) + } + if (attributes.isCollapsed) { + holder.separatorView.visibility = View.GONE + holder.expandView.setText(R.string.merged_events_expand) + } else { + holder.separatorView.visibility = View.VISIBLE + holder.expandView.setText(R.string.merged_events_collapse) + } + // No read receipt for this item + holder.readReceiptsView.isVisible = false + } + + protected val distinctMergeData by lazy { + attributes.mergeData.distinctBy { it.userId } + } + + override fun getEventIds(): List { + return if (attributes.isCollapsed) { + attributes.mergeData.map { it.eventId } + } else { + emptyList() + } + } + + data class Data( + val localId: Long, + val eventId: String, + val userId: String, + val memberName: String, + val avatarUrl: String? + ) + + fun Data.toMatrixItem() = MatrixItem.UserItem(userId, memberName, avatarUrl) + + interface Attributes { + val isCollapsed: Boolean + val mergeData: List + val avatarRenderer: AvatarRenderer + val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? + val onCollapsedStateChanged: (Boolean) -> Unit + } + + abstract class Holder(@IdRes stubId: Int) : BaseEventItem.BaseHolder(stubId) { + val expandView by bind(R.id.itemMergedExpandTextView) + val separatorView by bind(R.id.itemMergedSeparatorView) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedMembershipEventsItem.kt similarity index 61% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedMembershipEventsItem.kt index 93f7dc271d..8e3ba0bcff 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedMembershipEventsItem.kt @@ -24,28 +24,20 @@ import androidx.core.view.children import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import im.vector.matrix.android.api.util.MatrixItem 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 MergedHeaderItem : BaseEventItem() { - - @EpoxyAttribute - lateinit var attributes: Attributes - - private val distinctMergeData by lazy { - attributes.mergeData.distinctBy { it.userId } - } +abstract class MergedMembershipEventsItem : BasedMergedItem() { override fun getViewType() = STUB_ID + @EpoxyAttribute + override lateinit var attributes: Attributes + override fun bind(holder: Holder) { super.bind(holder) - holder.expandView.setOnClickListener { - attributes.onCollapsedStateChanged(!attributes.isCollapsed) - } if (attributes.isCollapsed) { val summary = holder.expandView.resources.getQuantityString(R.plurals.membership_changes, attributes.mergeData.size, attributes.mergeData.size) holder.summaryView.text = summary @@ -60,52 +52,28 @@ abstract class MergedHeaderItem : BaseEventItem() { view.visibility = View.GONE } } - holder.separatorView.visibility = View.GONE - holder.expandView.setText(R.string.merged_events_expand) } else { holder.avatarListView.visibility = View.INVISIBLE holder.summaryView.visibility = View.GONE - holder.separatorView.visibility = View.VISIBLE - holder.expandView.setText(R.string.merged_events_collapse) } // No read receipt for this item holder.readReceiptsView.isVisible = false } - override fun getEventIds(): List { - return if (attributes.isCollapsed) { - attributes.mergeData.map { it.eventId } - } else { - emptyList() - } - } - - data class Data( - val localId: Long, - val eventId: String, - val userId: String, - val memberName: String, - val avatarUrl: String? - ) - - fun Data.toMatrixItem() = MatrixItem.UserItem(userId, memberName, avatarUrl) - - data class Attributes( - val isCollapsed: Boolean, - val mergeData: List, - val avatarRenderer: AvatarRenderer, - val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null, - val onCollapsedStateChanged: (Boolean) -> Unit - ) - - class Holder : BaseHolder(STUB_ID) { - val expandView by bind(R.id.itemMergedExpandTextView) + class Holder : BasedMergedItem.Holder(STUB_ID) { val summaryView by bind(R.id.itemMergedSummaryTextView) - val separatorView by bind(R.id.itemMergedSeparatorView) val avatarListView by bind(R.id.itemMergedAvatarListView) } companion object { private const val STUB_ID = R.id.messageContentMergedHeaderStub } + + 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/home/room/detail/timeline/item/MergedRoomCreationItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt new file mode 100644 index 0000000000..81050194a8 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt @@ -0,0 +1,115 @@ +/* + * 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.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.RelativeLayout +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.core.view.isGone +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 MergedRoomCreationItem : BasedMergedItem() { + + @EpoxyAttribute + override lateinit var attributes: Attributes + + override fun getViewType() = STUB_ID + + override fun bind(holder: Holder) { + super.bind(holder) + + if (attributes.isCollapsed) { + val data = distinctMergeData.firstOrNull() + + 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 encryptionTile by bind(R.id.creationEncryptionTile) + + val e2eTitleTextView by bind(R.id.itemVerificationDoneTitleTextView) + val e2eTitleDescriptionView by bind(R.id.itemVerificationDoneDetailTextView) + } + + companion object { + private const val STUB_ID = R.id.messageContentMergedCreationStub + } + + 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, + val hasEncryptionEvent : Boolean, + val isEncryptionAlgorithmSecure: Boolean + ) : BasedMergedItem.Attributes +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestConclusionItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/StatusTileTimelineItem.kt similarity index 81% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestConclusionItem.kt rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/StatusTileTimelineItem.kt index 2b28e15cab..f9ea2a71df 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestConclusionItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/StatusTileTimelineItem.kt @@ -31,7 +31,7 @@ import im.vector.riotx.features.home.room.detail.timeline.MessageColorProvider import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController @EpoxyModelClass(layout = R.layout.item_timeline_event_base_state) -abstract class VerificationRequestConclusionItem : AbsBaseMessageItem() { +abstract class StatusTileTimelineItem : AbsBaseMessageItem() { override val baseAttributes: AbsBaseMessageItem.Attributes get() = attributes @@ -47,11 +47,17 @@ abstract class VerificationRequestConclusionItem : AbsBaseMessageItem { this.marginEnd = leftGuideline } - val title = if (attributes.isPositive) R.string.sas_verified else R.string.verification_conclusion_warning - holder.titleView.text = holder.view.context.getString(title) - holder.descriptionView.text = "${attributes.informationData.memberName} (${attributes.informationData.senderId})" - val startDrawable = if (attributes.isPositive) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning + holder.titleView.text = attributes.title + holder.descriptionView.text = attributes.description + holder.descriptionView.textAlignment = View.TEXT_ALIGNMENT_CENTER + + val startDrawable = when (attributes.shieldUIState) { + ShieldUIState.GREEN -> R.drawable.ic_shield_trusted + ShieldUIState.BLACK -> R.drawable.ic_shield_black + ShieldUIState.RED -> R.drawable.ic_shield_warning + } + holder.titleView.setCompoundDrawablesWithIntrinsicBounds( ContextCompat.getDrawable(holder.view.context, startDrawable), null, null, null @@ -75,9 +81,9 @@ abstract class VerificationRequestConclusionItem : AbsBaseMessageItem(initialState) { @AssistedInject.Factory @@ -240,6 +242,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi private fun handleRegisterWith(action: LoginAction.LoginOrRegister) { setState { copy(asyncRegistration = Loading()) } + reAuthHelper.rememberAuth(UserPasswordAuth(user = action.username, password = action.password)) currentTask = registrationWizard?.createAccount( action.username, action.password, diff --git a/vector/src/main/java/im/vector/riotx/features/login/ReAuthHelper.kt b/vector/src/main/java/im/vector/riotx/features/login/ReAuthHelper.kt new file mode 100644 index 0000000000..3a6142bc08 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/ReAuthHelper.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.riotx.features.login + +import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth +import java.util.Timer +import java.util.TimerTask +import javax.inject.Inject +import javax.inject.Singleton + +const val THREE_MINUTES = 3 * 60_000L + +@Singleton +class ReAuthHelper @Inject constructor() { + + private var timer: Timer? = null + + private var rememberedInfo: UserPasswordAuth? = null + + fun rememberAuth(password: UserPasswordAuth?) { + timer?.cancel() + timer = null + rememberedInfo = password + timer = Timer().apply { + schedule(object : TimerTask() { + override fun run() { + rememberedInfo = null + } + }, THREE_MINUTES) + } + } + + fun rememberedAuth() = rememberedInfo +} diff --git a/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt index d9da08254f..8ba21468b7 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt @@ -56,10 +56,17 @@ class ImageMediaViewerActivity : VectorBaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(im.vector.riotx.R.layout.activity_image_media_viewer) - mediaData = intent.getParcelableExtra(EXTRA_MEDIA_DATA) + + if (intent.hasExtra(EXTRA_MEDIA_DATA)) { + mediaData = intent.getParcelableExtra(EXTRA_MEDIA_DATA)!! + } else { + finish() + } + intent.extras?.getString(EXTRA_SHARED_TRANSITION_NAME)?.let { ViewCompat.setTransitionName(imageTransitionView, it) } + if (mediaData.url.isNullOrEmpty()) { supportFinishAfterTransition() return diff --git a/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt index 9c3312f3bb..e92b58c681 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt @@ -37,11 +37,14 @@ class VideoMediaViewerActivity : VectorBaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(im.vector.riotx.R.layout.activity_video_media_viewer) - val mediaData = intent.getParcelableExtra(EXTRA_MEDIA_DATA) - configureToolbar(videoMediaViewerToolbar, mediaData) - imageContentRenderer.render(mediaData.thumbnailMediaData, ImageContentRenderer.Mode.FULL_SIZE, videoMediaViewerThumbnailView) - videoContentRenderer.render(mediaData, videoMediaViewerThumbnailView, videoMediaViewerLoading, videoMediaViewerVideoView, videoMediaViewerErrorView) + if (intent.hasExtra(EXTRA_MEDIA_DATA)) { + val mediaData = intent.getParcelableExtra(EXTRA_MEDIA_DATA)!! + + configureToolbar(videoMediaViewerToolbar, mediaData) + imageContentRenderer.render(mediaData.thumbnailMediaData, ImageContentRenderer.Mode.FULL_SIZE, videoMediaViewerThumbnailView) + videoContentRenderer.render(mediaData, videoMediaViewerThumbnailView, videoMediaViewerLoading, videoMediaViewerVideoView, videoMediaViewerErrorView) + } } private fun configureToolbar(toolbar: Toolbar, mediaData: VideoContentRenderer.Data) { diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index a080cabf1b..2e91090ec4 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -83,12 +83,12 @@ class DefaultNavigator @Inject constructor( } } - override fun requestSessionVerification(context: Context) { + override fun requestSessionVerification(context: Context, otherSessionId: String) { val session = sessionHolder.getSafeActiveSession() ?: return val pr = session.cryptoService().verificationService().requestKeyVerification( supportedVerificationMethodsProvider.provide(), session.myUserId, - session.cryptoService().getUserDevices(session.myUserId).map { it.deviceId } + listOf(otherSessionId) ) if (context is VectorBaseActivity) { VerificationBottomSheet.withArgs( diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt index fcb3d7bb44..65ef08dd05 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt @@ -30,7 +30,7 @@ interface Navigator { fun performDeviceVerification(context: Context, otherUserId: String, sasTransactionId: String) - fun requestSessionVerification(context: Context) + fun requestSessionVerification(context: Context, otherSessionId: String) fun waitSessionVerification(context: Context) diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt index 53d69bce2d..04b267fb3a 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt @@ -97,7 +97,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { val message = getReplyMessage(intent) val roomId = intent.getStringExtra(KEY_ROOM_ID) - if (message.isNullOrBlank() || roomId.isBlank()) { + if (message.isNullOrBlank() || roomId.isNullOrBlank()) { // ignore this event // Can this happen? should we update notification? return diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt index 73297e9add..2984cc3889 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt @@ -187,7 +187,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context fun refreshNotificationDrawer() { // Implement last throttler - Timber.w("refreshNotificationDrawer()") + Timber.v("refreshNotificationDrawer()") backgroundHandler.removeCallbacksAndMessages(null) backgroundHandler.postDelayed( { @@ -197,7 +197,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context @WorkerThread private fun refreshNotificationDrawerBg() { - Timber.w("refreshNotificationDrawerBg()") + Timber.v("refreshNotificationDrawerBg()") val session = activeSessionHolder.getSafeActiveSession() ?: return diff --git a/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt b/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt index aa198eba02..1876d83617 100644 --- a/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt +++ b/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt @@ -20,20 +20,23 @@ import android.os.Build import android.os.Handler import android.os.Looper import android.view.View -import androidx.annotation.ColorInt -import androidx.annotation.ColorRes -import androidx.annotation.DrawableRes +import android.widget.ImageView import com.tapadoo.alerter.Alerter import com.tapadoo.alerter.OnHideAlertListener +import dagger.Lazy import im.vector.riotx.R +import im.vector.riotx.features.home.AvatarRenderer import timber.log.Timber import java.lang.ref.WeakReference +import javax.inject.Inject +import javax.inject.Singleton /** * Responsible of displaying important popup alerts on top of the screen. * Alerts are stacked and will be displayed sequentially */ -object PopupAlertManager { +@Singleton +class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy) { private var weakCurrentActivity: WeakReference? = null private var currentAlerter: VectorAlert? = null @@ -160,9 +163,19 @@ object PopupAlertManager { clearLightStatusBar() alert.weakCurrentActivity = WeakReference(activity) - Alerter.create(activity) - .setTitle(alert.title) + val alerter = if (alert is VerificationVectorAlert) Alerter.create(activity, R.layout.alerter_verification_layout) + else Alerter.create(activity) + + alerter.setTitle(alert.title) .setText(alert.description) + .also { al -> + if (alert is VerificationVectorAlert) { + val tvCustomView = al.getLayoutContainer() + tvCustomView?.findViewById(R.id.ivUserAvatar)?.let { imageView -> + alert.matrixItem?.let { avatarRenderer.get().render(it, imageView) } + } + } + } .apply { if (!animate) { setEnterAnimation(R.anim.anim_alerter_no_anim) @@ -226,37 +239,4 @@ object PopupAlertManager { displayNextIfPossible() }, 500) } - - /** - * Dataclass to describe an important alert with actions. - */ - data class VectorAlert(val uid: String, - val title: String, - val description: String, - @DrawableRes val iconId: Int?, - val shouldBeDisplayedIn: ((Activity) -> Boolean)? = null) { - - data class Button(val title: String, val action: Runnable, val autoClose: Boolean) - - // will be set by manager, and accessible by actions at runtime - var weakCurrentActivity: WeakReference? = null - - val actions = ArrayList. - Verwenden Sie die alte Überprüfung. + Es ist nichts aufgetaucht\? Noch nicht alle Clients unterstützen die interaktive Verifikation. Nutze die alte Verifikation.. + Verwende die alte Verifizierungsmethode. - Das Gerät kennt diese Transaktion nicht + Die Sitzung kennt diese Transaktion nicht Die Hash-Verpflichtung stimmte nicht überein Die SAS stimmte nicht überein - Wiederherstellungsschlüssel passt nicht - Der Benutzer Wiederherstellungsschlüssel passt nicht - Informieren Sie sich hier über ungelesene Nachrichten - Ihre direkte Konversation wird hier angezeigt + Schlüssel-Ungleichheit + Benutzer-Ungleichheit + Informiere dich hier über ungelesene Nachrichten + Deine direkten Konversationen werden hier angezeigt Fehlerhaftes Ereignis, kann nicht angezeigt werden Beim Abrufen der Vertrauensinformationen ist ein Fehler aufgetreten Beim Abrufen der Schlüsselsicherungsdaten ist ein Fehler aufgetreten @@ -1576,7 +1572,7 @@ Wenn du diese neue Wiederherstellungsmethode nicht eingerichtet hast, kann ein A Play Store Beschreibung Matrix SDK Version Sonstige Hinweise Dritter - Sie sehen diesen Raum bereits! + Du siehst diesen Raum bereits! Schnelle Reaktionen @@ -1595,8 +1591,8 @@ Wenn du diese neue Wiederherstellungsmethode nicht eingerichtet hast, kann ein A Token registrieren Mache einen Vorschlag - Bitte schreiben Sie Ihren Vorschlag unten. - Beschreiben Sie hier Ihren Vorschlag + Bitte schreibe unten deine Anmerkungen. + Beschreibe hier deine Anmerkung Versteckte Ereignisse in der Zeitleiste anzeigen Direkte Nachrichten @@ -1610,22 +1606,22 @@ Wenn du diese neue Wiederherstellungsmethode nicht eingerichtet hast, kann ein A Keine Änderungen gefunden Gespräche filtern… - Senden Sie eine neue Direktnachricht + Sende eine neue Direktnachricht Das Raumverzeichnis anzeigen Link in die Zwischenablage kopiert - Nach Matrix-ID hinzufügen + Mit Matrix-ID hinzufügen Raum erstellen… Bearbeitungsverlauf anzeigen Die andere Partei hat die Überprüfung abgebrochen. \n%s - Das Gerät kann sich nicht auf eine Schlüsselvereinbarung, eine Hash-, eine MAC- oder eine SAS-Methode einigen - Die neueste Featureliste befindet sich immer in %1$s. Wenn Sie Fehler finden, senden Sie uns bitte einen Bericht im Menü oben links von \"Startseite\". Wir werden den Fehler so schnell wie möglich beheben. - Wenn Sie Fehler finden, senden Sie uns bitte einen Bericht im Menü oben links von \"Startseite\". Wir werden diese so schnell wie möglich beheben. + Die Sitzung kann sich nicht auf eine Schlüsselvereinbarung, eine Hash-, eine MAC- oder eine SAS-Methode einigen + Die neueste Feature-Liste befindet sich immer in %1$s. Wenn du Fehler findest, sende uns bitte einen Bericht im Menü oben links von \"Startseite\". Wir werden den Fehler so schnell wie möglich beheben. + Wenn du Fehler findest, sende uns bitte einen Bericht im Menü oben links von der \"Startseite\". Wir werden diese so schnell wie möglich beheben. - Importieren Sie e2e-Schlüssel aus der Datei \"%1$s\". + E2E-Schlüssel aus der Datei \"%1$s\" importieren. Vielen Dank, der Vorschlag wurde erfolgreich gesendet Der Vorschlag konnte nicht gesendet werden (%s) @@ -1642,14 +1638,14 @@ Wenn du diese neue Wiederherstellungsmethode nicht eingerichtet hast, kann ein A Benutze die alte App - Können Sie nicht finden, wonach Sie suchen\? + Kannst du nicht finden, wonach du suchst\? Erstelle einen neuen Raum Name oder ID (#beispiel:matrix.org) - Aktivieren Sie das Streichen, um in der Zeitleiste zu antworten + Aktiviere Wischen, um in der Zeitleiste zu antworten - Kein Ergebnis gefunden. Verwenden Sie Nach Matrix-ID hinzufügen, um auf dem Server zu suchen. - Beginnen Sie mit der Eingabe, um Ergebnisse zu erhalten + Kein Ergebnis gefunden. Verwende \'Mit Matrix-ID hinzufügen\', um auf dem Server zu suchen. + Beginne mit der Eingabe, um Ergebnisse zu erhalten Filtern nach Benutzername oder ID… Raum betreten… @@ -1664,13 +1660,13 @@ Wenn du diese neue Wiederherstellungsmethode nicht eingerichtet hast, kann ein A Kein Integrationsserver konfiguriert. Anruf aufgrund eines falsch konfigurierten Servers fehlgeschlagen - Versuchen Sie es mit %s + Versuche es mit %s Nicht erneut fragen - Richten Sie eine E-Mail für die Kontowiederherstellung ein, die später von Personen, die Sie kennen, optional gefunden werden kann. - Richten Sie ein Telefon ein und lassen Sie es später optional von Personen erkennen, die Sie kennen. - Legen Sie eine E-Mail für die Kontowiederherstellung fest. Verwenden Sie eine spätere E-Mail oder ein späteres Telefon, um von Personen, die Sie kennen, optional gefunden zu werden. - Legen Sie eine E-Mail für die Kontowiederherstellung fest. Verwenden Sie eine spätere E-Mail oder ein späteres Telefon, um von Personen, die Sie kennen, optional gefunden zu werden. + Richte eine E-Mail für die Kontowiederherstellung ein. Optional, kannst du später einrichten, dass Personen dich über diese Adresse finden. + Richte eine Telefonnummer ein. Später kannst du einrichten, dass Personen dich über diese finden. + Lege eine E-Mail-Adresse für die Kontowiederherstellung fest. Später kann optional eine E-Mail-Adresse oder eine Telefonnummer dazu verwendet werden, um von anderen Personen gefunden zu werden. + Lege eine E-Mail-Adresse für die Kontowiederherstellung fest. Später kann optional eine E-Mail-Adresse oder eine Telefonnummer dazu verwendet werden, um von anderen Personen gefunden zu werden. Fallback-Call-Assist-Server zulassen Optimiert für die Batterie Optimiert für die Echtzeit @@ -1680,14 +1676,14 @@ Wenn du diese neue Wiederherstellungsmethode nicht eingerichtet hast, kann ein A Bevorzugtes Synchronisationsintervall Fund - Öffentlicher Name (sichtbar für Personen, mit denen Sie kommunizieren) - Der öffentliche Name eines Geräts ist für Personen sichtbar, mit denen Sie kommunizieren - Um fortzufahren, müssen Sie die Bedingungen dieses Dienstes akzeptieren. + Öffentlicher Name (sichtbar für Personen, mit denen du kommunizierst) + Der öffentliche Name der Sitzung ist für Personen sichtbar, mit denen du kommunizierst + Um fortzufahren, musst du die Nutzungsbedingungen akzeptieren. - Sie verwenden keinen Identity Server - Es ist kein Identitätsserver konfiguriert. Sie müssen Ihr Kennwort zurücksetzen. + Du verwendest keinen Identitätsserver + Es ist kein Identitätsserver konfiguriert. Du musst dein Kennwort zurücksetzen. - Sie versuchen anscheinend, eine Verbindung zu einem anderen Homeserver herzustellen. Möchten Sie sich abmelden\? + Du versuchst anscheinend, eine Verbindung zu einem anderen Heimserver herzustellen. Möchtest du dich abmelden\? Push-Key: App-Anzeigename: @@ -1695,26 +1691,26 @@ Wenn du diese neue Wiederherstellungsmethode nicht eingerichtet hast, kann ein A Nutzungsbedingungen Nutzungsbedingungen überprüfen Für andere auffindbar sein - Verwenden Sie Bots, Bridges, Widgets und Sticker-Packs + Verwende Bots, Bridges, Widgets und Sticker-Pakete Gelesen von Identitätsserver - Trennen Sie den Identitätsserver - Konfigurieren Sie den Identitätsserver + Verbindung zum Identitätsserver trennen + Identitätsserver konfigurieren Identitätsserver ändern Erkennbare E-Mail-Adressen - Erkennungsoptionen werden angezeigt, sobald Sie eine E-Mail hinzugefügt haben. + Erkennungsoptionen werden angezeigt, sobald du eine E-Mail hinzugefügt hast. ausstehend Gib einen neuen Identitätsserver ein Konnte keine Verbindung zum Heimserver herstellen Latn - Bitte frage den Administrator deines Home-Servers (%1$s) um einen TURN server einzurichten, damit Anrufe zuverlässig funktionieren. -\n -\nAlternativ kannst du einen öffentlichen Server auf %2$s nutzen doch wird das nicht zu zuverlässig sein und es wird deine IP-Adresse mit dem Server teilen. Du kannst dies auch in den Einstellungen konfigurieren. + Bitte frage die Administration deines Heimservers (%1$s) um einen TURN-Server einzurichten, damit Anrufe zuverlässig funktionieren. +\n +\nAlternativ kannst du einen öffentlichen Server auf %2$s nutzen, doch wird das nicht zu zuverlässig sein und es wird deine IP-Adresse mit dem Server geteilt. Du kannst dies auch in den Einstellungen konfigurieren. Dies ist keine Adresse eines Matrixservers Kann Home-Server nicht bei dieser URL erreichen. Bitte überprüfen Wir nutzen %s als Assistenten wenn dein Home-Server keinen anbietet (Deine IP-Adresse wird während des Anrufs geteilt) @@ -1725,14 +1721,14 @@ Wenn du diese neue Wiederherstellungsmethode nicht eingerichtet hast, kann ein A Hintergrundsynchronisierungsmodus (experimentell) - Riot wird sich im Hintergrund auf eine Art synchronisieren die die Ressourcen des Geräts schont (Akku). + Riot wird sich im Hintergrund auf eine Art synchronisieren die Ressourcen des Geräts schont (Akku). \nAbhängig von dem Ressourcen-Statuses deines Geräts kann dein System die Synchronisierung verschieben. - Riot wird sich im Hintergrund periodisch zu einem bestimmten Zeitpunkt synchronisieren (konfigurierbar). -\nDies wird Funk- und Akkunutzung beeinflussen. Es wird wird eine permanente Benachrichtigung geben, die sagt, dass Riot auf Ereignisse lauscht. + Riot wird sich im Hintergrund periodisch zu einem bestimmten Zeitpunkt synchronisieren (konfigurierbar). +\nDies wird Funk- und Akkunutzung beeinflussen. Es wird eine permanente Benachrichtigung geben, die sagt, dass Riot auf Ereignisse lauscht. %s \nDie Synchronisierung kann aufgrund deiner Ressourcen (Akku) oder Gerätezustands (schlafend) verschoben werden. Integrationen - Benutze einen Integrations-Manager um Bots, Brpcken, Widgets und Sticker-Pakete zu verwalten. + Benutze einen Integrations-Manager um Bots, Brücken, Widgets und Sticker-Pakete zu verwalten. \nIntegrations-Manager erhalten Konfigurationsdaten und können Widgets verändern, Raum-Einladungen senden und in deinem Namen Berechtigungslevel setzen. Erlaube Integrationen Widget @@ -1757,7 +1753,7 @@ Wenn du diese neue Wiederherstellungsmethode nicht eingerichtet hast, kann ein A Mikrofon benutzen Lese DRM-geschützte Medien - Frühere Versionen von Riot hatten einen Sicherheitsproblem, welches dem Identitätsserver (%1$s) Zugriff auf deinen Account geben konnte. Wenn du %2$s vertraust, kannst du dies ignorieren – ansonsten logge dich bitte aus und wieder ein. + Frühere Versionen von Riot hatten ein Sicherheitsproblem, welches dem Identitätsserver (%1$s) Zugriff auf deinen Account geben konnte. Wenn du %2$s vertraust, kannst du dies ignorieren – ansonsten logge dich bitte aus und wieder ein. \n \nWeitere Details gibt es hier (Englisch): \nhttps://medium.com/@RiotChat/36b4792ea0d6 @@ -1766,7 +1762,7 @@ Wenn du diese neue Wiederherstellungsmethode nicht eingerichtet hast, kann ein A Verwalte deine Erkennungseinstellungen. Zugriff für mich zurückziehen - Gerätename: + Sitzungsname: Format: RiotX ist ein neuer Client für das Matrix-Protokoll (matrix.org): Ein offenes Netzwerk für sichere, dezentrale Kommunikation. @@ -1778,12 +1774,12 @@ Wenn du diese neue Wiederherstellungsmethode nicht eingerichtet hast, kann ein A \n \nNicht alle Features in Riot sind bisher in RiotX implementiert. Hauptfunktionen, die noch fehlen (und bald kommen!): • Raum-Einstellungen (Raum-Mitglieder auflisten, etc.) • Anrufe • Widgets • … - Du nutzt aktuell %1$s um zu entdecken und von dir bekannten Kontakten entdeckt zu werden. + Du nutzt aktuell %1$s um vorhandene Kontakte zu finden und um von dir bekannten Kontakten gefunden zu werden. Du benutzt aktuell keinen Identitätsserver. Um zu entdecken und um von dir bekannten Kontakten entdeckt zu werden, richte unten einen ein. Entdeckbare Telefonnummern Bitte gebe Adresse des Identitätsserver ein Identitätsserver hat keine Nutzungsbedingungen - Der Identitätsserver den du ausgewählt hast, hat keine Nutzungsbedingungen. Fahre nur fort, wenn du dem Besitzer des Dienstes vertraust + Der Identitätsserver den du ausgewählt hast, hat keine Nutzungsbedingungen. Fahre nur fort, wenn du dem/r Besitzer!n des Dienstes vertraust Eine Textnachricht wurde an %s gesendet. Bitte gebe den Verifizierungscode ein, den sie enthält. Aktiviere ausführliche Logs. @@ -1825,7 +1821,7 @@ Wenn du diese neue Wiederherstellungsmethode nicht eingerichtet hast, kann ein A Kontakt Kamera Audio - Gallerie + Galerie Sticker Es ist Spam Es ist unangebracht @@ -1886,8 +1882,8 @@ Wenn du diese neue Wiederherstellungsmethode nicht eingerichtet hast, kann ein A Fortfahren Eine Trennung von deinem Identitätsserver würde bedeuten, dass du weder von anderen Nutzern gefunden werden, noch diese per E-Mail oder Telefonnummer einladen kannst. - Derzeitig teilst du deine E-Mail-Adresse oder Telefonnummer über den Identitätsserver %1$s. Du müsstest dich erneut zu %2$s verbinden um dies zu unterbinden. - Stimme den Nutzungsbedingungen des Identitätsservers (%s) zu, um zu erlauben per E-Mail oder Telefonnummer gefunden werden zu können. + Du teilst deine Email Adressen oder Telefonnummern momentan auf dem Identitätsserver %1$s. Du wirst dich erneut mit %2$s verbinden müssen, um mit dem Teilen aufzuhören. + Stimme den Nutzungsbedingungen des Identitätsservers (%s) zu, um zu erlauben per E-Mail oder Telefonnummer gefunden zu werden. gelesen von %1$s, %2$s und %3$d anderen Zu teilende Daten nicht verarbeitbar @@ -1916,7 +1912,7 @@ Wenn du diese neue Wiederherstellungsmethode nicht eingerichtet hast, kann ein A Neues Passwort Achtung! - Eine Änderung deines Passworts wird alle Ende-zu-Ende-Verschlüsselungsschlüssel zurücksetzen. Dein verschlüsselter Chatverlauf wird dadurch unlesbar. Richte eine Schlüsselsicherung ein oder exportiere deine Raumschlüssel von einem anderen Gerät bevor du dein Passwort zurücksetzt. + Eine Änderung deines Passworts wird alle Ende-zu-Ende-Verschlüsselungsschlüssel zurücksetzen. Dein verschlüsselter Chatverlauf wird dadurch unlesbar. Richte eine Schlüsselsicherung ein oder exportiere deine Raumschlüssel von einer anderen Sitzung bevor du dein Passwort zurücksetzt. Fortfahren Diese E-Mail-Adresse ist mit keinem Benutzerkonto verknüpft @@ -1959,4 +1955,321 @@ Wenn du diese neue Wiederherstellungsmethode nicht eingerichtet hast, kann ein A Wir haben dir eine E-Mail an %1$s gesendet. \nBitte öffne den darin enthaltenen Link, um mit der Benutzerkontoerstellung fortzufahren. Der eingegebene Code ist nicht korrekt. Bitte überprüfe deine Eingabe. + Beginne zu Tippen um eine Reaktion zu finden. + + %1$s hat den Raum auf \"nur-einladen\" gestellt. + Befreie deine Kommunikation + Premium-Hosting für Organisationen + Gib die Adresse des Modular Riot oder Servers ein, den du verwenden möchtest + Gibt die Adresse eines Servers oder eines Riot ein, zu dem du dich verbinden möchtest + + Die Anwendung kann sich nicht bei diesem Heimserver anmelden. Der Heimserver unterstützt die folgenden Anmeldetypen:%1$s. +\n +\nMöchtest du dich mit einem Webclient anmelden\? + Eine Bestätigungs-E-Mail wird an dich gesendet, um dein neues Passwort zu bestätigen. + Weiter + Du wurdest von allen Sitzungen abgemeldet und erhälst keine Push-Benachrichtigungen mehr. Um Benachrichtigungen wieder zu aktivieren, melde dich auf jedem Gerät erneut an. + Warnung + Lege eine E-Mail-Adresse fest, um dein Konto wiederherzustellen. Später kannst du optional zulassen, dass Personen dich anhand dieser E-Mail-Adresse entdecken. + Weiter + + Lege Telefonnummer fest + Lege eine Telefonnummer fest, damit Personen dich anhand dieser entdecken können. + Bitte verwende das internationale Format. + Weiter + + Weiter + + Internationale Telefonnummern müssen mit \'+\' beginnen + Die Telefonnummer scheint ungültig zu sein. Bitte prüfen + + Anmelden bei %1$s + Benutzername + Weiter + Warnung + Bitte löse das Captcha + Veralteter Heimserver + Auf diesem Heimserver läuft eine zu alte Version, um eine Verbindung herzustellen. Bitten die Heimserver-Administration um ein Upgrade. + + + Es wurden zu viele Anfragen gesendet. Versuche es erneut in %1$d Sekunde… + Es wurden zu viele Anfragen gesendet. Versuche es erneut in %1$d Sekunden… + + + Gesehen von + + Du bist abgemeldet + Dies kann verschiedene Gründe haben: +\n +\n• Du hast dein Passwort in einer anderen Sitzung geändert. +\n +\n• Du hast diese Sitzung aus einer anderen Sitzung heraus gelöscht. +\n +\n• Die Administration deines Servers hat deinen Zugriff aus Sicherheitsgründen ungültig gemacht. + Melde dich erneut an + + Du bist abgemeldet + Anmelden + Deine Heimserver-Administration (%1$s) hat dich von deinem Konto %2$s (%3$s) abgemeldet. + Melden dich an, um ausschließlich auf diesem Gerät gespeicherte Verschlüsselungsschlüssel wiederherzustellen. Du benötigst sie, um deine verschlüsselten Nachrichten auf jedem Gerät zu lesen. + Anmelden + Passwort + Persönliche Daten löschen + Warnung: Persönliche Daten (einschließlich Verschlüsselungsschlüssel) werden weiterhin auf diesem Gerät gespeichert. +\n +\nDeaktiviere diese Option, wenn dieses Gerät nicht mehr verwenden wird oder sich bei einem anderen Konto angemeldet werden soll. + Alle Daten löschen + + Daten löschen + Alle aktuell auf diesem Gerät gespeicherten Daten löschen\? +\nMelde dich erneut an, um auf deine Kontodaten und Nachrichten zuzugreifen. + Du verlierst den Zugriff auf verschlüsselte Nachrichten, außer, du meldest dich an, um den Verschlüsselungsschlüssel wiederherzustellen. + Daten löschen + Die aktuelle Sitzung gehört dem/der Benutzer!n%1$s. Die angegebenen Anmeldeinformationen sind von Benutzer!n %2$s. Dies wird nicht von RiotX unterstützt. +\nBitte zuerst die Daten löschen und dann erneut anmelden. + + matrix.to-Link fehlerhaft + Die Beschreibung ist zu kurz + + Initiale Synchronisierung… + + Alle meine Sitzungen anzeigen + Erweiterte Einstellungen + Entwicklungsmodus + Der Entwicklungsmodus aktiviert versteckte Funktionen und kann die Anwendung weniger stabil machen. Nur für Entwickler!nnen! + Wutschütteln + Erkennungsschwelle + Schüttel dein Telefon, um die Erkennungsschwelle zu testen + Schütteln erkannt! + Einstellungen + Aktuelle Sitzung + Andere Sitzungen + + Zeigt nur die ersten Ergebnisse, gib mehr Buchstaben ein… + + Ausfallsicher + RiotX kann häufiger abstürzen, wenn ein unerwarteter Fehler auftritt + + Überprüfe die angegebenen Benutzer-ID + Stellt einer Klartextnachricht ¯\\_(ツ)_/¯ voran + + Aktiviere Verschlüsselung + Nach der Aktivierung kann die Verschlüsselung nicht deaktiviert werden. + + Deine E-Mail-Domain ist nicht berechtigt, sich auf diesem Server zu registrieren + + Nicht vertrauenswürdige Anmeldung + Sie passen + Sie passen nicht + Verifiziere diese/n Benutzer!n, indem du bestätigst, dass diese einzigartigen Emoji in der selben Reihenfolge auf dem Bildschirm deines Gegenübers angezeigt werden. + Für ultimative Sicherheit verwende ein anderes vertrauenswürdiges Kommunikationsmittel oder mache es persönlich. + Suche nach dem grünen Schild, um sicherzustellen, dass ein/e Benutzer!n vertrauenswürdig ist. Vertraue allen Benutzer!nnen in einem Raum, um sicherzustellen, dass der Raum sicher ist. + + Nicht sicher + Eine der folgenden Möglichkeiten kann beeinträchtigt sein: +\n +\n - Dein Heimserver +\n - Der Heimserver mit dem dein Gegenüber verbunden ist +\n - Deine oder die Internetverbindung des Gegenüber +\n - Dein Gerät oder das Gerät des Gegenüber + + Video. + Bild. + Audio + Datei + + Warten… + %s brach ab + Du hast abgebrochen + %s hat akzeptiert + Du hast akzeptiert + Verifizierung gesendet + Verifizierung angefragt + + + Verifiziere diese Sitzung + Manuelle Verifizierung + + Ich + + Scanne den Code mit dem Gerät des Gegenüber, um sich gegenseitig zu überprüfen + Scanne ihren/seinen Code + Kann nicht scannen + Wenn ihr nicht am selben Ort seid, vergleicht Emoji stattdessen + + Verifizieren via Emoji-Vergleich + + Mit Emoji verifizieren + Wenn du den obigen Code nicht scannen kannst, verifiziert, indem ihr eine kurze, eindeutige Auswahl an Emoji vergleicht. + + QR-Code-Bild + + %s verifizieren + %s verifiziert + Warte auf %s… + Für zusätzliche Sicherheit überprüfe %s, indem ihr auf beiden Geräten einen einzigartigen Code überprüft. +\n +\nFür maximale Sicherheit macht dies persönlich. + Nachrichten in diesem Raum sind nicht Ende-zu-Ende verschlüsselt. + Nachrichten in diesem Raum sind Ende-zu-Ende verschlüsselt. +\n +\nDeine Nachrichten sind gesichert und nur du und dein Gegenüber haben die eindeutigen Schlüssel, um sie zu entsperren. + Sicherheit + Mehr erfahren + Mehr + Raum Einstellungen + Benachrichtigungen + + Eine Person + %1$d Personen + + Uploads + Raum verlassen + Verlasse den Raum… + + Admins + Moderierende + benutzerdefiniert + Eingeladen + Nutzer!n + + Admin in %1$s + Moderation in %1$s + Springen & als gelesen markieren + + RiotX kann keine Ereignisse vom Typ \'%1$s\' + RiotX beherrscht keine Nachrichten vom Typ \'%1$s\' + RiotX ist beim verarbeiten des Ereignisinhalts mit der ID \'%1$s\' auf ein Problem gestoßen + + Nicht ignorieren + + Diese Sitzung kann diese Verifizierung nicht mit deinen anderen Sitzungen teilen. +\nDie Überprüfung wird lokal gespeichert und in einer zukünftigen Version der App freigegeben. + + Neueste Räume + Andere Räume + + Sendet die angegebene Nachricht in Regenbogenfarben + Sendet das angegebene Emote in Regenbogenfarben + + Zeitleiste + + Nachrichteneditor + + Aktivieren Ende-zu-Ende-Verschlüsselung + Einmal aktiviert kann die Verschlüsselung nicht rückgängig gemacht werden. + + Verschlüsselung aktivieren\? + Nach der Aktivierung kann die Verschlüsselung für einen Raum nicht deaktiviert werden. In einem verschlüsselten Raum gesendete Nachrichten können vom Server nicht gesehen werden, nur von den Teilnehmenden des Raums. Durch die Verschlüsselung funktionieren viele Bots und Bridges möglicherweise nicht ordnungsgemäß. + Verschlüsselung aktivieren + + Um sicher zu gehen, verifiziere %s, indem ein einmaligen Code überprüft wird. + Um sicher zu sein, tut dies persönlich oder verwendet einen anderen Kommunikationsweg. + + Vergleiche die einzigartigen Emoji und stell sicher, dass sie in derselben Reihenfolge angezeigt werden. + Vergleiche den Code mit dem Code auf dem Bildschirm deines Gegenübers. + Nachrichten mit diesem Gegenüber sind Ende-zu-Ende verschlüsselt und können nicht von Dritten gelesen werden. + Deine neue Sitzung ist jetzt verifiziert. Sie hat Zugriff auf deine verschlüsselten Nachrichten, und andere Benutzer!nnen sehen sie als vertrauenswürdig an. + + + Cross-Signing + Cross-Signing ist aktiviert +\nPrivate Schlüssel auf dem Gerät. + Cross-Signing ist aktiviert +\nSchlüssel sind vertrauenswürdig. +\nPrivate Schlüssel sind nicht bekannt + Cross-Signing ist aktiviert +\nSchlüssel sind nicht vertrauenswürdig + Cross-Signing ist nicht aktiviert + + + Aktive Sitzungen + Zeige alle Sitzungen + Verwalte Sitzungen + Diese Sitzung abmelden + + Keine kryptografischen Informationen verfügbar + + Diese Sitzung ist für sichere Nachrichtenübertragung vertrauenswürdig, da du sie überprüft hast: + Verifiziere diese Sitzung, um sie als vertrauenswürdig zu markieren, und gewähren ihr Zugriff auf verschlüsselte Nachrichten. Wenn du dich nicht bei dieser Sitzung angemeldet hast, ist dein Konto möglicherweise gefährdet: + + + Eine aktive Sitzung + %d aktive Sitzungen + + + Verifiziere diese Sitzung + Andere Benutzer!nnen vertrauen ihr möglicherweise nicht + Vollständige Sicherheit + + Öffne eine vorhandene Sitzung und verwende sie, um diese zu überprüfen und ihr Zugriff auf verschlüsselte Nachrichten zu gewähren. + + + Verifizieren + Verifiziert + Warnung + + Sitzungen konnten nicht abgerufen werden + Sitzungen + Vertraut + Nicht vertraut + + Diese Sitzung ist für sichere Nachrichtenübertragung vertrauenswürdig, weil %1$s (%2$s) sie verifiziert hat: + %1$s (%2$s) hat sich in einer neuen Sitzung angemeldet: + Bis diese/r Benutzer!n dieser Sitzung vertraut, werden an und von ihr/ihm gesendete Nachrichten mit Warnungen gekennzeichnet. Alternativ kannst du dies manuell überprüfen. + + + Initialisiere Cross-Signing + Schlüssel zurücksetzen + + QR-Code + + Ja + Nein + + Verbindung zum Server wurde unterbrochen + + Entwicklungswerkzeuge + Kontodaten + + %d Stimme + %d Stimmen + + + %d Stimme - Endergebnis + %d Stimmen - Endergebnis + + Ausgewählte Option + Erstellt eine einfache Umfrage + Kann nicht auf eine vorhandene Sitzung zugegriffen werden\? + Verwende deinen Wiederherstellungsschlüssel oder deine Passphrase + + Neue Anmeldung + + Kann keine Geheimnisse im Speicher finden + Gib die geheime Speicherpassphrase ein + Warnung: + Du solltest nur von einem vertrauenswürdigen Gerät auf den geheimen Speicher zugreifen + Greife auf deinen sicheren Nachrichtenverlauf und deine Cross-Signing-Identität zu, um andere Sitzungen zu überprüfen, indem du deine Passphrase eingibst + + Entfernen… + Möchtest du diesen Anhang an %1$s senden\? + + Sende Bild in Originalgröße + Sende Bilder in Originalgröße + + + Entfernen bestätigen + Möchtest du dieses Ereignis wirklich entfernen (löschen)\? Beachte, dass beim Löschen eines Raumnamens oder einer Themenänderung die Änderung rückgängig gemacht werden kann. + Grund hinzufügen + Grund für das Editieren + + Ereignis gelöscht von Benutzer!n, Grund: %1$s + Ereignis vom Raumadministration moderiert, Grund: %1$s + + Schlüssel sind bereits aktuell! + + Spoiler + Benutzerdefiniert (%1$d) in %2$s + diff --git a/vector/src/main/res/values-es/strings.xml b/vector/src/main/res/values-es/strings.xml index 9ae69b7e52..ec37ea2825 100644 --- a/vector/src/main/res/values-es/strings.xml +++ b/vector/src/main/res/values-es/strings.xml @@ -1482,7 +1482,7 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de Ok ¿No aparece nada\? No todas las aplicaciones cliente soportan verificación interactiva. Usa la verificación clásica. - Usar verificación clásica + Usar verificación clásica. Verificación de clave Solicitud cancelada @@ -1597,7 +1597,7 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de Por favor, pídele al administrador de tu servidor doméstico (%1$s) que configure un servidor TURN para que las llamadas funcionen de forma fiable. \n \nAlternativamente, puedes intentar usar el servidor público en %2$s, pero no será tan confiable, y compartirá tu dirección IP con ese servidor. También puedes cambiar esto en Configuración. - Configura un correo electrónico para la recuperación de la cuenta, y más tarde configúralo ser descubierto opcionalmente por personas que te conozcan + Configura un correo electrónico para la recuperación de la cuenta, y opcionalmente para encontrar personas conocidas Configura un correo electrónico para la recuperación de la cuenta. Usa el correo electrónico o el teléfono más tarde para ser descubierto opcionalmente por personas que te conozcan. Configura un correo electrónico para la recuperación de la cuenta. Usa el correo electrónico o el teléfono más tarde para ser descubierto opcionalmente por personas que te conozcan. Esta no es una dirección de servidor Matrix válida @@ -1878,4 +1878,36 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de Empieza Selecciona un servidor + Como el correo electrónico, las cuantas tienen un hogar, aunque se puede hablar con cualquiera + Alojamiento de pago para organizaciones + Saber más + Otro + Ajustes avanzados y de personalización + + Continuar + Conectarse a %1$s + Conectarse a Modular + Conectarse a un servidor externo + Iniciar sesión en %1$s + Registrarse + Iniciar sesión + Dirección + Alojamiento de pago para organizaciones + Introduzca la dirección de Modular Riot o servidor que quieres usar + Introduzca la dirección del servidor Riot al que quieres conectarte + + Se produjo un error al cargar la pagina: %1$s (%2$d) + "La aplicación no es capaz de iniciar sesión en este servidor. Este solo soporta el acceso mediante: %1$s. +\n +\n¿Quieres acceder usando un cliente web\?" + Lo sentimos, este servidor no acepta nuevas cuentas. + La aplicación no fue capaz de crear una cuenta en este servidor. +\n +\n¿Quieres registrarte usando un cliente web\? + + La dirección de coreo electrónico no está asociada a ninguna cuenta. + + Reiniciar contraseña en %1$s + ¡Las claves ya están al día! + diff --git a/vector/src/main/res/values-eu/strings.xml b/vector/src/main/res/values-eu/strings.xml index 86fb1e0e12..f6a4ab139f 100644 --- a/vector/src/main/res/values-eu/strings.xml +++ b/vector/src/main/res/values-eu/strings.xml @@ -285,7 +285,7 @@ Baimendu sarbidea hurrengo laster-leihoan zure telefonotik fitxategiak bidali ah Debekua kendu Berrezarri erabiltzaile arrunt gisa Bihurtu moderatzaile - Bihurtu kudeatzaile + Bihurtu administratzaile Ezkutatu kide honen mezu guztiak Erakutsi kide honen mezu guztiak Erabiltzailearen IDa, izena edo e-maila @@ -635,7 +635,7 @@ Orain egin dezakezu edo gero aplikazioaren ezarpenetatik. ERABILTZAILE DIREKTORIOA (%s) Datuak gordetzeko modua - Itxura + Azala Letra-tamaina Oso txikia @@ -645,9 +645,9 @@ Orain egin dezakezu edo gero aplikazioaren ezarpenetatik. Oso handia Handiena Erraldoia - Itxura argia - Itxura iluna - Itxura beltza + Azal argia + Azal iluna + Azal beltza Sinkronizatzen… Entzun gertaerak @@ -991,7 +991,7 @@ Kontuan izan ekintza honek aplikazioa berrabiaraziko duela eta denbora bat behar Errore bat gertatu da - Status.im itxura + Status.im azala %s bertsioa Sortu esportatutako gakoak zifratzeko pasaesaldi bat. Pasaesaldi hori gakoak inportatzeko sartu beharko duzu. @@ -1291,7 +1291,7 @@ Abisua: Fitxategi hau ezabatu daiteke aplikazioa desinstalatzen bada. Ezikusi Ziur saioa amaitu nahi duzula\? - Hasi saioa urrats batean + Hasi saioa Single sign-on bidez URL-a ez dago eskuragarri, egiaztatu mesedez Zure gailuak zaharkitutako TLS segurtasun protokolo bat darabil, erasotu daitekeena, zure segurtasunerako ezin izango zara konektatu Bidali mezua Sartu tekla sakatuta @@ -1553,12 +1553,12 @@ Abisua: Fitxategi hau ezabatu daiteke aplikazioa desinstalatzen bada. RiotX - Hurrengo belaunaldiko Matrix bezeroa Matrix-erako bezero azkarrago eta arinago bat azken Android tresnak erabiliz eginak - RiotX bezero berri bat da Matrix protokoloarentzako (Matrix.org): komunikazioa seguru eta deszentralizatuarentzako sare libre bat. RiotX Android plataformarako Riot bezeroaren berridazketa oso bat da, erabat berridatzitako Android SDK-n oinarritua. -\n -\nAbisua: hau beta bertsio bat da. RiotX garapen aktiboan dago eta baditu mugak zein akatsak (gehiegi ez espero dugu). Iruzkin guztiak ongi etorriak dira! -\n -\nRiotX bezeroak honakoa ahalbidetzen du: • Badagoen kontu batean saioa hasi • Gelak sortu eta gela publikoetara elkartu • Gonbidapenak onartu edo ukatu • Erabiltzailearen gelak zerrendatu • Gelaren xehetasunak ikusi • Testuzko mezuak bidali • Eranskinak bidali • Zifratutako geletan mezuak irakurri eta idatzi • Zifratzea: E2Egakoen babeskopia, gailuaren egiaztaketa aurreratua, gakoa partekatzeko eskaria eta erantzuna • Push jakinarazpena • Gai argia, iluna eta beltza -\n + RiotX bezero berri bat da Matrix protokoloarentzako (Matrix.org): komunikazioa seguru eta deszentralizatuarentzako sare libre bat. RiotX Android plataformarako Riot bezeroaren berridazketa oso bat da, erabat berridatzitako Android SDK-n oinarritua. +\n +\nAbisua: hau beta bertsio bat da. RiotX garapen aktiboan dago eta baditu mugak zein akatsak (gehiegi ez espero dugu). Iruzkin guztiak ongi etorriak dira! +\n +\nRiotX bezeroak honakoa ahalbidetzen du: • Badagoen kontu batean saioa hasi • Gelak sortu eta gela publikoetara elkartu • Gonbidapenak onartu edo ukatu • Erabiltzailearen gelak zerrendatu • Gelaren xehetasunak ikusi • Testuzko mezuak bidali • Eranskinak bidali • Zifratutako geletan mezuak irakurri eta idatzi • Zifratzea: E2Egakoen babeskopia, gailuaren egiaztaketa aurreratua, gakoa partekatzeko eskaria eta erantzuna • Push jakinarazpena • Azal argia, iluna eta beltza +\n \nEz dira oraindik Riot bezeroaren ezaugarri guztiak ezarri RiotX bezeroan. Falta diren (eta laster etorriko direnen) artean nabarmenak dira: • Gelaren ezarpenak (gelako kideak zerrendatzea, eta abar.) • Deiak • Trepetak • … aplikazio_pantaila_izena: @@ -1784,7 +1784,7 @@ Abisua: Fitxategi hau ezabatu daiteke aplikazioa desinstalatzen bada. Zure pantaila-izena Zure abatarraren URL-a Zure erabiltzaile ID-a - Zure gaia + Zure azala Trepetaren ID-a Gelaren ID-a @@ -2079,8 +2079,8 @@ Abisua: Fitxategi hau ezabatu daiteke aplikazioa desinstalatzen bada. Saltatu irakurragirira - RiotX aplikazioak oraindik ez ditu \'%1$s\' motako gertaerak kudeatzen - RiotX aplikazioak oraindik ez ditu \'%1$s\' motako mezuak kudeatzen + RiotX aplikazioak ez ditu \'%1$s\' motako gertaerak kudeatzen + RiotX aplikazioak ez ditu \'%1$s\' motako mezuak kudeatzen RiotX aplikazioak arazo bat izan du \'%1$s\' id-a duen edukia erakusteko Utzi ezikusteari @@ -2144,7 +2144,7 @@ Abisua: Fitxategi hau ezabatu daiteke aplikazioa desinstalatzen bada. Beste erabiltzaile batzuk ez fidagarritzat jo lezakete Bete segurtasuna - Ireki aurreko saio bat eta erabili hori saio hau egiaztatzeko, mezu zifratuetara sarbidea emanez. Ez baduzu bat erabiltzerik, erabili berreskuratze gakoa edo pasa-esaldia. + Ireki aurreko saio bat eta erabili hori saio hau egiaztatzeko, mezu zifratuetara sarbidea emanez. Egiaztatu @@ -2166,9 +2166,49 @@ Abisua: Fitxategi hau ezabatu daiteke aplikazioa desinstalatzen bada.
    QR kodea - Beste erabiltzaileak QR kodea ongi eskaneatu du\? Bai Ez Zerbitzariarekin konexioa galdu da + Erabiltzaile-izena + Garapen tresnak + Kontuaren datuak + + boto %d + %d boto + + + boto %d - Azken emaitza + %d boto - Azken emaitza + + Hautatutako aukera + Inkesta sinplea sortzen du + Ezin zara badagoen saio batera sartu\? + Erabili berreskuratze gakoa edo pasa-esaldia + + Saio berria + + Ezin izan da sekreturik aurkitu biltegian + Sartu biltegi sekretuko pasa-esaldia + Abisua: + Biltegi sekretura gailu fidagarri batetik konektatu beharko zinateke beti + Atzitu zure mezu seguruen historiala eta zeharkako sinatzerako identitatea beste saioak egiaztatzeko zure pasa-esaldia sartuz + + Kendu… + Eranskin hau %1$s gelara bidali nahi duzu\? + + Bidali irudia jatorrizko tamainan + Bidali irudiak jatorrizko tamainan + + + Berretsi kentzea + Ziur gertaera hau kendu (ezabatu) nahi duzula\? Jakin gelaren izenaren edo mintzagaiaren aldaketa ezabatzen baduzu, aldaketa desegin daitekeela. + Eman arrazoi bat + Kentzeko arrazoia + + Erabiltzaileak kendu du gertaera, arrazoia: %1$s + Gelako moderatzaile batek kendu du gertaera, arrazoia: %1$s + + Gakoak egunean daude jada! + diff --git a/vector/src/main/res/values-fa/strings.xml b/vector/src/main/res/values-fa/strings.xml index 7f8a2d0c3a..5ec62b8158 100644 --- a/vector/src/main/res/values-fa/strings.xml +++ b/vector/src/main/res/values-fa/strings.xml @@ -3,9 +3,9 @@ fa IR - قالب روشن - قالب تیره - قالب سیاه + زمینهٔ روشن + زمینهٔ تیره + زمینهٔ سیاه گوش دادن به رویدادها پیام‌ها @@ -32,7 +32,7 @@ تماس فعال صوتی تصویری - اطلاعات دستگاه + اطّلاعات نشست به هر حال ارسال کنید یا دعوت @@ -106,7 +106,7 @@ شماره تلفن نامعتبر به نظر می‌رسد گذرواژه‌ها مطابقت ندارد گذرواژه را فراموش کردید؟ - از گزینه‌های کارگزار سفارشی استفاده کنید (پیشرفته) + استفاده از گزینه‌های کارساز سفارشی (پیش‌رفته) برای ادامه‌ی ثبت‌نام، لطفاً ایمیل خود را بررسی کنید من آدرس ایمیلم را تایید کرده‌ام امکان ورود به سیستم وجود ندارد: خطای شبکه @@ -141,13 +141,13 @@ پسندیده‌ها افراد - جامعه‌ها + اجتماع‌ها - جستجوی اتاق‌ها - جستجوی پسندها - جستجوی افراد - جستجوی اتاق‌ها - جستجوی جامعه‌ها + پالایش نام‌های اتاق + پالایش برگزیده‌ها + پالایش افراد + پالایش نام‌های اتاق + پالایش نام‌های اجتماع دعوت‌ها هشدارهای سیستمی @@ -160,8 +160,8 @@ فهرست اتاق‌ها اتاقی نیست - اتاق عمومی در دسترس نیست - جامعه‌ها + هیچ اتاق عمومی‌ای موجود نیست + اجتماع‌ها ارسال رخدادنگارها ارسال رخدادنگارهای خطا ارسال تصویر صفحه @@ -273,8 +273,8 @@ ابزارهای مدیر تماس - پیام‌های شخصی - دستگاه‌ها + گپ‌های مستقیم + نشست‌ها دعوت ترک این اتاق @@ -296,21 +296,21 @@ آیا می‌خواهید از حساب کاربری خود خارج شوید؟ علامت‌گذاری به عنوان خوانده شده ورود با سامانه‌های احراز هویت مرکزی - قالب Status.im + زمینهٔ Status.im راه‌اندازی سرویس - اعلان‌های باصدا - اعلان‌های بی‌صدا + آگاهی‌های پرصدا + آگاهی‌های صامت تاریخچه - جزئیات جامعه + جزییات اجتماع ارسال استیکر پشتیبان‌گیری کلید بازیابی پشتیبان کلید پشتیبان‌گیری از کلید هنوز به پایان نرسیده است، لطفاً صبر کنید… در صورتی که الآن از حساب خود خارج شوید، پیام‌های رمز خود را از دست خواهید داد پشتیبان‌گیری کلید در جریان است. در صورتی که الآن از حساب خود خارج شوید، پیام‌های رمز خود را از دست خواهید داد. - برای جلوگیری از گم شدن پیام‌های رمز، پشتیبان امن کلید باید روی تمام دستگاه‌های شما فعال باشد. + برای از دست ندادن دسترسی به پیام‌های رمزشده، باید پشتیبان کلید امن روی تمام نشست‌هایتان فعّال باشد. پیام‌های رمز خود را نمی‌خواهم پشتیبان‌گیری از کلیدها… استفاده از پشتیبان کلید @@ -332,14 +332,326 @@ \nمی‌توانید در ادامه ایمیل خود را در تنظیمات برنامه به پروفایل خود اضافه کنید.
    کارگزار می‌خواهد اطمینان یابد که شما ربات نیستید نام کاربری قبلاً استفاده شده است - دریافت کلیدهای رمزنگاری از سایر دستگاه‌های شما. + بازدرخواست کلیدهای رمزنگاری از دیگر نشست‌هایتان. درخواست کلید ارسال شد. ارسال درخواست - لطفاً برنامه را روی یکی از دستگاه‌های دیگرتان که به این پیام دسترسی داشته است، اجرا کنید تا کلیدها منتقل شوند. + لطفاً ریوت را روی افزاره‌ای دیگر که می‌تواند پیام را رمزگشایی کند، اجرا کنید تا بتواند کلیدها را به این نشست بفرستد. - مشاهده گیرندگان + فهرست رسیدهای خواندن آیا مطمئن هستید؟ + عدم اتصال + بررسی + رد کردن + + دوباره از من نپرس + + برای بازیابی یک ایمیل وارد کنید، و بعدا در صورت دلخواه می توانید از آن برای شناسایی دوستان خود استفاده کنید. + ثبت شماره تلفن (بعدا در صورت دلخواه می توانید از آن برای شناسایی دوستان هود استفاده کنید). + نشانی رایانامهٔ پیوسته به حسابتان را برای بازنشانی گذواژه‌تان وارد کنید: + Latn + + شناسهٔ کاربری، نام یا رایانامه + فرستادن پاسخی رمزشده… + فرستادن یک پاسخ (رمزنشده)… + پالایش اعضای اتاق + اتاق‌ها + اتاق‌ها + + ۱ اتاق + %d اتاق + + + %1$s اتاق برای %2$s پیدا شد + %1$s اتاق برای %2$s پیدا شد + + گپ مستقیم + نگارش + نگارش %s + شرایط و ضوابط + تذکّرهای سوم‌شخص + حق رونوشت + سیاست محرمانگی + + عکس نمایه + نام نمایشی + رایانامه + افزودن نشانی رایانامه + تلفن + افزودن شماره تلفن + اطّلاعات برنامه + نمایش اطّلاعات برنامه در تنظیمات سامانه. + تأیید گذرواژه‌تان + نمی‌توانید این کار را از ریوت همراه انجام دهید + نیاز به تأیید هویت است + + + تنظمیات پیش‌رفتهٔ آگاهی + آگاهی‌های رفع‌اشکال + آگاهی‌ها در تنظیمات سامانه به کار افتاده‌اند. + آگاهی‌ّا در تنظیمات سامانه از کار افتاده‌اند. +\nلطفاً تنظیمات سامانه را بررسی کنید. + آگاهی‌ها برای حسابتان به کار افتاده‌اند. + آگاهی‌ها برای حسابتان از کار افتاده‌اند. +\nلطفاً تتظیمات حساب را بررسی کنید. + آگاهی‌ها برای این نشست به کار افتاده‌اند. + آگاهی‌ها برای این نشست از کار افتاده‌اند. +\nلطفاً تنظینات ریوت را بررسی کنید. + برخی آگاهی‌ها در تنظیمات سفارشیتان از کار افتاده‌اند. + خدمت آگاهی + خدمت آگاهی در حال اجراست. + خدمت آگاهی در حال اجرا نیست. +\nتلاش کنید برنامه را دوباره شروع کنید. + شروع دوبارهٔ خودکار خدمت آگاهی + به کار انداختن أگاهی‌ها برای این حساب + به کار انداختن أگاهی‌ها برای این نشست + پیکربندی أگاهی‌های پرصدا + پیکربندی أگاهی‌های تماس + پیکربندی أگاهی‌های صامت + تأخیر بین همگام‌سازی‌ها + ثانیه + ثانیه + + نگارش + نگارش olm + شرایط و ضوابط + تذکّرهای سوم‌شخص + حق رونوشت + سیاست محرمانگی + نگه‌داری رسانه + پاک‌سازی انباره + پاک‌سازی انبارهٔ رسانه + + تنظیمات کاربر + أگاهی‌ها + کاربران چشم‌پوشیده + دیگر + پیش‌رفته + یک‌پارچگی‌ها + رمزنویسی + مدیریت کلیدهای رمزنویسی + هدف‌های آگاهی + مخاطبان محلّی + اجازهٔ مخاطبان + کشور دفترچه تلفن + نمای حانه + سنجاق کردن اتاق‌هایی با آگاهی‌های بی‌پاسخ + سنجاق کردن اتاق‌هایی با پیام‌های نخوانده + نشست‌ها + پیش‌نمایش نشانی نامعتبر + فرستادن آگاهی‌های نوشتن + قالب‌بندی مارک‌دون + نمایش رسیدهای خواندن + برای یک فهرست باجزیییات، روی رسیدهای خواندن کلیک کنید. + تأیید هویت + گذرواژه: + ثبت + + واردشده به عنوان + کارساز خانگی + کارساز هویت + اجازهٔ یک‌پارچگی‌ها + مدیر یکپارچگی + + رابط کاربری + زبان + گزینش زبان + + هم‌اکنون عضو هیچ اجتماعی نیستید. + + آگاهی‌ها + کاربران مسدود + + پیش‌رفته + شناسهٔ داخلی این اتاق + نشانی‌ها + آزمایشگاه‌ها + رمزنگاری سرتاسری + رمزنگاری سرتاسری فعّال است + برای به کار انداختن رمزنگاری سرتاسری باید حارج شوید. + رمزنگاری فقط به نشست‌های تأیید شده + شناسهٔ اجتماع جدید (مثلاً ‪+foo:matrix.org‬) + شناسهٔ اجتماع نامعتبر + %s یک شناسهٔ اجتماع معتبر نیست + + + زمینه + + رمزنگاری فقط به نشست‌های تأیید شده + اتاق شامل نشست‌های ناشناخته + برای فهرست شدن اتاق‌های عمومی از یک کاساز، نامش را بنویسید + تمامی اتاق‌های روی کارساز %s + تمامی اتاق‌های بومی %s + + + ۱ اتاق + %d اتاق + + + + %d آگاهی + %d آگاهی + + + زمینه‌تان + افزودن کاره‌های ماتریس + پیام رمزنگاشته + + ایجاد اجتماع + نام اجتماع + شناسهٔ اجتماع + اتاق‌ها + اتاق‌ها + پالایش اعضای گروه + پالایش اتاق‌های گروهی + + + ۱ اتاق + %d اتاق + + مدیر اجتماع توضیحی بلند برای این اجتماع فراهم نکرده است. + + بار کردن تنبلانهٔ اعضای اتاق + (پیش‌رفته) + (پیش‌رفته) برپایی با کلید بازیابی + گرفتن نگارش پشتیبان… + شکست در گرفتن آخرین نگارش کلیدهای بازیابی (%s). + نگارش + ویرایش + پاسخ + + تلاش دوباره + برای آغاز استفاده از کاره، به اتاقی بپیوندید. + برایتان دعوتی فرستاد + دعوت‌شده به دست %s + + همه‌چی سر جاشه! + هیچ پیام نخواندهٔ دیگری ندارید + به خانه خوش آمدید! + این‌جا به پیام‌های نخوانده برسید + گفت‌وگوها + گفت‌گوهای پیام مستقیمتان این‌جا نشان داده خواهند شد + اتاق‌ها + اتاق‌هایتان این‌جا نشان داده خواهند شد + + بازخودها + موافقت + پسند + افزودن بازخورد + دیدن بازخوردها + بازخوردها + + رویداد به دست کاربر حذف شد + رویداد به دست مدیر اتاق مدیریت شد + آخرین ویرایش به دست %1$s در %2$s + + + رویداد بدشکل. نمی‌توان نمایش داد + ایجاد اتاق جدید + بدون شبکه. لطفاً اتّصال اینترنتیتان را بررسی کنید. + تغییر + تغییر شبکه + لطفاً شکیبایی کنید… + تمام اجتماع‌ها + + این اتاق نمی‌تواند پیش‌نمایش یابد + پیش‌نمایش اتاق‌های قابل خواندن به صورت عمومی هنوز در ریوت‌اکس پشتیبانی نمی‌شود + + اتاق‌ها + پیام‌های مستقیم + + اتاق جدید + ایجاد + نام اتاق + عمومی + هرکسی می‌تواند به این اتاق بپیوندد + نمایهٔ اتاق‌ها + انتشار این اتاق در نمایهٔ اتاق‌ها + + نگارش SDK ماتریس + عمومی + ترجیحات + امنیت و محرمانگی + صدا و تصویر + راهنما و درباره + + + ارایهٔ پیشنهاد + لطفاً پیشنهادتان را در زیر بنویسید. + پیشنهادتان را این‌جا شرح دهید + ممنون. پیشنهاد با موفّقیت فرستاده شد + فرستادن پیشنهاد شکست خورد (%s) + + نمایش رویدادهای نهفته در خط زمانی + + ریوت‌اکس - نسل بعدی کارخواه ماتریس + پیام‌های مستقیم + + منتظر… + رمزنگاری بندانگشتی‌ها… + فرستادن بندانگشتی‌ها (%1$s / %2$s) + رمزنگاری پرونده… + فرستادن پرونده (%1$s / %2$s) + + بارگری پرونده %1$s… + پرونده %1$s بارگیری شد! + + (ویراسته) + + پالایش گفت‌وگوها… + ایجاد اتاقی جدید + فرستادن یک پیام مستقیم جدید + نام یا شناسه (‪#example:matrix.org‬) + + به کار انداختن کشیدن برای پاسخ در خط زمانی + + افزودن با شناسهٔ ماتریس + نتیجه‌ای پیدا نشد. برای جست‌وجو روی کارساز، از افزون با شناسهٔ ماتریس استفاده کنید. + پالایش با نام کاربری یا شناسه… + + ایجاد یک گفت‌وگوی مستقیم جدید + ایجاد اتاقی جدید + هرزنامه است + نامناسب است + گزارش سفارشی… + گزارش این محتوا + دلیل گزارش این محتوا + گزارش + انسداد کاربر + + محتوا گزارش شد + به عنوان هرزنامه گزارش شد + به عنوان نامناسب گزارش شد + انسداد کاربر + + تنظیمات پیش‌رفته و سفارشی + + دیدن تمام نشست‌هایم + تنظیمات پیش‌رفته + حالت توسعه‌دهنده + تکان دادن + تکان تشخیص داده شد! + دیگر نشست‌ها + + آگاهی‌ها + اتاق‌های اخیر + دیگر اتاق‌ها + + خط زمانی + + ویرایشگر پیام + + نشست‌های فعّال + نمایش تمامی نشست‌ها + مدیریت نشست‌ها + + %d نشست فعّال + %d نشست فعّال + + + شکست در گرفتن نشست‌ها + نشست‌ها + ابزارهای توسعه diff --git a/vector/src/main/res/values-fi/strings.xml b/vector/src/main/res/values-fi/strings.xml index fef747d546..41d8ba1057 100644 --- a/vector/src/main/res/values-fi/strings.xml +++ b/vector/src/main/res/values-fi/strings.xml @@ -43,7 +43,7 @@ Puuttuvien oikeuksien takia osa ominaisuuksista ei ole käytettävissä… Sinulta puuttuu oikeus käynnistää ryhmäpuhelu tässä huoneessa Puhelua ei voitu käynnistää - Laitteen tiedot + Istunnon tiedot Ryhmäpuhelut eivät ole tuettuja salatuissa huoneissa Lähetä silti tai @@ -313,7 +313,7 @@ YLLÄPITÄJÄN TYÖKALUT PUHELUT YKSITYISKESKUSTELUT - LAITTEET + ISTUNNOT Kutsu Poistu huoneesta @@ -327,7 +327,7 @@ Näytä kaikki tämän käyttäjän viestit Käyttäjätunnus, nimi tai sähköpostiosoite Mainitse - Näytä laitelista + Näytä istuntolista Olet ylentämässä käyttäjää samalle tasolle kuin oma käyttäjätasosi. Et voi perua tätä toimintoa.\nOletko varma? Haluatko kutsua käyttäjän %s tähän keskusteluun\? @@ -350,7 +350,7 @@ Lähetä viesti (salaamaton)… Yhteys palvelimeen katkesi. Viestejä ei lähetetty. %1$s vai %2$s\? - Viestejä ei lähetetty koska huoneessa on tuntemattomia laitteita. %1$s vai %2$s\? + Viestejä ei lähetetty, koska läsnä on tuntemattomia istuntoja. %1$s vai %2$s\? Lähetä kaikki uudelleen Peruuta kaikki Lähetä lähettämättömät viestit @@ -443,7 +443,7 @@ Sovelluksen tiedot Ota ilmoitukset käyttöön tällä tilillä - Ota ilmoitukset käyttöön tällä laitteella + Ota ilmoitukset käyttöön tässä istunnossa Näyttö päälle kolmeksi sekunniksi Viestit yksityiskeskusteluissa @@ -482,8 +482,8 @@ Koti Kiinnitä huoneet, joissa on huomaamatta jääneitä ilmoituksia Kiinnitä huoneet, joissa on lukemattomia viestejä - Laitteet - Laitteen tiedot + Istunnot + Istunnon tiedot ID Julkinen nimi Päivitä julkinen nimi @@ -618,11 +618,11 @@ Sessio-ID Salauksenpurkuvirhe - Lähettävän laitteen tiedot + Lähettäjän istunnon tiedot Julkinen nimi Julkinen nimi Tunnus - Laitteen avain + Istunnon avain Vahvistus Ed25519-sormenjälki @@ -647,7 +647,7 @@ Vahvistettu Kielletty - tuntematon laite + tuntematon istunto ei mitään Vahvista @@ -665,7 +665,7 @@ \nVoit tehdä sen nyt tai myöhemmin sovelluksen asetuksissa.
    - Huoneessa on tuntemattomia laitteita + Huoneessa on tuntemattomia istuntoja Huoneessa on tuntemattomia laitteita joita ei ole vahvistettu.\nLaitteet eivät välttämättä kuulu väitetyille omistajilleen.\nJokainen uusi laite kannattaa vahvistaa ennen kuin jatkat, mutta voit myös lähettää viestit vahvistamattomille laitteille.\n\nTuntemattomat laitteet: @@ -752,7 +752,7 @@ Käytä järjestelmän kamerasovellusta - Lisäsit uuden laitteen \'%s\', joka pyytää salausavaimia. + Lisäsit uuden istunnon \'%s\', joka pyytää salausavaimia. Vahvistamaton laitteesi \'%s\' pyytää salausavaimia. Aloita varmennus Jaa ilman varmennusta @@ -913,12 +913,12 @@ Haluatko lisätä paketteja?
    jatka sovelluksella… Yhtään ulkopuolista sovellusta tämän toiminnon suorittamiseksi ei löytynyt. - Pyydä salausavaimia uudelleen muilta laitteiltasi. + Pyydä salausavaimia uudelleen muista istunnoistasi. Avainpyyntö lähetetty. Pyyntö lähetetty - Käynnistä Riot toisella laitteela, joka voi purkaa viestin, jotta se voi lähettää avaimet tähän laitteeseen. + Käynnistä Riot toisella laitteella, joka voi purkaa viestin, jotta se voi lähettää avaimet tähän istuntoon. yksi jäsenyysmuutos @@ -1018,9 +1018,9 @@ Haluatko lisätä paketteja?
    \nTarkista tilisi asetukset.
    Ota käyttöön - Laitteen asetukset. - Ilmoitukset ovat käytössä tällä laitteella. - Ilmoituksia ei sallita tällä laitteella. + Istunnon asetukset. + Ilmoitukset ovat käytössä tässä istunnossa. + Ilmoitukset eivät ole käytössä tässä istunnossa. \nTarkista Riotin asetukset. Ota käyttöön @@ -1320,8 +1320,8 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös Varmuuskopio palautettiin %d avaimella. - yksi uusi avain lisätty tähän laitteeseen. - %d uutta avainta lisätty tähän laitteeseen. + Yksi uusi avain lisätty tähän istuntoon. + %d uutta avainta lisätty tähän listuntoon. Uusimman palautusavaimen version hakeminen epäonnistui (%s). @@ -1332,11 +1332,11 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös Poista varmuuskopio Avaimien varmuuskopiointi on käytössä tällä laitteella. - Avaimien varmuuskopiointi ei ole käytössä tällä laitteella. - Avaimiasi ei varmuuskopioida tältä laitteelta. + Avaimien varmuuskopiointi ei ole käytössä tässä istunnossa. + Avaimiasi ei varmuuskopioida tästä istunnosta. Varmuuskopiossa on allekirjoitus tuntemattomasta laitteesta ID:llä %s. - Varmuuskopiossa on pätevä allekirjoitus tältä laitteelta. + Varmuuskopiossa on pätevä allekirjoitus tästä istunnosta. Varmuuskopiossa on pätevä allekirjoitus varmennetulta laitteelta %s. Varmuuskopiossa on pätevä allekirjoitus varmentamattomalta laitteelta %s Varmuuskopiossa on epäkelpo allekirjoitus varmennetulta laitteelta %s @@ -1451,21 +1451,21 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös Selvä Pyyntö peruttu - Laite vastaanotti odottamattoman viestin + Istunto vastaanotti odottamattoman viestin Virheellinen viesti vastaanotettu Avain ei täsmää Käyttäjä ei täsmää Tuntematon virhe Kotipalvelimellasi on jo varmuuskopio - Näyttää, että olet jo asettanut avainten varmuuskopioinnin toiselta laitteelta. Halutatko korvata sen tällä\? + Näyttää, että olet jo asettanut avainten varmuuskopioinnin toisesta istunnosta. Halutatko korvata sen tällä\? Korvaa Seis Tarkistetaan varmuuskopion tilaa Odotetaan vastapuolen varmistusta… - Laite ei ole tietoinen kyseisestä transaktiosta + Istunto ei ole tietoinen kyseisestä transaktiosta SAS ei täsmännyt Muokkaa Vastaa @@ -1624,7 +1624,7 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös %s \nSynkronointia saatetaan lykätä resursseista (akusta) tai laitteen tilasta (virransäästö) riippuen. Julkinen nimi (näkyy ihmisille, joihin olet yhteydessä) - Laitteen julkinen nimi näkyy ihmisille, joihin olet yhteydessä + Istunnon julkinen nimi näkyy ihmisille, joihin olet yhteydessä Jatkaaksesi sinun täytyy hyväksyä palvelun käyttöehdot. Et käytä identiteettipalvelinta @@ -2026,4 +2026,29 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös \n \n• palvelimen ylläpitäjä on estänyt pääsysi turvallisuussyistä. Kirjaudu sisään palauttaaksesi salausavaimesi, jotka ovat tallessa vain tällä laitteella. Tarvitset niitä lukeaksi kaikki salatut viestisi millä tahansa laitteella. + Näytä kaikki istuntoni + Lisäasetukset + Asetukset + Nykyinen istunto + Muut istunnot + + Ota salaus käyttöön + Salausta ei voi poistaa käytöstä, kun se on kerran otettu käyttöön. + + Odotetaan… + %s peruutti + Sinä peruutit + %s hyväksyi + Sinä hyväksyit + Sinä + + Lue lisää + Ilmoitukset + + Yksi henkilö + %1$d ihmistä + + Poistu huoneesta + Poistutaan huoneesta… + diff --git a/vector/src/main/res/values-fr/strings.xml b/vector/src/main/res/values-fr/strings.xml index 341a247d52..ea432d61bc 100644 --- a/vector/src/main/res/values-fr/strings.xml +++ b/vector/src/main/res/values-fr/strings.xml @@ -287,7 +287,7 @@ Activer les notifications pour cette session Messages dans les discussions directes Messages dans les discussions de groupe - Demandes d’appel + Appels entrants Messages envoyés par un robot Synchronisation en arrière-plan @@ -2087,8 +2087,8 @@ Si vous n’avez pas configuré de nouvelle méthode de récupération, un attaq Aller à l’accusé de lecture - RiotX ne gère pas (encore) les évènements de type « %1$s » - RiotX ne gère pas (encore) les messages de type « %1$s » + RiotX ne gère pas les évènements de type « %1$s » + RiotX ne gère pas les messages de type « %1$s » RiotX a rencontré un problème lors de l’affichage du contenu de l’évènement ayant pour identifiant « %1$s » Ne plus ignorer @@ -2152,7 +2152,7 @@ Si vous n’avez pas configuré de nouvelle méthode de récupération, un attaq Les autres utilisateurs ne lui font peut-être pas confiance Compléter la sécurité - Ouvrez une session existante et utilisez-la pour vérifier celle-ci, pour lui permettre d’accéder aux messages chiffrés. Si vous ne pouvez pas le faire, utilisez votre clé de récupération ou votre phrase secrète. + Ouvrez une session existante et utilisez-la pour vérifier celle-ci, pour lui permettre d’accéder aux messages chiffrés. Vérifier @@ -2174,9 +2174,49 @@ Si vous n’avez pas configuré de nouvelle méthode de récupération, un attaq Code QR - L’autre utilisateur a-t-il bien scanné le code QR \? Oui Non La connectivité avec le serveur a été perdue + Nom d’utilisateur + Outils de développement + Données du compte + + %d vote + %d votes + + + %d vote − Résultats finaux + %d votes − Résultats finaux + + Option sélectionnée + Crée un sondage simple + Vous n’avez pas accès à une session existante \? + Utilisez votre clé de récupération ou votre phrase de passe + + Nouvelle connexion + + Impossible de trouver les secrets dans le stockage + Saisir la phrase de passe du coffre secret + Attention : + Vous devriez accéder à votre coffre secret uniquement depuis un appareil de confiance + Accédez à l’historique de vos messages sécurisés et à votre identité de signature croisée pour vérifier d’autres sessions en saisissant votre phrase de passe + + Supprimer… + Voulez-vous envoyer cette pièce jointe à %1$s \? + + Envoyer l’image en taille originale + Envoyer les images en taille originale + + + Confirmer la suppression + Voulez-vous vraiment supprimer cet évènement \? Notez que si vous supprimez un changement de nom ou de sujet du salon, cela pourrait annuler le changement. + Fournir un motif + Motif de la suppression + + Évènement supprimé par l’utilisateur, motif : %1$s + Évènement modéré par l’administrateur du salon, motif : %1$s + + Les clés sont déjà à jour ! + diff --git a/vector/src/main/res/values-fy/strings.xml b/vector/src/main/res/values-fy/strings.xml index 13b3b21468..87e1b59509 100644 --- a/vector/src/main/res/values-fy/strings.xml +++ b/vector/src/main/res/values-fy/strings.xml @@ -78,4 +78,83 @@ Om in gearkomst yn dit groepspetear te starten hasto útnûgingsrjochten nedich Kin de oprop net starte Apparaatynformaasje + Gearkomstpetearen wurde net stipe yn fersifere petearen + Dochs belje + Dochs ferstjoere + of + Utnûgje + Offline + Akseptearje + Oerslaan + Klear + Ofbrekke + Negearje + Beoardielje + Wegerje + + Ofslute + Aksjes + Ofmelde + Binne jo wis dat jo jo ôfmelde wolle\? + Spraakoprop + Fideo-oprop + Globaal sykje + Alles as lêzen markearje + Histoarysk + Flugge reaksje + As lêzen markearje + Iepenje + Slute + Nei klamboerd kopiearre + Utskeakelje + + Befêstiging + Warskôging + Flater + + Start + Favoriten + Minsken + Petearen + Mienskippen + + Petearnammen filterje + Favoriten filterje + Persoanen filterje + Petearnammen filterje + Mienskipsnammen filterje + + Utnûgingen + Lege prioriteit + Systeemmeldingen + + Petearen + Lokale kontaktelist + Brûkerskatalogus + Allinnich Matrix-kontakten + Gjin petearen + Jo hawwe Riot gjin tagong ta jo lokale kontakten jûn + Gjin resultaten + Gjin identiteitsserver konfigurearre. + + Petearen + Petearkatalogus + Gjin petearen + Gjin publike petearen beskikber + + 1 brûker + %d brûkers + + + Utnûgje + Mienskippen + Gjin groepen + + Lochboek ferstjoere + Ungeloklochboek ferstjoere + Skermôfdruk ferstjoere + Flater melde + Beskriuw de flater. Wat hawwe jo dien\? Wat ferwachten jo dat der barre soe\? Wat is der echt bard\? + Beskriuw it probleem yn it Ingelsk, wannear mooglik. + Beskriuw hjir jo probleem diff --git a/vector/src/main/res/values-hu/strings.xml b/vector/src/main/res/values-hu/strings.xml index 64eee79075..c40bd860b8 100644 --- a/vector/src/main/res/values-hu/strings.xml +++ b/vector/src/main/res/values-hu/strings.xml @@ -1497,7 +1497,7 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró Minden közösség Ennek a szobának nincs előnézete - A RiotX-ben a nyilvános szoba előnézete egyenlőre nem támogatott + A RiotX-ben a nyilvános szoba előnézete egyelőre nem támogatott Szobák Közvetlen üzenetek @@ -1895,7 +1895,7 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró Következő Telefonszám ellenőrzése - Elküldtük a kódot ide: %1$s. Add meg itt alul amivel ellenőrizhetjük, hogy te te vagy. + Elküldtük a kódot ide: %1$s. Add meg itt alul amivel ellenőrizhetjük, hogy te vagy. Kód megadása Küld újra Következő @@ -2082,8 +2082,8 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró Olvasási visszaigazolásra ugrás - RiotX (egyenlőre) nem kezeli ezt az eseményt: \'%1$s\' - RiotX (egyenlőre) nem kezeli ezt az üzenet típust: \'%1$s\' + RiotX nem kezeli ezt az eseményt: \'%1$s\' + RiotX nem kezeli ezt az üzenet típust: \'%1$s\' RiotX problémába ütközött az esemény (azon: %1$s) megjelenítésekor Figyelembe vesz @@ -2147,7 +2147,7 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró Más felhasználók lehet, hogy nem bíznak benne Biztonság beállítása - A titkosított üzenetekhez való hozzáféréshez nyiss meg egy létező munkamenetet és használd ennek a hitelesítésére. Ha egyhez sem férsz hozzá használd a visszaállítási kulcsodat vagy jelmondatodat. + A titkosított üzenetekhez való hozzáféréshez nyiss meg egy létező munkamenetet és használd ennek a hitelesítésére. Ellenőriz @@ -2169,9 +2169,49 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró QR kód - A másik felhasználó sikeresen beolvasta a QR kódot\? Igen Nem Megszakadt a kapcsolat a szerverrel + Felhasználónév + Fejlesztői Eszközök + Fiók Adatok + + %d szavazat + %d szavazat + + + Végeredmény - %d szavazat + Végeredmény - %d szavazat + + Kiválasztott Beállítások + Egyszerű szavazás készítése + A létező munkamenet nem érhető el\? + Használd a visszaállítási kulcsot vagy jelmondatot + + Új Bejelentkezés + + A tárolóban nem található jelszó/kulcs + Add meg a jelmondatot a biztonsági tárolóhoz + Figyelem: + Csak biztonságos eszközről férj hozzá a biztonsági tárolóhoz + A jelmondat megadásával hozzáférhetsz a biztonságos üzeneteidhez és az eszközök közötti hitelesítéshez használt személyazonosságodhoz, hogy más munkameneteket hitelesíthess + + Töröl… + Ezt a csatolmányt el szeretnéd küldeni ide: %1$s\? + + Kép küldése eredeti méretben + Képek küldése eredeti méretben + + + Törlés megerősítése + Biztos hogy eltávolítod (törlöd) ezt az eseményt\? Figyelem, ha törlöd vagy megváltoztatod a szoba nevét vagy a témát ez a változtatás érvényét vesztheti. + Ok megadása + Ok a kitakaráshoz + + Az eseményt a felhasználó törölte, ezért: %1$s + Az eseményt a szoba adminisztrátora moderálta, ezért: %1$s + + A kulcsok már frissek! + diff --git a/vector/src/main/res/values-it/strings.xml b/vector/src/main/res/values-it/strings.xml index 4d1ac8edaf..65e78c601f 100644 --- a/vector/src/main/res/values-it/strings.xml +++ b/vector/src/main/res/values-it/strings.xml @@ -38,7 +38,7 @@ Audio Video Impossibile avviare la chiamata, riprova più tardi - Poichè i tuoi permessi non sono sufficienti, alcune funzioni potrebbero non esser disponibili… + Poiché i tuoi permessi non sono sufficienti, alcune funzioni potrebbero non esser disponibili… Non hai permessi sufficienti per avviare una conferenza in questa stanza Impossibile avviare la chiamata Informazioni sulla sessione @@ -268,7 +268,7 @@ \n \nTi sta bene comunicare i dati di tutti i tuoi contatti per questo scopo\?
    - Purtroppo l\'azione non è stata eseguita poichè mancano i permessi + Purtroppo l\'azione non è stata eseguita poiché mancano i permessi Salvato @@ -2132,8 +2132,8 @@ Vai alla ricevuta di lettura - RiotX non gestisce eventi del tipo \'%1$s\' (non ancora) - RiotX non gestisce messaggi del tipo \'%1$s\' (non ancora) + RiotX non gestisce eventi del tipo \'%1$s\' + RiotX non gestisce messaggi del tipo \'%1$s\' RiotX ha riscontrato un errore con il rendering del contenuto dell\'evento con id \'%1$s\' Non ignorare @@ -2185,7 +2185,7 @@ Nessuna informazione crittografica disponibile - Questa sessione è fidata per i messaggi sicuri perchè l\'hai verificata: + Questa sessione è fidata per i messaggi sicuri perché l\'hai verificata: Verifica questa sessione per segnarla come fidata e darle l\'accesso ai messaggi cifrati. Se non hai fatto l\'accesso a questa sessione il tuo account potrebbe essere compromesso: @@ -2197,7 +2197,7 @@ Gli altri utenti potrebbero non fidarsi Completa la sicurezza - Apri una sessione esistente e usala per verificare questa, dandole l\'accesso ai messaggi cifrati. Se non riesci ad accedere a nessuna, usa la tua chiave o password di recupero. + Apri una sessione esistente e usala per verificare questa, dandole l\'accesso ai messaggi cifrati. Verifica @@ -2209,7 +2209,7 @@ Fidato Non fidato - Questa sessione è fidata per i messaggi sicuri perchè %1$s (%2$s) l\'ha verificata: + Questa sessione è fidata per i messaggi sicuri perché %1$s (%2$s) l\'ha verificata: %1$s (%2$s) ha fatto l\'accesso con una nuova sessione: Finché questo utente non si fida di questa sessione, i messaggi inviati da e verso di essa sono etichettati con avvisi. In alternativa, puoi verificarlo manualmente. @@ -2219,9 +2219,49 @@ Codice QR - L\'altro utente ha scansionato correttamente il codice QR\? No La connessione al server è stata persa + Nome utente + Strumenti Svil + Dati account + + %d voto + %d voti + + + %d voto - Risultato finale + %d voti - Risultato finale + + Opzione selezionata + Crea un semplice sondaggio + Non puoi accedere ad una sessione esistente\? + Usa la chiave di recupero o la password + + Nuovo accesso + + Impossibile trovare segreti nell\'archivio + Inserisci la password dell\'archivio segreto + Attenzione: + Dovresti accedere all\'archivio segreto solo da un dispositivo fidato + Accedi alla cronologia dei messaggi sicuri e all\'identità di firma incrociata per verificare altre sessioni inserendo la tua password + + Rimuovi… + Vuoi inviare questo allegato a %1$s\? + + Invia immagine nella dimensione originale + Invia immagini nella dimensione originale + + + Conferma rimozione + Sei sicuro di volere rimuovere (eliminare) questo evento\? Nota che se elimini il nome della stanza o cambi l\'argomento, ciò potrebbe annullare la modifica. + Includi un motivo + Motivo della revisione + + Evento eliminato da un utente, motivo: %1$s + Evento moderato da un admin della stanza, motivo: %1$s + + Le chiavi sono già aggiornate! + diff --git a/vector/src/main/res/values-ja/strings.xml b/vector/src/main/res/values-ja/strings.xml index c44c400208..6507c31620 100644 --- a/vector/src/main/res/values-ja/strings.xml +++ b/vector/src/main/res/values-ja/strings.xml @@ -1071,4 +1071,10 @@ Matrixでのメッセージの可視性は電子メールと同様です。メ アルゴリズム 署名 + 通知に関する問題の解決 + システム設定 + アカウント設定 + カスタム設定 + 起動時の実行 + バックグラウンド制限の確認 diff --git a/vector/src/main/res/values-nb-rNO/strings.xml b/vector/src/main/res/values-nb-rNO/strings.xml new file mode 100644 index 0000000000..54cac5a112 --- /dev/null +++ b/vector/src/main/res/values-nb-rNO/strings.xml @@ -0,0 +1,726 @@ + + + nb + NO + Latn + + Lyst tema + Mørkt tema + Svart tema + Klargjør tjenesten + Synkroniserer … + Meldinger + Rom + Innstillinger + Medlemsdetaljer + Historisk + Feilrapport + Send et klistremerke + Er du sikker\? + Laster… + + OK + Lukk + Lagre + Forlat + Send + Kopier + Send på nytt + Fjern + Sitering + Nedlastning + Del + Tøm + Senere + Neste + Permalenke + Vis kilden + Slett + Endre navn + Ingen + Tilbakekall + Koble fra + Stemme + Film + eller + Inviter + Frakoblet + Godta + Hopp over + Fullført + Løp vekk + Ignorer + Gjennomgang + Avslå + + Avslutt + Handlinger + Logg ut + Historisk + Lukk + Kopiert til utklippstavle + Slå av + + Advarsel + Feil + + Hjem + Folk + Rom + Samfunn + + Invitasjoner + Lavprioritet + Ingen treff + Rom + Ingen rom + Inviter + Samfunn + Ingen grupper + + Meld fra om en bug + Fremgang (%s%%) + + Les + + Bli med i rommet + Brukernavn + Opprett konto + Logg inn + Logg ut + Identitetstjener-URL + Søk + + Ta et bilde + Spill inn en video + + Logg inn + Opprett konto + Send + Hopp over + Gå tilbake til påloggingsskjermen + Passord + Nytt passord + E-postadresse + Telefonnummer + Ugyldig token + Glemt passord\? + Jeg har verifisert E-postadressen min + Vennligst skriv inn en gyldig URL + Mobil + + Original + Store + Medium + Små + + I går + I dag + + Informasjon + Lagret + JA + NEI + Fortsett + + Fjern + Bli med + Forhåndsvisning + Avvis + + Synkroniserer … + + 1sek + %dsek + + + 1m + %dm + + + 1t + %dt + + + 1d + %dd + + + Lag + + Tilkoblet + Frakoblet + Rolig + Inviter + Utesteng + Opphev utestengelse + Spark ut + %1$s %2$s + + Søk + Filen ble ikke funnet + Stol på + Logg ut + Ignorer + Folk + Filer + Instillinger + BLE MED + + Avbryt opplastning + Avbryt nedlasting + + Ingen treff + ROM + ROM + Demp + Direktesamtale + Glem + Meldinger + Instillinger + Versjon + Versjn %s + Opphavsrettighet + Profilbilde + Visningsnavn + E-post + Telefon + Åpne Innstillinger + + Slå på + + Slå på + + Vanlig + Varslingslyd + sekunder + + Versjon + Opphavsrettighet + Retningslinjer for personvern + Tøm mellomlageret + Varsler + Annet + Avansert + Kryptografi + Lokale kontakter + Sesjoner + Send varsler om at du skriver + Analyser + Send analytiske data + Ja, jeg vil hjelpe til! + + ID + Offentlig navn + Sist sett + Autentisering + Passord: + Send + + Logget inn som + Identitetstjener + Brukergrensesnitt + Språk + Passord + Endre passord + Nåværende passord + Nytt passord + Bekreft nytt passord + Passordene er ikke like + + Land + Telefonnummer + Kode + Media + Velg + Velg + Merkeskilt + 3 dager + 1 uke + 1 måned + For alltid + + Emne + Lavprioritet + Ingen + + Varsler + Alle + Bannlyste brukere + + Avansert + Adresser + Mappe + Tema + + Hendelsesinformasjon + Bruker-ID + Algoritme + Økt-ID + Det offentlige navnet til en økt er synlig for folkene du kommuniserer med + ID + Øktnøkkel + Verifisering + Eksporter + Importer + Svartelistet + + ingen + + Bekreft + Svarteliste + Hjemmetjener-URL + Skriv her … + + Rom + Meg + Skriftstørrelse + Små + Vanlig + Store + Større + Det største + Enorm + + Åpne i nettleser + Din bruker-ID + Ditt tema + Modul-ID + Rom-ID + + + Tillat + Bekreft + Del + Ignorer + + Av + Lag + Eksempel + eksempel + + Hjem + Folk + Ble med + Invitert + Bli med igjen + Glem rommet + + Profilbilde + + Deaktiver kontoen + Deaktiver kontoen + + Vennligst skriv inn et brukernavn. + utvid + skjul + + Alltid + %1$s: + %1$s: %2$s + Aldri mist krypterte beskjeder + Fullført + Del + Lagre som fil + Erstatt + Stopp + + Uventet feil + Er du sikker\? + Aldri mist krypterte beskjeder + Aldri mist krypterte beskjeder + Versjon + Algoritme + Signatur + + Verifisert! + Jeg forstår + + Verifiseringsforespørsel + Ukjent feil + + Rediger + Svar + + Prøv igjen + Invitert av %s + + Reaksjoner + Like + Reaksjoner + + Endre + Vennligst vent … + Nytt rom + Offentlig + Generelt + Brukervalg + Sikkerhet og personvern + Ekspert + Format: + + Stemme og video + Hjelp/Om + + + Vis skjulte hendelser i tidslinjen + + Venter … + (redigert) + + Vilkår for bruk + Bytt ut identitetstjener + Venter + + Opprett et nytt rom + Vis passord + Skjul passord + Fil + Kontakt oss + Kamera + Lyd + Galleri + Demp + Instillinger + Lær mer + Annet + Fortsett + Registrer deg + Logg inn + Fortsett med SSO + + Neste + E-post + Nytt passord + + Fortsett + + Jeg har verifisert E-postadressen min + + Suksess! + Passordet ditt har blitt tilbakestilt. + Advarsel + E-post + E-post (valgfritt) + Neste + + Telefonnummer + Neste + + Skriv inn kode + Neste + + Brukernavn + Passord + Neste + Advarsel + Vennligst sjekk eposten din + Sett av + + Logg på + Logg på + Passord + Instillinger + Gjeldende økt + Føyer til ¯\\_(ツ)_/¯ på en råtekstmelding + + Video. + Bilde. + Lyd + Fil + + Du avbrøt + Du aksepterte + Verifiseringsforespørsel + + + Du + + Verifiser %s + Verifiserte %s + Sikkerhet + Lær mer + Mer + Varsler + Opplastinger + Ordstyrere + Tilpasset + Invitasjoner + Brukere + + Opphev ignorering + + Tidslinje + + Vil du skru på kryptering\? + Bekreft + Advarsel + + Sesjoner + Ja + Advarsel: + Bekreft fjerning + Status.im-tema + + Lytter etter hendelser + Verifiser økten + + Aktiv samtale + Send loggbøker + Send tilbakestillings-E-post + Brukernavnet er i bruk + Hjemmetjener: + Identitetstjener: + Samtaler + + 1 medlem + %d medlemmer + + 1 medlem + + %1$s nå + %s skriver … + Søk + Filtrer rommets medlemmer + + 1 rom + %d rom + + Alle meldinger + Kun nevninger + olm-versjon + Deaktiver kontoen + Riot samler inn anonyme statistikker for å hjelpe oss med å forbedre programmet. + %1$s @ %2$s + Integreringsbehandler + + Rommets navn + Hvem kan lese historikken\? + Hvem kan gå inn i dette rommet\? + + Kun medlemmer (f.o.m. da denne innstillingen ble valgt) + Kun medlemmer (f.o.m. da de ble invitert) + Kun medlemmer (f.o.m. de ble med) + + Kun folk som har blitt invitert + Dette rommet viser ikke merkeskilter for noen samfunn + Kopier rommets ID + Eksporter E2E-romnøkler + Eksporter romnøkler + Importer E2E-romnøkler + Importer romnøkler + Verifisert + Av-verifiser + Verifiser økten + + 1 rom + %d rom + + + + %1$s: 1 melding + %1$s: %2$d meldinger + + + %d varsel + %d varsler + + + Ny hendelse + Nye meldinger + Ny invitasjon + Bitteliten + + 1 aktiv modul + %d aktive moduler + + + + Modul + Last inn modul + Ditt visningsnavn + Din avatars URL + Legg til Matrix-apper + Begynn verifisering + Advarsel! + Kommandofeil + Forlat rommet + Velg rommets tema + Stille + Bråkete + + Kryptert melding + + Opprett et samfunn + Samfunnsnavn + Samfunns-ID + Rom + Ingen brukere + + Rom + + 1 medlem + %d medlemmer + + + + 1 rom + %d rom + + Årsak: %1$s + For å gå videre, vennligst skriv inn passordet ditt: + +%d + %d+ + Begynn å bruke Nøkkelsikkerhetskopiering + Det var meg + Begynn å bruke Nøkkelsikkerhetskopiering + + Sendte deg en invitasjon + Rom + Opprett et nytt rom + Rom + Direktemeldinger + + Rommets navn + Velkommen til betaen! + URL: + Direktemeldinger + + Meldingsredigeringer + Filtrer samtaler … + Opprett et nytt rom + Blir med i rommet … + + Identitetstjener + Skriv inn en ny identitetstjener + Send vedlegg + + Klistremerke + Det er søppelpost + Alle meldinger + Kun nevninger + Forlat rommet + Uleste meldinger + + Kom i gang + + Velg en tjener + Tilbakestill passordet på %1$s + Advarsel! + Velg en E-postadresse + Velg et telefonnummer + Godkjenn vilkårene for å fortsette + + Avanserte innstillinger + Utviklermodus + Dersom dette først har blitt skrudd på, kan kryptering aldri bli skrudd av. + + De samsvarer + Ikke sikker + Venter … + Verifiser denne økten + QR-kodebilde + + Forlat rommet + Forlater rommet … + + Dersom dette først har blitt skrudd på, kan kryptering aldri bli skrudd av. + + Aktive økter + Vis alle økter + Behandle økter + Verifiser denne økten + Verifisert + Betrodd + Ikke betrodd + + QR-kode + + Nei + + Utviklerverktøy + Ny innlogging + + Fjern … + Bråkete notifikasjoner + Stille notifikasjoner + + Samfunnsdetaljer + Sikkerhetskopiering av nøkler + Bruk sikkerhetskopiering av nøkler + nøkkelbackup er ikke fulllført, vennligst vent… + du kommer til å miste dine enkrypterte meldinger hvis du logger ut nå + Sikkerhetskopiering av nøkler pågår. Hvis du logger ut nå mister du tilgang til dine enkrypterte meldinger. + Sikker sikkerhetskopiering av nøkler burde være aktivt på alle øktene dine for å unngå å miste tilgang til dine enkrypterte meldinger. + Jeg vil ikke ha mine enkrypterte meldinger + Sikkerhetskopierer nøkler… + Bruk sikkerhetskopiering av nøkler + Sikkerhetskopi + Du kommer til å miste tilgang til dine enkrypterte meldinger med mindre du sikkerhetskopierer nøklene dine før du logger av. + + Tredjepartslinsenser + + Bli + Snakk + Se dekryptert kilde + Rapporter innhold + Pågående konferansesamtale. +\nBli med som %1$s eller %2$s + Kan ikke starte samtalen, vennligst prøv igjen senere + På grunn av manglende tillatelse, kan noen funksjoner mangle… + Denne handlingen er ikke mulig på grunn av manglende tillatelser. + Du må ha tillatelse til å invitere for å starte en konferanse i dette rommet + Kan ikke starte samtale + Informasjon om Økten + Konferansesamtaler er ikke støttet i enkrypterte rom + Ring likevel. + Send likevel + Er du sikker på at vil logge ut\? + Telefonsamtale + Videosamtale + Globalt søk + Marker alle som lest + Raskt svar + Marker som lest + Åpne + Bekreftelse + Favoritter + Filtrer romnavn + Filtrer favoritter + Filtrer folk + Filtrer romnavn + Filtrer samfunnsnavn + + Systemadvarsler + + Samtaler + Lokal adressebok + Brukerkatalog + Bare matrix-kontakter + Ingen samtaler + Du ga ikke Riot tilgang til dine lokale kontakter + Ingen identitetsserver konfigurert. + + Romkatalog + Ingen offentlige rom tilgjengelig + + 1 bruker + %d brukere + + + Send kjæsjlogg + Send skjermbilde + Vennligst forklar feilen. Hva gjorde du\? Hva forventet du at skulle skje\? Hva skjedde i stedet\? + Om mulig, vennligst beskriv på engelsk. + Forklar problemet ditt her + For å diagnotisere feil, logger fra denne klienten vil bli sendt med denne feilmeldingen. Feilmeldingen, som inkluderer loggene og skjermdumper, blir ikke offentlig synlig. Hvis du foretrekker å bare sende teksten over, vennligst fjern merkene fra boksene: + Det ser ut som du rister på telefonen i frustrasjon. Har du list til å åpne feilrapporteringsskjermen\? + Applikasjonen kræsjet sist gang. Har du lyst til å åpne kræsjskjermen\? + Sinnarist for å rapportere feil + + Feilrapport har blitt sendt + feilrapporten feilet å sendes (%s) + Send inn i + Hjemmetjener URL + Start ny chat + diff --git a/vector/src/main/res/values-nl/strings.xml b/vector/src/main/res/values-nl/strings.xml index bacc675f86..eac681e604 100755 --- a/vector/src/main/res/values-nl/strings.xml +++ b/vector/src/main/res/values-nl/strings.xml @@ -40,7 +40,7 @@ Sommige functies zijn misschien afwezig wegens ontbrekende rechten… Om een vergadering in dit groepsgesprek te starten heeft u uitnodigingsrechten nodig Kan de oproep niet starten - Apparaatinformatie + Sessie-informatie Vergadergesprekken worden niet ondersteund in versleutelde gesprekken Toch sturen of @@ -174,9 +174,9 @@ Er moet een nieuw wachtwoord ingevoerd worden. Er is een e-mail verstuurd naar %s. Klik hieronder zodra u de koppeling in de e-mail hebt bezocht. Verifiëren van het e-mailadres is mislukt: zorg dat u op de koppeling in de e-mail hebt geklikt - Uw wachtwoord is opnieuw ingesteld. -\n -\nU bent op alle apparaten afgemeld en zult niet langer pushmeldingen ontvangen. Om meldingen opnieuw in te schakelen, meldt u zich op elk apparaat opnieuw aan. + Uw wachtwoord is opnieuw ingesteld. +\n +\nU bent op alle sessies afgemeld en zult niet langer pushmeldingen ontvangen. Om meldingen opnieuw in te schakelen, meldt u zich op elk apparaat opnieuw aan. URL moet met http[s]:// beginnen @@ -196,8 +196,8 @@ Er is nog niet op de koppeling in de e-mail geklikt - U moet zich opnieuw aanmelden om de sleutels voor eind-tot-eind-versleuteling voor dit apparaat te genereren, en om de publieke sleutel naar uw thuisserver te sturen. -\nDit is eenmalig. + U moet zich opnieuw aanmelden om de sleutels voor eind-tot-eind-versleuteling voor deze sessie te genereren, en om de publieke sleutel naar uw thuisserver te sturen. +\nDit is eenmalig. \nExcuses voor het ongemak. @@ -261,12 +261,11 @@ Riot heeft toegang nodig tot uw camera en microfoon om video-oproepen te maken. \n \nVerleen toegang op de volgende pop-ups om de oproep te maken. - Riot heeft toegang nodig tot uw adresboek om andere Matrix-gebruikers te vinden aan de hand van hun e-mailadressen en telefoonnummers. -\n -\nVerleen toegang op de volgende pop-up om gebruikers op Riot te ontdekken via uw adresboek. - Riot heeft toegang nodig tot uw adresboek om andere Matrix-gebruikers te vinden aan de hand van hun e-mailadressen en telefoonnummers. -\n -\nRiot toegang verlenen tot uw contacten\? + Riot kan uw adresboek benaderen om andere Matrix-gebruikers te vinden aan de hand van hun e-mailadressen en telefoonnummers. +\nAls u het goed vindt om uw adresboek hiervoor te delen, verleen dan toegang op de volgende pop-up. + Riot kan uw adresboek gebruiken om andere Matrix-gebruikers te vinden aan de hand van hun e-mailadressen en telefoonnummers. +\n +\nWilt u uw adresboek hiervoor delen\? Sorry. De actie is niet toegepast vanwege ontbrekende rechten @@ -312,7 +311,7 @@ BEHEERDERSGEREEDSCHAPPEN BELLEN TWEEGESPREKKEN - APPARATEN + SESSIES Uitnodigen Dit gesprek verlaten @@ -326,7 +325,7 @@ Alle berichten van deze gebruiker tonen Gebruikers-ID, naam of e-mailadres Vermelden - Lijst met apparaten weergeven + Sessielijst weergeven U kunt deze veranderingen niet ongedaan maken aangezien u de gebruiker tot hetzelfde niveau als uzelf promoveert. \nWeet u het zeker\? @@ -350,7 +349,7 @@ Verstuur een versleuteld bericht… Verstuur een bericht (niet versleuteld)… Berichten zijn niet verstuurd. Nu %1$s of %2$s\? - Berichten zijn niet verstuurd omdat er onbekende apparaten aanwezig zijn. Nu %1$s of %2$s? + Berichten zijn niet verstuurd omdat er onbekende sessies aanwezig zijn. Nu %1$s of %2$s\? alles opnieuw versturen alles annuleren Onverstuurde berichten opnieuw versturen @@ -442,7 +441,7 @@ App-informatie Meldingen voor deze account inschakelen - Meldingen voor dit apparaat inschakelen + Meldingen voor deze sessie inschakelen Het scherm voor 3 seconden aanzetten Berichten in één-op-één-gesprekken @@ -454,7 +453,7 @@ Synchronisatie in de achtergrond Achtergrondssynchronisatie inschakelen Synchronisatieverzoek is verlopen - Pauze tussen elk verzoek + Pauze tussen elk synchronisatie seconde seconden @@ -481,11 +480,11 @@ Startscherm Gesprekken met gemiste meldingen vastprikken Gesprekken met ongelezen berichten vastprikken - Apparaten - Apparaatinformatie + Sessies + Sessie-informatie ID - Naam - Apparaatnaam + Publieke naam + Publieke naam bijwerken Laatst gezien %1$s @ %2$s @@ -579,8 +578,8 @@ Eind-tot-eind-versleuteling Om de versleuteling in te schakelen dient u zich eerst af te melden. Eind-tot-eind-versleuteling is actief - Alleen naar geverifieerde apparaten versleutelen - Ongeverifieerde apparaten in dit gesprek nooit berichten sturen vanaf dit apparaat. + Alleen naar geverifieerde sessies versleutelen + Ongeverifieerde sessies in dit gesprek nooit berichten sturen vanaf deze sessie. Dit gesprek heeft geen lokale adressen @@ -618,11 +617,11 @@ Sessie-ID Ontsleutelingsfout - Informatie over apparaat van afzender - Apparaatnaam - Naam - Apparaats-ID - Apparaatssleutel + Informatie over sessie van afzender + Publieke naam + Publieke naam + ID + Sessiesleutel Verificatie Ed25519-vingerafdruk @@ -640,14 +639,14 @@ Gesprekssleutels importeren Importeer de sleutels uit een lokaal bestand Importeren - Enkel naar geverifieerde apparaten versleutelen - Versleutelde berichten nooit naar ongeverifieerde apparaten sturen vanaf dit apparaat. + Enkel naar geverifieerde sessies versleutelen + Versleutelde berichten nooit naar ongeverifieerde sessies sturen vanaf deze sessie. NIET geverifieerd Geverifieerd Geblokkeerd - onbekend apparaat + onbekende sessie geen Verifiëren @@ -655,9 +654,9 @@ Blokkeringslijst Deblokkeringslijst - Apparaat verifiëren - Om te verifiëren dat dit apparaat vertrouwd kan worden, contacteert u de eigenaar via een andere methode (bv. persoonlijk of via een telefoontje) en vraagt u hem/haar of de sleutel die hij/zij ziet in zijn/haar Gebruikersinstellingen van dit apparaat overeenkomt met de sleutel hieronder: - Als het overeenkomt, drukt u op de knop ‘Verifiëren’ hieronder. Als het niet overeenkomt, dan onderschept iemand anders het apparaat en zou u het beter blokkeren. In de toekomst zal dit verificatieproces verbeterd worden. + Sessie verifiëren + Om te verifiëren dat deze sessie vertrouwd kan worden, contacteert u de eigenaar via een andere methode (bv. persoonlijk of via een telefoontje) en vraagt u hem/haar of de sleutel die hij/zij ziet in zijn/haar Gebruikersinstellingen van deze sessie overeenkomt met de sleutel hieronder: + Als het overeenkomt, drukt u op de knop ‘Verifiëren’ hieronder. Als het niet overeenkomt, dan onderschept iemand anders deze sessie en zou u het beter blokkeren. In de toekomst zal dit verificatieproces verbeterd worden. Ik verifieer dat de sleutels overeenkomen Riot ondersteunt nu eind-tot-eind-versleuteling, maar u moet zich opnieuw aanmelden om het in te schakelen. @@ -665,12 +664,12 @@ \nU kunt dit nu of later doen vanuit de app-instellingen. - Dit gesprek bevat onbekende apparaten - Dit gesprek bevat onbekende apparaten die niet geverifieerd zijn. -\nDit betekent dat er geen garantie is dat de apparaten bij de gebruikers horen waartoe ze beweren te horen. -\nWe raden u aan om bij elk apparaat door het verificatieprocces heen te gaan voordat u verdergaat, maar u kunt het bericht ook zonder te verifiëren opnieuw versturen. + Dit gesprek bevat onbekende sessies + Dit gesprek bevat onbekende sessies die niet geverifieerd zijn. +\nDit betekent dat er geen garantie is dat de sessies bij de gebruikers horen waartoe ze beweren te horen. +\nWe raden u aan om bij elke sessie door het verificatieprocces heen te gaan voordat u verdergaat, maar u kunt het bericht ook zonder te verifiëren opnieuw versturen. \n -\nOnbekende apparaten: +\nOnbekende sessies: Kies een gesprekscatalogus @@ -757,8 +756,8 @@ Systeemcamera gebruiken - U heeft een nieuw apparaat ‘%s’ toegevoegd, dat versleutelingssleutels aanvraagt. - Uw ongeverifieerde apparaat ‘%s’ vraagt versleutelingssleutels aan. + U heeft een nieuwe sessie ‘%s’ toegevoegd, die versleutelingssleutels aanvraagt. + Uw ongeverifieerde sessie ‘%s’ vraagt versleutelingssleutels aan. Verificatie starten Delen zonder te verifiëren Verzoek negeren @@ -920,7 +919,7 @@ Mijn account deactiveren Meldingsprivacy - Riot kan in de achtergrond werken om uw meldingen veilig en privé te beheren. Dit beïnvloedt mogelijk het accuverbruik. + Riot kan op de achtergrond werken om uw meldingen veilig en privé te beheren. Dit beïnvloedt mogelijk het accuverbruik. Toestemming verlenen Kies een andere optie @@ -948,12 +947,12 @@ Downloaden Inspreken - Beveiligingssleutels van uw apparaten opnieuw aanvragen. + Beveiligingssleutels van uw sessies opnieuw aanvragen. Sleutelaanvraag verstuurd. Aanvraag verstuurd - Start Riot op een ander apparaat dat het bericht kan ontsleutelen, zodat het de sleutels naar dit apparaat kan sturen. + Start Riot op een ander apparaat dat het bericht kan ontsleutelen, zodat het de sleutels naar deze sessie kan sturen. Typ hier… @@ -1091,7 +1090,7 @@ Sleutelback-up is nog niet klaar, even geduld… Indien u zich nu afmeldt, zult u uw versleutelde berichten verliezen Sleutelback-up is bezig. Indien u zich nu afmeldt, zult u de toegang tot uw versleutelde berichten verliezen. - Veilige sleutelback-up dient actief te zijn op al uw apparaten om de toegang tot uw versleutelde berichten niet te verliezen. + Veilige sleutelback-up dient actief te zijn op al uw sessies om de toegang tot uw versleutelde berichten niet te verliezen. Ik wil mijn versleutelde berichten niet Sleutels worden geback-upt… Sleutelback-up gebruiken @@ -1136,9 +1135,9 @@ \nGelieve de accountinstellingen te controleren. Inschakelen - Apparaatinstellingen. - Meldingen zijn ingeschakeld voor dit apparaat. - Meldingen zijn niet toegestaan voor dit apparaat. + Sessie-instellingen. + Meldingen zijn ingeschakeld voor deze sessie. + Meldingen zijn niet ingeschakeld voor deze sessie. \nGelieve de Riot-instellingen te controleren. Inschakelen @@ -1378,14 +1377,14 @@ De back-up kan met deze herstelsleutel niet ontsleuteld worden: controleer of u de juiste herstelsleutel heeft ingevoerd. Back-up hersteld %s! - %1$d sessiesleutels hersteld, en %2$d nieuwe sleutel(s) die dit apparaat nog niet kende toegevoegd + %1$d sessiesleutels hersteld, en %2$d nieuwe sleutel(s) die deze sessie nog niet kende toegevoegd Back-up met %d sleutel hersteld. Back-up met %d sleutels hersteld. - Er is %d nieuwe sleutel toegevoegd aan dit apparaat. - Er zijn %d nieuwe sleutels toegevoegd aan dit apparaat. + Er is %d nieuwe sleutel toegevoegd aan deze sessie. + Er zijn %d nieuwe sleutels toegevoegd aan deze sessie. Verkrijgen van laatste herstelsleutelversie (%s) mislukt. @@ -1395,19 +1394,19 @@ Herstellen uit back-up Back-up verwijderen - Sleutelback-up is correct ingesteld voor dit apparaat. - Sleutelback-up is niet actief op dit apparaat. - Uw sleutels worden niet geback-upt vanaf dit apparaat. + Sleutelback-up is correct ingesteld voor deze sessie. + Sleutelback-up is niet actief op deze sessie. + Uw sleutels worden niet geback-upt vanaf deze sessie. - De back-up heeft een ondertekening van een onbekend apparaat met ID %s. - De back-up heeft een geldige ondertekening van dit apparaat. - De back-up heeft een geldige ondertekening van het geverifieerde apparaat %s. - De back-up heeft een geldige ondertekening van het ongeverifieerde apparaat %s - De back-up heeft een ongeldige ondertekening van het geverifieerde apparaat %s - De back-up heeft een ongeldige ondertekening van het ongeverifieerde apparaat %s + De back-up heeft een ondertekening van een onbekende sessie met ID %s. + De back-up heeft een geldige ondertekening van deze sessie. + De back-up heeft een geldige ondertekening van de geverifieerde sessie %s. + De back-up heeft een geldige ondertekening van de ongeverifieerde sessie %s + De back-up heeft een ongeldige ondertekening van de geverifieerde sessie %s + De back-up heeft een ongeldige ondertekening van de ongeverifieerde sessie %s Verkrijgen van vertrouwensinformatie voor back-up mislukt (%s). - Herstel nu met uw wachtwoord of herstelsleutel om sleutelback-up op dit apparaat te gebruiken. + Herstel nu met uw wachtwoord of herstelsleutel om sleutelback-up op deze sessie te gebruiken. Back-up wordt verwijderd… Verwijderen van back-up is mislukt (%s) @@ -1448,17 +1447,17 @@ Sorry, vergadergesprekken met Jitsi worden nog niet ondersteund op oudere apparaten (met een Android-versie lager dan 5.0) - Apparaat verifiëren + Sessie verifiëren onbekend IP-adres - Een nieuw apparaat vraagt versleutelingssleutels aan. -\nApparaatnaam: %1$s -\nLaatst gezien: %2$s -\nAls u zich niet heeft aangemeld op een ander apparaat, negeer dan dit verzoek. - Een ongeverifieerd apparaat vraagt versleutelingssleutels aan. -\nApparaatnaam: %1$s -\nLaatst gezien: %2$s -\nAls u zich niet heeft aangemeld op een ander apparaat, negeer dan dit verzoek. + Een nieuwe sessie vraagt versleutelingssleutels aan. +\nSessienaam: %1$s +\nLaatst gezien: %2$s +\nAls u zich niet heeft aangemeld op een andere sessie, negeer dan dit verzoek. + Een ongeverifieerde sessie vraagt versleutelingssleutels aan. +\nSessienaam: %1$s +\nLaatst gezien: %2$s +\nAls u zich niet heeft aangemeld op een andere sessie, negeer dan dit verzoek. Verifiëren Delen @@ -1469,18 +1468,18 @@ Voor een maximale beveiliging bevelen we aan om dit onder vier ogen te doen, of via een ander vertrouwd communicatiekanaal. Verificatie beginnen Inkomend verificatieverzoek - Verifieer dit apparaat door het als vertrouwd te markeren. Door de apparaten van uw gesprekspartners te vertrouwen, hoeft u zich nog minder zorgen te maken over het gebruik van eind-tot-eind-versleutelde berichten. - Dit apparaat verifiëren zal het als vertrouwd markeren, en het ook aan uw gesprekspartner als vertrouwd markeren. + Verifieer de sessie door deze als vertrouwd te markeren. Door de sessie van uw gesprekspartners te vertrouwen, hoeft u zich nog minder zorgen te maken over het gebruik van eind-tot-eind-versleutelde berichten. + De sessie verifiëren zal deze als vertrouwd markeren, en deze ook aan uw gesprekspartner als vertrouwd markeren. - Verifieer dit apparaat door te bevestigen dat de volgende emoticons op het scherm van uw gesprekspartner verschijnen - Verifieer dit apparaat door te bevestigen dat de volgende cijfers op het scherm van uw gesprekspartner verschijnen + Verifieer deze sessie door te bevestigen dat de volgende emoticons op het scherm van uw gesprekspartner verschijnen + Verifieer deze sessie door te bevestigen dat de volgende cijfers op het scherm van uw gesprekspartner verschijnen U heeft een inkomend verificatieverzoek ontvangen. Verzoek bekijken Wachten op bevestiging van gesprekspartner… Geverifieerd! - U heeft het apparaat geverifieerd. + U heeft de sessie geverifieerd. Beveiligde berichten met deze gebruiker worden eind-tot-eind-versleuteld en kunnen niet door derde partijen gelezen worden. Ik snap het @@ -1494,17 +1493,17 @@ De verificatie is geannuleerd. \nReden: %s - Interactieve apparaatsverificatie + Interactieve sessieverificatie Verificatieverzoek - %s wil uw apparaat verifiëren + %s wil uw sessie verifiëren De gebruiker heeft de verificatie geannuleerd Het verificatieproces is verlopen - Het apparaat heeft geen weet van die transactie - Het apparaat kan geen sleutelovereenkomst-, hash-, MAC- of SAS-methode kiezen + De sessie heeft geen weet van die transactie + De sessie kan geen sleutelovereenkomst-, hash-, MAC- of SAS-methode kiezen De hashovereenkomst kwam niet overeen De SAS kwam niet overeen - Het apparaat heeft een onverwacht bericht ontvangen + De sessie heeft een onverwacht bericht ontvangen Er is een ongeldig bericht ontvangen Sleutels komen niet overeen Gebruikers komen niet overeen @@ -1519,4 +1518,91 @@ Geen identiteitsserver geconfigureerd. Oproep mislukt door verkeerd geconfigureerde server + Vraag de beheerder van uw thuisserver (%1$s) om een TURN-server te configureren om oproepen betrouwbaar te doen werken. +\n +\nAls alternatief kunt u de publieke server op %2$s gebruiken. Dit is minder betrouwbaar en zal tevens uw IP-addres delen met die server. U kunt dit ook configureren in de Instellingen. + Probeer %s te gebruiken + Vraag het niet opnieuw + + Kies een e-mailadres om te gebruiken voor accountherstel. Later kunt u ervoor kiezen om mensen u te laten vinden via uw e-mailadres. + Kies een telefoonnummer. Later kunt u ervoor kiezen om mensen u te laten vinden via dit nummer. + Kies een e-mailadres om te gebruiken voor accountherstel. Later kunt u ervoor kiezen om mensen u te laten vinden via uw e-mailadres of telefoonnummer. + Kies een e-mailadres om te gebruiken voor accountherstel. Later kunt u ervoor kiezen om mensen u te laten vinden via uw e-mailadres of telefoonnummer. + Dit is geen geldig Matrix-serveradres + Kan geen verbinding maken met een thuisserver op deze URL, controleer de URL + Zal %s gebruiken om te assisteren in het geval dat uw thuisserver er niet over beschikt (uw IP-adres zal tijdens een oproep gedeeld worden) + Voeg een identiteitsserver toe in de instellingen om dit te doen. + Bevestig uw wachtwoord + U kunt dit niet doen vanaf de mobiele Riot + Synchroniseren op de achtergrond (experimenteel) + Geoptimaliseerd voor batterij + Riot zal op een batterijzuinige manier synchroniseren op de achtergrond. +\nAfhankelijk van de staat van uw apparaat kan het besturingssysteem de synchronisatie uitstellen. + Geoptimaliseerd voor snelheid + Riot zal periodiek op de achtergrond synchroniseren (configureerbaar). +\nDit heeft een negatieve impact op uw batterij- en datagebruik. Er zal een melding getoond worden ter informatie. + Geen achtergrondssynchronisatie + U zal geen melding van berichten ontvangen als de app zich in de achtergrond bevindt. + Kon de instellingen niet bijwerken. + + + Voorkeur voor synchronisatie-interval + %s +\nDe synchronisatie is mogelijk uitgesteld als gevolg van de batterij of staat van uw apparaat (slaapmodus). + Integraties + Latn + + Terugvaloproepassistentieserver toestaan + Authenticatie vereist + + + Gebruik een integratiebeheerder om bots, bruggen, widgets en stickerpakketten te beheren. +\nIntegratiebeheerders ontvangen configuratiedata en kunnen widgets aanpassen, gespreksuitnodigingen versturen en bestuursniveaus instellen namens u. + Ontdekken + Beheer uw ontdekinstellingen. + Integraties toestaan + Integratiebeheerder + + Publieke naam (zichtbaar voor mensen met wie u communiceert) + De publieke naam van een sessie is zichtbaar voor mensen met wie u communiceert + Widget + Widget laden + Deze widget is toegevoegd door: + Dit gebruiken kan cookies toevoegen en gegevens delen met %s: + Dit gebruiken kan gegevens delen met %s: + Kon widget niet laden. +\n%s + Widget herladen + Openen in browser + Toegang intrekken voor mij + + Uw weergavenaam + Uw profielfoto-URL + Uw gebruikers-ID + Uw thema + Widget-ID + Gespreks-ID + + + Deze widget wil gebruik maken van de volgende bronnen: + Toestaan + Alles blokkeren + Camera gebruiken + Microfoon gebruiken + DRM-beschermde media lezen + + Geen integratiebeheerder ingesteld. + Om verder te gaan dient u de dienstvoorwaarden te aanvaarden. + + Er bestaat al een back-up op uw thuisserver + Het lijkt erop dat u al een back-up van uw herstelsleutel heeft uit een andere sessie. Wilt u deze vervangen door degene die u nu aanmaakt\? + Vervangen + Stoppen + + Back-upstatus wordt gecontroleerd + U bent afgemeld vanwege onjuiste of verlopen gebruikersreferenties. + + U gebruikt geen identiteitsserver + Er is geen identiteitsserver geconfigureerd. Dit is vereist om uw wachtwoord opnieuw in te stellen. + diff --git a/vector/src/main/res/values-nn/strings.xml b/vector/src/main/res/values-nn/strings.xml index 44755a1ef0..aad1451984 100644 --- a/vector/src/main/res/values-nn/strings.xml +++ b/vector/src/main/res/values-nn/strings.xml @@ -210,7 +210,7 @@ Fekk ikkje til å stadfesta e-postadressa: sjå til at du klikka på lenken i e-posten Passordet ditt vart nullstilt. Du vart logga ut av alle sesjonar og får ikkje push-varsel lenger. For å skru varsel på att, logg inn att på kvar eining. - URL-en må byrja på http[s]:// + URL-en må starta på http[s]:// Fekk ikkje til å logga inn: Nettverksfeil Fekk ikkje til å logga inn Fekk ikkje til å registrere: Nettverksfeil @@ -433,7 +433,7 @@ Søk - Filtrer rommedlemer + Filtrer rommedlemmar Ingi treff ROM MELDINGAR @@ -446,13 +446,13 @@ ROM LÅGRETT INNBJODINGAR - Byrja samtala + Start samtale Laga eit rom Vert med i romet Vert med i eit rom Skriv inn ein rom-ID eller eit romalias - Sjå gjenom utvalet + Bla gjennom katalog 1 rom %d rom @@ -511,10 +511,10 @@ Meldingar i ein-og-ein-samtalar Meldingar i gruppesamtalar Når eg blir invitert til eit rom - Røystsamtalainnbjodingar + Invitasjon til anrop Meldingar frå botar - Byrja ved uppstarten + Køyr ved oppstart Bakgrunnsamstilling Skru på bakgrunnsamstilling Samstillingsfyrespurnaden fekk tidsavbrot @@ -523,7 +523,7 @@ sekund Versjon - olm-versjon + olm versjon Vilkår for bruk Informasjon frå tredjepart Opphavsrett @@ -588,7 +588,7 @@ For å gå fram, ver venleg og skriv passordet ditt inn. Språk Vel språk - Ventar på Godkjenning + Ventar på verifikasjon Ver venleg og sjekk eposten din og klikk på lenkja han inneheld. Når det er gjort, klikk gå fram. Fekk ikkje til å stadfesta e-postadressa. Ver venleg og sjekk e-posten din og klikk på lenkja han inneheld. Når det er gjort, klikk gå fram E-postadressa er allereie i bruk @@ -620,8 +620,8 @@ For å gå fram, ver venleg og skriv passordet ditt inn.
    Kode - Særpreg - Du er førebels ikkje med i nokre samfunn. + Etikett + Du er førebels ikkje med i nokre fellesskap. 3 dagar 1 veke @@ -639,7 +639,7 @@ For å gå fram, ver venleg og skriv passordet ditt inn.
    Ingen Tilgang og synlegheit - Vis rommet i romutvalet + Vis dette rommet i romkatalogen Varsel Romtilgang Romhistoria si lesbarheit @@ -647,9 +647,9 @@ For å gå fram, ver venleg og skriv passordet ditt inn.
    Kven kan koma inn i rommet? Kven som helst - Berre medlemer (frå då innstillinga er vald) - Berre medlemer (frå då dei vart bodne inn) - Berre medlemer (frå då dei kom inn) + Berre medlemmar (frå då innstillinga er vald) + Berre medlemmar (frå då dei vart bodne inn) + Berre medlemmar (frå då dei kom inn) For å lenkja eit rom må det ha ei adresse. Berre folk som har vorte bodne inn @@ -658,7 +658,7 @@ For å gå fram, ver venleg og skriv passordet ditt inn. Utestengde brukarar - Omfattande + Avansert Rommet sin interne ID Adresser Eksperimentelle funksjonar @@ -672,7 +672,7 @@ For å gå fram, ver venleg og skriv passordet ditt inn. Dette rommet har ingen lokale adresser Ny adresse (t.d. #foo:matrix.org) - Dette rommet viser ikkje særpreg for nokre samfunn + Dette rommet viser ikkje etikettar for nokre fellesskap Ny fellesskaps-ID (t.d. +foo:matrix.org) Ugyldig fellesskaps-ID \'%s\' er ikkje ein gyldig fellesskaps-ID @@ -696,7 +696,7 @@ For å gå fram, ver venleg og skriv passordet ditt inn. Utval Preg - %s prøvde å lasta eit spesifikt punkt i rommet si tidslinje men klarte ikkje å finna det. + %s prøvde å lasta eit spesifikt punkt i rommet sin historikk men klarte ikkje å finna det. Ende-til-ende-krypteringsinfo @@ -713,7 +713,7 @@ For å gå fram, ver venleg og skriv passordet ditt inn. Offentleg namn ID Sesjonsnøkkel - Godkjenning + Verifikasjon Ed25519-fingeravtrykk Eksporter E2E-romnøkklar @@ -739,13 +739,13 @@ For å gå fram, ver venleg og skriv passordet ditt inn. ingen Godkjenn - Fjern godkjenning + Fjern verifikasjon Set på svartelista Fjern frå svartelista Verifiser sesjonen For å godkjenna at denne sesjonen er til å stola på, ver venleg og snakk med eigaren på ein anna måte (t.d. ansikt til ansikt eller på telefon) og spør han om nøkkelen han ser i Brukarinnstillingane for denne sesjonen samsvarar med nøkkelen under: - Viss det samsvarer, klikk Verifiser-knappen under. Viss det ikkje gjer det, avlyttar nokon andre denne sesjonen og du bør sannsynlegvis svarteliste den. I framtida vil denne godkjenningsprosessen bli meir forbetra. + Viss det samsvarer, klikk Verifiser-knappen under. Viss det ikkje gjer det, avlyttar nokon andre denne sesjonen og du bør sannsynlegvis svarteliste den. I framtida vil denne verifikasjonsprosessen bli meir forbetra. Eg stadfestar at nøkklane er like Riot støttar no ende-til-ende-kryptering men du må logga inn att for å skru det på. Du kan gjera det no eller seinare i App-innstillingane. @@ -755,7 +755,7 @@ For å gå fram, ver venleg og skriv passordet ditt inn. \n \nUkjende sesjonar: - Vel eit romutval + Vel ein romkatalog Tenaren er kanskje utilgjengeleg eller overlasta Skriv ein heimtenar inn for å få henta opp offentlege rom frå Heimtenar-URL @@ -800,7 +800,7 @@ For å gå fram, ver venleg og skriv passordet ditt inn. Klarte ikkje å laga widget. Fekk ikkje til å senda førespurnad. - Makthøgda må vere eit positivt heiltal. + Tilgangsnivået må vere eit positivt heiltal. Du er ikkje i dette rommet. Du har ikkje tillating til å gjera det i dette rommet. Vantande room_id i førespurnaden. @@ -814,8 +814,8 @@ For å gå fram, ver venleg og skriv passordet ditt inn. Du la til den nye sesjonen \'%s\', som etterspør krypteringsnøkklar. Den ikkje-verifiserte sesjonen din \'%s\' etterspør krypteringsnøkklar. - Byrj godkjenning - Del utan godkjenning + Start verifikasjon + Del utan verifikasjon Oversjå førespurnaden Åtvaring! @@ -826,11 +826,11 @@ For å gå fram, ver venleg og skriv passordet ditt inn. Viser handling Stengjer brukarar med den gjevne IDen ute Slepp utestengde brukarar med den gjevne IDen inn at - Set makthøgda på ein brukar + Definer tilgangsnivå for ein brukar AvOPar brukarar med den gjevne IDen Byd brukarar med den gjevne IDen inn til det noverande rommet Vert med i rommet med det gjevne aliaset - Fer frå rommet + Forlat rom Set romemnet Sparkar brukarar med gjeven ID Forandrar visingsnamnet ditt @@ -857,7 +857,7 @@ For å gå fram, ver venleg og skriv passordet ditt inn. Rom Komne inn Bodne inn - Filtrer gruppemedlemer + Filtrer gruppemedlemmar Filtrer grupperom Fellesskapssadministratoren har ikkje satt ei lang skildring for dette fellesskapet. @@ -1063,11 +1063,79 @@ Meldingssynlegheit på Matrix liknar på epost. At vi gløymer meldingane dine t Bruk ein integrasjonshandterar (Integration Manager) for å handtere botar, bruker, tillegg og klistermerkepakker. \nIntegrasjonshandterarar hentar konfigurasjonsdata, kan endre tillegg, sende rominvitasjonar og sette tilgangsnivå på vegne av deg. Handtering av kryptografiske nøkklar - Dette valet krev ein tredjepartsapplikasjon for å registrere meldingane. + Dette valet krev eit tredjepartsprogram for å registrere meldingane. Last rom-medlemmar etter behov (lazy-load) Heimetenaren din støttar ikkje såklalla lazy-loading av rommelemar endå. Prøv igjen seinare. Sikre meldingar med denne brukaren er ende-til-ende kryptert, dei kan ikkje bli lesne av tredje partar. Anna informasjon frå tredjepart Meldingar med denne brukaren er ende-til-ende kryptert, dei kan ikkje bli lesne av tredje part. + Konfiguer bråkete-varsel + Konfiguer anrops-varsel + Konfiguer stille-varsel + Konfiguer LED-farge, vibrasjon eller lyd… + + + Vis bli-med/forlat hendelsar + Invitasjonar, utkastingar og utestengingar gjeld ikkje dette. + Opna i nettlesar + Program for nettprat, under din kontroll og med full fleksibilitet. Riot lar deg kommunisere på den måten du vil. Implementert for [matrix] - protokollen for open, desentralisert kommunikasjon. +\n +\nOpprett ein gratis matrix.org-konto, sett opp eigen server på https://modular.im, eller ta i bruk ein annan Matrix-server. +\n +\nKvifor velje Riot.im\? +\n +\n• KOMPLETT KOMMUNIKASJON: Bygg rom rund teamet, venner, fellesskapet - du bestemmer! Send direktemeldingar, del filer, legg til tillegg, start taleanrop og videokonferansar! Alt saman er tilgjengeleg utan kostnad. +\n +\n• KRAFTIGE INTEGRASJONAR: Bruk Riot.im saman med verktøya du brukar frå før. Med Riot.Im kan du kommunisere med brukarar på andre plattformar. +\n +\n• PRIVAT OG SIKKER: Hald dine samtalar hemmelege. Industristandard ende-til-ende-kryptering sørgjer for at private samtalar forblir private. +\n +\n• OPEN, IKKJE LUKKA: Open kjeldekode, på toppen av Matrix-protokollen. Eig dine eigne data ved å drifte eigen server eller hjå nokon du stolar på. +\n +\n• OVERALT DER DU ER: Hald kontakten uansett kvar du er; fullstendig synkronisert meldingshistorikk på alle klientar, samt online på https://riot.im. + + Hentar sikkerheitskopiversjon… + Versjon + Matrix SDK versjon + Innstillingar + Sikkerheit og personvern + Tale & Video + Hjelp & Om + + + Vis skjulte hendelsar i historikken + + Aktiver swipe-for-å-svare i historikken + + Forlat rommet + Uleste meldingar + + Denne heime-tenaren køyrer ein gammal versjon, du kan derfor ikkje kopla til. Be administrator om å oppgradere. + + Etter aktivering, så kan ikkje kryptering bli deaktivert. + + Sikkerheit + Få meir info + Rominnstillingar + Forlat rom + Administratorar + Moderatorar + Tilpassa + Invitasjonar + Brukarar + + Administrator i %1$s + Moderator i %1$s + Tilpassa (%1$d) i %2$s + + Denne økta klarde ikkje å dele denne verifikasjonen med dine andre økter. +\nVerifikasjonsdata er lagra lokalt, og vil bli delt i ein framtidig versjon av programmet. + + Historikk + + Etter aktivering, så kan ikkje kryptering bli deaktivert. + + Fullstendig sikkerheit + diff --git a/vector/src/main/res/values-pl/strings.xml b/vector/src/main/res/values-pl/strings.xml index 37e402911e..a747e92ee0 100644 --- a/vector/src/main/res/values-pl/strings.xml +++ b/vector/src/main/res/values-pl/strings.xml @@ -33,7 +33,7 @@ Ze względu na brak pewnych uprawnień, niektóre funkcje mogą nie działać… Musisz być uprawniony, aby rozpocząć połączenie grupowe Nie można rozpocząć połączenia - Informacje o urządzeniu + Informacje o sesji Połączenia grupowe nie są obsługiwane w szyfrowanych pokojach Wyślij mimo wszystko i @@ -84,7 +84,6 @@ 1 użytkownik %d użytkowników %d użytkowników - Wyślij dzienniki @@ -251,7 +250,7 @@ Przyznaj dostęp w następnym oknie. NARZĘDZIA ADMINISTRACYJNE DZWOŃ WIADOMOŚCI BEZPOŚREDNIE - URZĄDZENIA + SESJE Zaproś Opuść pokój @@ -265,7 +264,7 @@ Przyznaj dostęp w następnym oknie. Pokaż wszystkie wiadomości od tego użytkownika ID, nazwa lub e-mail użytkownika Wspomnij - Pokaż listę urządzeń + Pokaż listę sesji Czy na pewno chcesz zaprosić %s do tej rozmowy? Zaproś przez ID @@ -357,7 +356,7 @@ Zauważ, że ta czynność spowoduje ponowne uruchomienie aplikacji i może to t Informacje o aplikacji Włącz powiadomienia dla tego konta - Włącz powiadomienia dla tego użytkownika + Włącz powiadomienia dla tej sesji Podświetl ekran na 3 sekundy Wiadomości bezpośrednie @@ -388,14 +387,14 @@ Zauważ, że ta czynność spowoduje ponowne uruchomienie aplikacji i może to t Uprawnienie do dostępu do kontaktów Przypnij pokoje z ominiętymi powiadomieniami Przypnij pokoje z nieprzeczytanych wiadomościami - Urządzenia + Sesje Pokaż czas wysłania dla wszystkich wiadomości Tryb oszczędzania danych - Szczegóły o urządzeniu + Szczegóły o sesji ID - Nazwa - Nazwa urządzenia + Nazwa publiczna + Zaaktualizuj nazwę publiczną Ostatnio widziany(-a) %1$s @ %2$s To działanie wymaga dodatkowego uwierzytelnienia. @@ -480,9 +479,9 @@ Zauważ, że ta czynność spowoduje ponowne uruchomienie aplikacji i może to t ID użytkownika Algorytm ID sesji - Nazwa urządzenia - Nazwa - ID urządzenia + Nazwa publiczna + Nazwa publiczna + ID Weryfikacja Odcisk palca Ed25519 @@ -493,17 +492,17 @@ Zauważ, że ta czynność spowoduje ponowne uruchomienie aplikacji i może to t Zweryfikowano Na czarnej liście - nieznane urządzenie + nieznana sesja brak Weryfikuj Dodaj na czarną listę Usuń z czarnej listy - Weryfikuj urządzenie + Weryfikuj sesję Te klucze są ze sobą zgodne - Pokój zawiera nieznane urządzenie + Pokój zawiera nieznane sesje Wybierz katalog pokojów Serwer może być wyłączony lub przeciążony Adres serwera domowego @@ -575,17 +574,13 @@ Zauważ, że ta czynność spowoduje ponowne uruchomienie aplikacji i może to t Możesz dodać adres e-mail do swojego profilu w ustawieniach. Serwer Domowy: Serwer Tożsamości: - Twoje hasło zostało zresetowane. - -Zostałeś wylogowany ze wszystkich urządzeń i nie będziesz więcej otrzymywać powiadomień push. Aby ponownie włączyć powiadomienia, zaloguj się ponownie na każdym urządzeniu. + Twoje hasło zostało zresetowane. Zostałeś wylogowany ze wszystkich sesji i nie będziesz więcej otrzymywać powiadomień push. Aby ponownie włączyć powiadomienia, zaloguj się ponownie na każdym urządzeniu. Numer telefonu Wprowadzony token dostępu nie został rozpoznany Uszkodzony JSON - Należy się ponownie zalogować w celu wygenerowania kluczy szyfrowania end-to-end dla tego urządzenia i wysłania klucza publicznego do Twojego serwera domowego. -Jest to jednorazowe działanie. -Przepraszamy za trudności. + Należy się ponownie zalogować w celu wygenerowania kluczy szyfrowania end-to-end dla tej sesji i wysłania klucza publicznego do Twojego serwera domowego. Jest to jednorazowe działanie. Przepraszamy za trudności. Lista grup @@ -638,7 +633,7 @@ Jesteś pewien? połączenie odebrane gdzie indziej Wiadomość nie została wysłana. Czy %1$s lub %2$s teraz? - Wiadomość niewysłana z powodu obecności nieznanych urządzeń. Czy %1$s lub %2$s teraz? + Wiadomość niewysłana z powodu obecności nieznanych sesji. Czy %1$s lub %2$s teraz\? Nowa wiadomość Kilka nowych wiadomości @@ -677,7 +672,7 @@ Jesteś pewien? Wiadomości zawierające moją wyświetlaną nazwę Wiadomości zawierające moją nazwę użytkownika Żądanie synchronizacji nieudane z powodu przekroczenia limitu czasu - Opóźnienie między każdym żądaniem + Opóźnienie między każdą synchronizacją Wersja olm Informacje o stronach trzecich Zatrzymaj media @@ -686,7 +681,7 @@ Jesteś pewien? Ekran domowy Podgląd zawartości URL Pokaż czas w formacie 12-godzinnym - Wibruj gdy ktoś wspomni o tobie + Wibruj gdy ktoś wspomni o Tobie Analityka @@ -711,8 +706,8 @@ Jesteś pewien? Każdy kto zna link pokoju, razem z gośćmi Musisz się wylogować aby uruchomić szyfrowanie. - Szyfruj wiadomości tylko do zaufanych urządzeń - Nigdy nie wysyłaj zaszyfrowanych wiadomości do niezweryfikowanych urządzeń w tym pokoju z tego urządzenia. + Szyfruj wiadomości tylko do zaufanych sesji + Nigdy nie wysyłaj zaszyfrowanych wiadomości do niezweryfikowanych sesjiw tym pokoju z tego urządzenia. Ten pokój nie ma adresu lokalnego Nowe ID społęczności (np. +foo:matrix.org) @@ -728,7 +723,7 @@ Jesteś pewien? NIe ustawiaj jako główny adres Błąd odszyfrowywania - Klucz urządzenia + Klucz sesji Wyeksportuj klucze pokoju Eksportuj klucze do pliku lokalnego Dołącz ponownie @@ -737,12 +732,12 @@ Jesteś pewien? Pobierz Wyczyść Wyślij naklejkę - Poproś ponownie o klucze szyfrujące z innych Twoich urządzeń. + Poproś ponownie o klucze szyfrujące z innych Twoich sesji. Prośba o klucz wysłana. Prośba wysłana - Uruchom proszę Riot na innym urządzeniu, które może odszyfrować wiadomość, aby wysłać klucze do tego urządzenia. + Uruchom proszę Riot na innym urządzeniu, które może odszyfrować wiadomość, aby wysłać klucze do tej sesji. Prywatność powiadomień Standardowa @@ -777,7 +772,7 @@ Jesteś pewien? Informacje o szyfrowaniu end-to-end Informacje zdarzenia - Informacja o urządzeniu nadawcy + Informacja o sesji nadawcy Eksportuj klucze E2E pokoju Klucze pokoju zostały zapisane w \'%s\' @@ -787,21 +782,17 @@ Uwaga: ten plik może zostać usunięty, jeśli aplikacja zostanie odinstalowana Importuj klucze pokoju Importuj klucze z lokalnego pliku Importuj - Szyfruj wiadomości tylko do zaufanych urządzeń - Nigdy nie wysyłaj zaszyfrowanych wiadomości do niezweryfikowanych urządzeń w tym pokoju z tego urządzenia. + Szyfruj wiadomości tylko do zaufanych sesji + Nigdy nie wysyłaj zaszyfrowanych wiadomości do niezweryfikowanych sesji w tym pokoju z tej sesji. Usuń weryfikację - Aby sprawdzić czy to urządzenie jest zaufane, skontaktuj się z jego właścicielem używając innych form (np. osobiście lub telefonicznie) i zapytaj ich czy klucz, który widzą w ustawieniach użytkownika dla tego urządzenia pasuje do klucza poniżej: - Jeśli klucz pasuje, potwierdź to przyciskiem poniżej. Jeśli nie, to ktoś inny najprawdopodobniej przejmuje lub podszywa się pod to urządzenie i powinieneś dodać urządzenie do czarnej listy. W przyszłości proces weryfikacji będzie bardziej skomplikowany. + Aby sprawdzić czy ta sesja jest zaufana, skontaktuj się z jej właścicielem używając innych form (np. osobiście lub telefonicznie) i zapytaj czy klucz, który widzą w ustawieniach użytkownika dla tego urządzenia pasuje do klucza poniżej: + Jeśli klucz pasuje, potwierdź to przyciskiem poniżej. Jeśli nie, to ktoś inny najprawdopodobniej przejmuje lub podszywa się pod tą sesję i powinieneś dodać tę sesję do czarnej listy. W przyszłości proces weryfikacji będzie bardziej skomplikowany. Riot obsługuje już szyfrowanie end-to-end (E2E), ale musisz zalogować się ponownie, aby je włączyć. Możesz to zrobić teraz lub później z poziomu ustawień aplikacji. - Ten pokój zawiera nieznane urządzenia, które nie zostały zweryfikowane. -Oznacza to brak gwarancji, że urządzenia należą do użytkowników do których twierdzą, że należą. -Przed kontynuowaniem, zalecamy wykonanie procesu weryfikacji każdego urządzenia, ale możesz ponownie wysłać wiadomość bez weryfikacji, jeśli wolisz. - -Nieznane urządzenia: + Ten pokój zawiera nieznane sesje, które nie zostały zweryfikowane. Oznacza to brak gwarancji, że sesje należą do użytkowników do których twierdzą, że należą. Przed kontynuowaniem, zalecamy wykonanie procesu weryfikacji każdego urządzenia, ale możesz ponownie wysłać wiadomość bez weryfikacji, jeśli wolisz. Nieznane sesje: Wyślij naklejkę @@ -862,7 +853,7 @@ Czy chcesz dodać teraz kilka? Poziom uprawnień musi być liczbą dodatnią. Nie jesteś w tym pokoju. Nie masz uprawnień, aby zrobić to w tym pokoju. - Nastrój + Wyróżnik społeczności Ten pokój nie wyświetla wyróżników dla żadnych społeczności Brakujące room_id w żądaniu. @@ -872,8 +863,8 @@ Czy chcesz dodać teraz kilka? Dodaj aplikacje Matrix Wyślij wiadomości głosowe - Dodałeś(-aś) nowe urządzenie \'%s\', które żąda kluczy szyfrujących. - Twoje niezweryfikowane urządzenie \'%s\' żąda kluczy szyfrujących. + Dodałeś(-aś) nową sesję \'%s\', która żąda kluczy szyfrujących. + Twoje niezweryfikowana sesja \'%s\' żąda kluczy szyfrujących. Rozpocznij weryfikację Współdziel bez weryfikacji Zignoruj żądanie @@ -932,8 +923,7 @@ Widoczność wiadomości w Matrix jest podobna do wiadomości e-mail. Nasze zapo 1 członek %d członków - %d członków - + @@ -955,26 +945,22 @@ Widoczność wiadomości w Matrix jest podobna do wiadomości e-mail. Nasze zapo 1 sekunda %d sek. - %d sek. - + 1 minuta %d min. - %d min. - + 1 godzina %s godz. - %s godz. - + 1 dzień %d dni - %d dni - + Pokazać wszystkie wiadomości od tego użytkownika? @@ -1052,10 +1038,9 @@ Pamiętaj ta akcja może zresetować aplikacje i potrwać jakiś czas. Połączenia Wyrzuć - Czy chcesz wyrzucić tego użytkownika z rozmowy? - Czy chcesz wyrzucić tych użytkowników z rozmowy? - Czy chcesz wyrzucić tych użytkowników z rozmowy? - + Czy chcesz wyrzucić tego użytkownika z rozmowy\? + Czy chcesz wyrzucić tych użytkowników z rozmowy\? + Formatowanie Markdown Pokaż zdarzenia dołączenia i wyjścia @@ -1086,8 +1071,8 @@ Pamiętaj ta akcja może zresetować aplikacje i potrwać jakiś czas. Powiadomienia są właczone dla twojego konta. Włącz - Ustawienia Urządzenia. - Powiadomienia są włączone dla tego urządzenia. + Ustawienia Sesji. + Powiadomienia są włączone dla tej sesji. Włącz Token Firebase @@ -1100,8 +1085,7 @@ Pamiętaj ta akcja może zresetować aplikacje i potrwać jakiś czas. Sprawdź ustawienia systemowe. Powiadomienia są wyłączone dla twojego konta. Sprawdź ustawienia konta. - Powiadomienia nie są dozwolone dla tego urządzenia. -Proszę sprawdź ustawienia Riot. + Powiadomienia nie są włączone dla tej sesji. Proszę sprawdź ustawienia Riot. "Zawsze wprowadzamy zmiany i ulepszenia do Riot.im. Pełną listę zmian można znaleźć tutaj: %1$s. Aby upewnić się, że niczego nie przegapisz, po prostu miej włączone aktualizacje." @@ -1152,27 +1136,27 @@ Spróbuj uruchomić ponownie aplikację. Usuń kopię zapasową Wersja - Zweryfikuj urządzenie + Zweryfikuj sesję Oznacz jako przeczytane Zweryfikowano! Weryfikacja klucza Żądanie weryfikacji - %s chce zweryfikować twoje urządzenie + %s chce zweryfikować twoją sesję Nieznany błąd Użyj kopii zapasowej klucza Podpis - Nowe urządzenie żąda kluczy szyfrujących. -\nNazwa urządzenia: %1$s -\nOstatnio widziane: %2$s -\nJeśli to nie Ty zalogowałeś(-aś) się na innym urządzeniu, zignoruj to żądanie. - Nowe niezweryfikowane urządzenie żąda kluczy szyfrujących. -\nNazwa urządzenia: %1$s -\nOstatnio widziane: %2$s -\nJeśli to nie Ty zalogowałeś(-aś) się na innym urządzeniu, zignoruj to żądanie. + Nowa sesja żąda kluczy szyfrujących. +\nNazwa sesji: %1$s +\nOstatnio widziana: %2$s +\nJeśli to nie Ty zalogowałeś(-aś) się na innej sesji, zignoruj to żądanie. + Nowa niezweryfikowana sesja żąda kluczy szyfrujących. +\nNazwa sesji: %1$s +\nOstatnio widziana: %2$s +\nJeśli to nie Ty zalogowałeś(-aś) się na innej sesji, zignoruj to żądanie. Weryfikuj Udostępnij @@ -1183,7 +1167,7 @@ Spróbuj uruchomić ponownie aplikację. Przywrócono kopię zapasową z %d kluczem. Przywrócono kopię zapasową z %d kluczami. - Przywrócono kopię zapasową z %d kluczami. + Użyj kopii zapasowej klucza Uruchamianie… (%1$d z %2$d) @@ -1205,7 +1189,7 @@ Spróbuj uruchomić ponownie aplikację. Utwórz nowy pokój Wszystkie społeczności - Rozmowy bezpośrednie + Wiadomości Bezpośrednie Nowy pokój STWÓRZ @@ -1219,27 +1203,1041 @@ Spróbuj uruchomić ponownie aplikację. Pokoje Opublikuj pokój do spisu pokojów - Aby nie utracić dostępu do Twoich zaszyfrowanych wiadomości, powinienieś aktywować kopię zapasową klucza na wszystkich swoich urządzeniach. + Aby nie utracić dostępu do Twoich zaszyfrowanych wiadomości, powinienieś aktywować kopię zapasową klucza na wszystkich aktywnych sesjach. Tworzenie kopii zapasowej kluczy… Utwórz kopię zapasową Twoje urządzenie używa przestarzałego protokołu bezpieczeństwa TSL, podatnego na ataki, dlatego dla Twojego bezpieczeństwa nie będziesz mógł się połączyć - Kopia zapasowa klucza została prawidłowo skonfigurowana dla tego urządzenia. + Kopia zapasowa klucza została prawidłowo skonfigurowana dla tej sesji. Inicjalizacja usługi Zaloguj się za pomocą logowania jednorazowego Ustaw ważność powiadomienia za pomocą wydarzeń, skonfiguruj dźwięk, diodę LED, wibracje Ważność powiadomień ze względu na wydarzenie - Powiadomienia diagnostyczne + Diagnostyka powiadomień Rozwiązywanie problemów Diagnostyka podstawowa nie wykazała problemów. Jeżeli wciąż nie otrzymujesz powiadomień, prosimy o przesłanie raportu o błędach, w celu ich rozwiązania. Aplikacja nie potrzebuje łączyć się z serwerem domowym w tle, powinno to zredukować użycie baterii Jeżeli nie pamiętasz swoich danych odzystkiwania, możesz %s. Zgubiłeś (-łaś) swój klucz odzyskiwania\? Możesz ustawić nowy w ustawieniach. - Kopia zapasowa posiada poprawną sygnaturę z niezweryfikowanego urządzenia %s + Kopia zapasowa posiada poprawną sygnaturę z niezweryfikowanej sesji %s Wykryto nową, bezpieczną kopię kluczy wiadomości. \n \nJeżeli nie ustawiałeś nowej metody odzyskiwania, atakujący mogą uzyskać dostęp do Twojego konta. Zmień hasło konta i ustaw nową metodę odzyskiwania jak najszybciej w Ustawieniach. Jesteś na bieżąco! Ten adres URL jest nieosiągalny, proszę sprawdź jego poprawność + Unieważnij + Rozłącz + Nie pytaj ponownie + + To nie jest prawidłowy adres serwera Matrix + Potwierdź swoje hasło + Napraw Usługi Play + + Dodaj Konto + + Zarządzanie Kluczami Kryptograficznymi + Zezwól na integracje + Menadżer Integracji + + Hasło jest nieprawidłowe + Hasła nie pasują do siebie + + Wybierz + Wybierz + Odtwarzaj dźwięk migawki + + Nazwa urządzenia (widoczna dla osób, z którymi się komunikujesz) + Publiczna nazwa urządzenia jest widoczna dla ludzi, z którymi się komunikujesz + nieznany adres ip + Pokój + Nowe Zaproszenie + Ten widget został dodany przez: + Otwórz w przeglądarce + Twój motyw + Identyfikator Pokoju + + + Zezwól + Wprowadź nazwę użytkownika. + Wprowadź hasło + (Zaawansowane) + Ustaw Hasło + Sukces ! + Twoje klucze są archiwizowane. + Zastąp + Zatrzymaj + + Klucz Odzyskiwania + Nieoczekiwany błąd + Czy na pewno\? + Pobieranie wersji kopii zapasowej… + Wprowadź Klucz Odzyskiwania + + Odzyskiwanie Wiadomości + + Błąd sieci: sprawdź połączenie i spróbuj ponownie. + + Obliczanie klucza odzyskiwania… + Pobieranie kluczy… + Importowanie kluczy… + Odblokuj Historię + Wprowadź klucz odzyskiwania + Kopia Przywrócona %s ! + + dodano jeden nowy klucz do tej sesji. + dodano %d nowe klucze do tej sesji. + dodano %d nowych kluczy do tej sesji. + + + Niepowodzenie przy pobieraniu wersji kluczy (%s). + Usuwanie kopii zapasowej… + Sprawdzanie stanu kopii zapasowej + Oczekiwanie na potwierdzenie partnera… + + Sesja została z powodzeniem zweryfikowana. + Druga strona odrzuciła weryfikację. +\n%s + Reakcje + + Ostatnio zmieniony %2$s przez %1$s + + + Brak sieci. Sprawdź swoje połączenie z Internetem. + Proszę czekać… + Tego pokoju nie można podejrzeć + Witaj w programie beta! + Bezpieczeństwo i Prywatność + Ekspert + Brak reguł push + Oczekiwanie… + Wysyłanie miniatury (%1$s / %2$s) + Szyfrowanie pliku… + Wysyłanie pliku (%1$s / %2$s) + + Pobieranie pliku %1$s… + Plik %1$s został pobrany! + + Tworzę pokój… + Warunki Usługi + Wyśli załącznik + + Utwórz nowy pokój + Pokaż hasło + Ukryj hasło + Przewiń do dołu + + Plik \'%1$s\' (%2$s) jest zbyt duży do przesłania. Limit wynosi %3$s. + + Plik + Kontakt + Galeria + Naklejka + To jest spam + Nieodpowiednia treść + Inny powód… + Zgłoś tę treść + Powód zgłoszenia treści + ZGŁOŚ + ZABLOKUJ UŻYTKOWNIKA + + Treść zgłoszona + Zgłoszone jako spam + Weyfikacja zostaje anulowana. +\nPowód: %s + + Proszę napisz swoją sugestię poniżej. + Opisz swoją sugestię tutaj + Utwórz nową rozmowę bezpośrednią + Połączenie z serwerem zostało utracone + + Proszę wykonać kopię + Preferencje + Głos i wideo + Wiadomości Bezpośrednie + + Filtruj rozmowy… + Wyślij nową wiadomość bezpośrednią + Dodaj przez matrix ID + Filtruj wg nazwy użytkownika lub ID… + + Wersja Matrix SDK + Pomoc i o aplikacji + + + Wszystkie wiadomości (hałaśliwy) + Wszystkie wiadomości + Tylko wspomnienia + Wycisz + Ustawienia + Nie ignorujesz żadnych użytkowników + + Widziany przez + + Zaawansowane ustawienia + Tryb programisty + Ustawienia + Bezpieczeństwo + Więcej + Ustawienia pokoju + Powiadomienia + Opuść pokój + Włącz szyfrowanie end-to-end + Brak + Brak skonfigurowanego serwera tożsamości. + + Poproś administratora serwera (%1$s) o skonfigurowanie usługi TURN, aby połączenia mogły działać prawidłowo. +\n +\nMożesz także użyć publicznego serwera %2$s, choć nie będzie on tak niezawodny i pozna twój adres IP. Wybór możesz zmienić w Ustawieniach. + Spróbuj użyć %s + Wprowadź adres email, aby możliwe było odzyskiwanie konta. W przyszłości pozwoli on także odnaleźć Cię Twoim znajomym. + Wprowadź numer telefonu, później pozwoli on Cię odnaleźć Twoim znajomym. + Nie udało się połączyć z serwerem o podanym adresie URL, upewnij się, że wpisano go poprawnie + Dodaj serwer tożsamości w ustawieniach, aby móc wykonać tę akcję. + Wymagane jest uwierzytelnienie + + + Niektóre rodzaje wiadomości będą ciche (wygenerują powiadomienie bez dźwięku). + Nie udało się wczytać niestandardowych reguł, spróbuj ponowić. + Weryfikacja Usług Google + Riot używa Usług Google Play do dostarczania wiadomości push. Konfiguracja usług nie wydaje się być prawidłowa: +\n%1$s + Otrzymano token FCM: +\n%1$s + Niepowodzenie przy pobieraniu tokena FCM: +\n%1$s + [%1$s] +\nRiot nie ma wpływu na wystąpienie tego problemu. Na tym urządzeniu nie ma konta Google. Otwórz menadżer kont i dodaj konto Google. + Token FCM z powodzeniem zarejestrowany na serwerze domowym. + Niepowodzenie przy rejestracji tokena FCM na serwerze domowym: +\n%1$s + + Usługa została zatrzymana i automatycznie uruchomiona ponownie. + Usługa nie uruchomiła się ponownie + + Usługa zostanie uruchomiona przy starcie urządzenia. + Usługa nie zostanie uruchomiona przy starcie urządzenia, nie otrzymasz żadnych powiadomień, dopóki Riot nie zostanie uruchomiony. + Dla zwiększenia bezpieczeństwa, zalecamy aby wykonać ten krok osobiście lub przez inne zaufane środki komunikacji. + Treść została zgłoszona. +\n +\nJeśli nie chcesz więcej otrzymywać wiadomości od tego użytkownika, możesz go zablokować by ukryć jego wiadomości + Treść została zgłoszona jako spam. +\n +\nJeśli nie chcesz więcej widzieć treści tego użytkownika,możesz go zablokować by ukryć jego wiadomości + Sprawdź ograniczenia pracy w tle + Wyłącz ograniczenia + + Konfiguruj powiadomienia połączeń + Wybierz kolor diody LED, wibrację, dźwięk… + + + Integracje + Latn + + Wprowadź adres e-mail, aby możliwe było odzyskiwanie konta. Opcjonalnie użyj adresu e-mail lub numeru telefonu aby móc zostać odkrytym przez znajomych. + Wprowadź adres e-mail, aby możliwe było odzyskiwanie konta. Opcjonalnie użyj adresu e-mail lub numeru telefonu aby móc zostać odkrytym przez znajomych. + Pozwól na awaryjny serwer wspomagania połączeń + Użyje %s aby wspomagać gdy Twój serwer domowy takiego nie ofertuje (Twój adres IP będzie udostępniony podczas połączenia) + [%1$s] +\nBłąd jest poza kontrolą Riot i nawiązując do Google sygnalizuje on, iż urządzenie posiada zbyt wiele aplikacji zarejestrowanych z FCM. Błąd występuje jedynie w przypadku posiadania skrajnie wielu aplikacji, w związku z czym nie powinno dotknąć to normalnego użytkownika. + [%1$s] +\nBłąd jest poza kontrolą Riot. Może on występować z wielu powodów. Przypuszczalnie aplikacja powróci do normalnego stanu po spróbowaniu ponownie, chociaż można sprawdzić także w ustawieniach systemu uprawnienia Usług Google Play dotyczące dostępu do sieci, sprawdzić prawidłowość zegaru urządzenia lub też, może być to błąd niestandardowego oprogramowania systemowego. + Aktywuj uruchamianie przy starcie systemu + + Restrykcje dotyczące działania aplikacji w tle są wyłączone dla Riot. Test powinen zostać uruchomiony używając danych komórkowych (bez WIFI). +\n%1$s + "Restrykcje dotyczące działania aplikacji w tle są włączone dla Riot. +\nPraca którą aplikacja próbuje wykonać będzie agresywnie ograniczona podczas działania w tle i może wpłynąć na wyświetlanie powiadomień. +\n%1$s" + Na Riot nie ma wpływu Optymalizacja Baterii. + Jeżeli użytkownik pozostawi urządzenie odłączone od zasilania oraz nieużywane przez określony okres, z wyłączonym ekranem, urządzenie przejdzie w tryb Doze. Uniemożliwia to aplikacjom dostęp do sieci i opóźnia ich zadania, synchonizację oraz standardowe alarmy. + Tryb synchronizacji w tle (eksperymentalny) + Zoptymalizowano dla baterii + Riot będzie synchronizował się w tle w sposób który oszczędza limitowane zasoby urządzenia (baterię). +\nW zależności od stanu zasobów urządzenia, synchronizacja może być opóźniania przez system operacyjny. + Zopytmalizowano dla działania w czasie rzeczywistym + Riot będzie synchornizował się okresowo o ściśle określonym czasie (konfigurowalne). +\nWpłynie to na użycie baterii i sieci, na panelu powiadomień pozostanie wyświetlone stałe powiadomiene o nasłuchiwaniu zdarzeń. + Brak synchronizacji w tle + Nie będziesz otrzymywać powiadomień o przychodzących wiadomościach gdy aplikacja będzie działać w tle. + Niepowodzenie przy aktualizacji ustawień. + + + Preferowany interwał synchronizacji + %s +\nSynchronizacja może zostać opóźniona w zależności od zasobów (bateria) lub stanu urządzenia (hibernacja). + Użyj Menedżera Integracji aby zarządzać botami, mostami, widżetami oraz pakietami naklejek. +\nMenadżerowie Integracji odbierają dane konfiguracji, mogą zmieniać widżety, wysyłać zaproszenia do pokoi oraz ustawiać poziomy uprawnień na Twoje żądanie. + Pokaż podgląd linków wewnątrz czatu jeśli twój serwer wspiera tę funkcję. + Formatuj wiadomości używając składni Markdown zanim zostaną wysłane. Pozwala to na zaawansowane formatowanie takie jak używanie asterysków do wyświetlania tekstu w kursywie. + Nie wpływa to na zaproszenia, wyrzucenia oraz bany. + Wysyłaj wiadomości za pomocą klawisza enter + Przycisk enter na klawiaturze programowej wyśle wiadomość zamiast wprowadzania łamanania linii + + Znajdź + Zarządzaj ustawieniami wyszukiwania. + Riot potrzebuje utrzymać mało wpływowe połączenie w tle, w celu otrzymywania wiarygodnych powiadomień. +\nNa następnym ekranie zostanie się poproszonym o pozwolenie działania w tle dla Riot, proszę zaakceptować. + Tryb oszczędzania danych użyje filtra szczegółowego, w związku z czym aktualizacje o obecności i powiadomienia o pisaniu zostaną przefiltrowane. + + Media + Domyślne źródło mediów + Odzyskiwanie zaszyforwanych wiadomości + + %1$s: 1 wiadomość + %1$s: %2$d wiadomości + + + + %d powiadomienie + %d powiadomień + + + + Nowe wydarzenie + Nowe wiadomości + Ja + ** Nie udało się wysłać - proszę otworzyć pokój + + Widżet + Załaduj widżet + Używając tego pliki cookies mogą zostać ustawione i dane wspóldzielone z %s: + Używając tego dane mogą być współdzielone z %s: + Uruchomienie widżetu nie powiodło się. +\n%s + Przeładuj widżet + Cofnij dostęp dla mnie + + Wyświetlana nazwa + Adres URL awatara + Twój ID użytkownika + ID Widżetu + Przepraszamy, połączenia konferencyjne za pomocą Jitsi nie są wspierane na starszych urządzeniach (urządzenia z systemem Android poniżej wersji 5.0) + Widżet chce użyć następujących zasobów: + Zablokuj wszystko + Użyj aparatu + Użyj mikrofonu + Odczytaj media zabezpieczone DRM + + Nie skonfigurowano menedżera integracji. + Uruchom systemową kamerę zamiast niestandardowego ekranu kamery w aplikacji. + Aby kontynuować, musisz zaakceptować Warunki użytkowania dla tej usługi. + + Nie znaleziono prawidłowej aplikacji Usługi Google Play. Powiadomienia mogą nie działać prawidłowo. + + Riot.im - Rozmawiaj, we własny sposób + Bezpieczna aplikacja czatu całkowicie pod Twoją kontrolą. + Aplikacja czatu, całkowicie pod twoją kontrolą i całkowicie dopasowująca się. Riot pozwala na komunikowanie się w jakikolwiek sposób chcesz. Stworzony dla [Matrix] - standardu otwartej, zdecentralizowanej komunikacji. +\n +\nStwórz darmowe konto matrix.org, zdobądź swój własny serwer na https://modular.im, lub użyj innego serwera Matrix. +\n +\nDlaczego warto wybrać Riot.im\? +\n +\n• KOMPLETNA KOMUNIKACJA: Stwórz pokoje dla twojego zespołu, przyjaciół, społeczności - tak, jak lubisz! Czatuj, udostępniaj pliki, dodawaj widżety i wykonuj połączenia głosowe lub wideo - wszystko za darmo. +\n +\n• SILNA INTEGRACJA: Używaj Riot.im z narzędziami które znasz i kochasz. Z Riot.im możesz pisać z użytkownikami i grupami na innych aplikacjach do czatu. +\n +\n• PRYWATNE I BEZPIECZNE: Utrzymuj twoje konwersacje w sekrecie. Nowoczesne szyfrowanie typu \"end-to-end\" zapewnia, że to co jest prywatne, pozostaje prywatne +\n +\n• OTWARTE, NIE ZAMKNIĘTE: Otwartoźródłowe i oparte o Matrix. Bądź w posiadaniu swoich danych posiadając swój własny serwer, lub wybierając taki, któremu ufasz. +\n +\n•GDZIEKOLWIEK JESTEŚ: Bądź w kontakcie gdziekolwiek jesteś wraz z w pełni synchronizowaną historią wiadomości na wszystkich twoich urządzeniach lub na https://riot.im. + + Hasło jest zbyt słabe + + Proszę usunąć hasło, jeżeli chcesz aby Riot wygenerował klucz odzyskiwania. + Brak dostępnych sesji Matrix + + Nie utrać zaszyfrowanych wiadomości + Wiadomości w pokojach zaszyfrowanych są bezpieczne dzięki szyfrowaniu end-to-end. Jedynie Ty i Twój odbiorca posiadają klucze dla tych wiadomości. +\n +\nBezpiecznie utwórz kopię zapasową, aby ich nie utracić. + Zacznij używać Kopii Zapasowej Kluczy + Wyeksportuj klucze ręcznie + + Zabezpiecz swoją kopię zapasową używając hasła. + Będziemy przechowywać zaszyfrowaną kopię Twoich kluczy na serwerze domowym. Chroń swoją kopię zapasową hasłem, aby pozostała bezpieczna. +\n +\nDla pełni bezpieczeństwa, powinno być ono inne niż hasło konta. + Tworzenie kopii zapasowej + Lub, zabezpiecz swoją kopię zapasową z Kluczem Odzyskiwania i zachowaj w bezpiecznym miejscu. + (Zaawansowane) Ustaw za pomocą Klucza Odzyskiwania + Twój klucz odzyskiwania jest siatką bezpieczeństwa - możesz użyć go, aby odzyskać dostęp do zaszyfrowanych wiadomości jeżeli zapomnisz hasła. +\nPrzechowuj swój klucz odzyskiwania w bardzo bezpiecznym miejscu, takim jak menedżer haseł (lub sejf) + Przechowuj swój klucz odzyskiwania w bardzo bezpiecznym miejscu, takim jak menedżer haseł (lub sejf) + Zrobiłem kopię + Zapisz Klucz Odzyskiwania + Zapisz jako plik + "Klucz odzyskiwania został zapisany do \'%s\'. +\n +\nUwaga: plik może zostać usunięty, jeżeli aplikacja jest odinstalowana." + + Kopia zapasowa już istnieje na Twoim serwerze domowym + Wygląda na to, iż kopia zapasowa kluczy została skonfigurowana za pomocą innej sesji. Czy chcesz zastąpić ją tą, którą tworzysz\? + Generowanie Klucza Odzyskiwania używając hasła, proces może zająć kilka sekund. + Kopia zapasowa uruchomiona + Twoje klucze szyfrujące będą kopiowane do kopii zapasowej w tle przez Twój serwer domowy. Wstępna kopia zapasowa może zająć kilka minut. + + + Utracisz dostęp do swoich wiadomości jeżeli wylogujesz się lub utracisz to urządzenie. + + Użyj kopii zapasowej klucza aby odblokować historię zaszyfrowanych wiadomości + użyj klucza odzyskiwania + Użyj Klucza Odzyskiwania aby odblokować historię zaszyfrowanych wiadomości + Kopia zapasowa nie może zostać zdeszyfrowana za pomocą tego hasła: proszę upewnij się, czy wprowadzone hasło jest poprawne. + Przywracanie kopii zapasowej: + Kopia zapasowa nie może zostać zdeszyfrowana za pomocą tego klucza odzyskiwania: proszę upewnij się, czy wprowadzony klucz odzyskiwania jest poprawny. + + Przywrócono %1$d kluczy sesji, i dodano %2$d nowych kluczy które nie były znane tej sesji + Szyfrowanie sesji nie jest aktywowane + + + Kopia zapasowa klucza nie jest aktywna dla tej sesji. + Twoje klucze nie są będą zapisywane w kopii zapasowej od tej sesji. + + Kopia zapasowa posiada sygnaturę od nieznanej sesji z ID %s. + Kopia zapasowa posiada prawidłową sygnaturę dla tej sesji. + Kopia zapasowa posiada prawidłową sygnaturę z zweryfikowanej sesji %s. + Kopia zapasowa posiada nieprawidłową sygnaturę ze zweryfikowej sesji %s + Kopia zapasowa posiada niezweryfikową sygnaturę z nieznanej sesji %s + Nie udało się uzyskać zaufanych informacji dla kopii zapasowej (%s). + + Aby użyć Kopii Zapasowej Kluczy dla tej sesji, przywróć ją za pomocą hasła lub klucza odzyskiwania. + Usuwanie kopii zapasowej nie powiodło się (%s) + + Nowa kopia zapasowa kluczy + To byłem(-łam) ja + Nigdy nie utrać zaszyfrowanych wiadomości + Zacznij korzystać z Kopii Zapasowej Kluczy + + Nigdy nie utrać zaszyfrowanych wiadomości + Użyj Kopii Zapasowej Kluczy + + Nowe klucze szyfrowanych wiadomości + Zarządzaj w Kopii Zapasowej Kluczy + + Tworzenei kopii zapasowej kluczy… + + + Kopiowanie %d klucza… + Kopiowanie %d kluczy… + + + + Nieprawidłowa odpowiedź funkcji autoodkrywania serwera domowego + Opcje automatycznego uzupełniania serwerów + Riot wykryło niestandardową konfigurację serwera dla Twojej domeny userID \"%1$s\": +\n%2$s + Użyj Konfiguracji + + Zostałeś(-łaś) wylogowana ze względu na nieprawidłowe lub wygasłe dane logowania. + + Zweryfikuj porównując krótki ciąg tekstowy. + Rozpocznij weryfikację + Przychodzące żądanie weryfikacji + Zweryfikuj tą sesję poprzez oznaczenie jej jako zaufanej. Zaufanie sesji innych osób przynosi spokój na umyśle gdy są to szyfrowane wiadomości end-to-end. + Werfikacja tej sesji oznaczy ją jako zaufaną, a także oznaczy Twoją sesję jako zaufaną dla rozmówcy. + + Wyświetl żądanie + Bezpieczne wiadomości od tego użytkownika są zabezpeiczone za pomocą szyfrowania end-to-end i są nie do odczytania przez osoby trzecie. + Połączenie nie powiodło się z powodu niewłaściwie skonfigurowanego serwera + Nie możesz tego zrobić z mobilnej aplikacji Riot + Niektóre powiadomienia są wyłączone w osobistej konfiguracji. + Usługi Google Play są aktualne. + Automatycznie uruchom ponownie usługę powiadomień + Potwierdź, że następujące emoji pojawiły się na ekranie partnera + Potwierdź, że następujące liczby pojawiły się na ekranie partnera + + Otrzymano przychodzące żądanie weryfikacji. + Rozumiem + + Nic się nie pojawiło\? Nie wszystkie aplikacje obsługują już interaktywną weryfikację. Spróbuj weryfikacji starszego typu. + Użyj weryfikacji starszego typu. + + Weryfikacja Anulowana + Interaktywna Weryfikacja Sesji + Użytkownik anulował weryfikację + Upłynął limit czasu weryfikacji + Sesja nie rozpoznała tej transakcji + Sesje nie mogą porozumieć się w sprawie wykorzystania metody szyfrowania (wymiana kluczy, hasz, MAC lub SAS) + Zobowiązanie bitowe nie zgadza się + SAS nie zgadza się + Sesja otrzymała niespodziewaną wiadomość + Otrzymano nieprawidłową wiadomość + Poprzednie wersje Riot posiadały błąd bezpieczeństwa, który umożliwiał Twojemu serwerowi tożsamości (%1$s) dostęp do Twojego konta. Jeżeli ufasz %2$s, możesz to zignorować; w przeciwnym wypadku wyloguj się i zaloguj ponownie. +\n +\nInformacje o szczegółach tutaj: +\nhttps://medium.com/@RiotChat/36b4792ea0d6 + + Dołącz do pokoju, aby rozpocząć korzystanie z aplikacji. + Zapoznaj się z nieprzeczytanymi wiadomościami tutaj + Twoje rozmowy bezpośrednie będą wyświetlane tutaj + Twoje pokoje będą wyświetlane tutaj + + Nieprawidłowe zdarzenie, nie można wyświetlić + Podgląd globalnego, publicznego pokoju nie jest wciąż wspierany w RiotX + + Wystąpił błąd podczas otrzymywania zaufanych informacji + Wystąpił błąd podczas uzyskiwania danych kluczy kopii zapasowej + + Jako że RiotX jest we wczesnej fazie rozwoju, niektóre funkcje mogą być niedostępne i możesz doświadczyć błędów. + Najnowsza lista funkcji jest zawsze na %1$s, i jeżeli znajdziesz błąd, proszę wyślij raport w lewym górnym menu poprzez \"Ekran domowy\", postaramy załatać się go tak szybko, jak tylko się da. + Opis w Sklepie Play + Jeżeli znajdziesz błędy, proszę wyślij raport za pomocą menu w lewym górnym rogu \"Ekranu domowego\", postaramy się je naprawić tak szybko, jak tylko się da. + + Importowanie kluczy E2E z pliku \"%1$s\". + + Informacje o stronach trzecich + Już wyświetlasz ten pokój! + + id_aplikacji: + klucz_push: + wyświetlana_nazwa_aplikacji: + nazwa_sesji: + Url: + Format: + + Zarejestruj token + + Dziękujemy, sugestia została szczęśliwie wysłana + Wysłanie sugestii nie powiodło się (%s) + + Wyświetl ukryte wydarzenia na linii czasowej + + RiotX - Klient Matrix następnej generacji + Szybszy i lżejszy klient Matrix używający najnowszych frameworków Androida + RiotX jest nowym klientem dla protokołu Matrix (Matrix.org): otwarta sieć dla bezpiecznej, zdecentralizowej komunikacji. RiotX jest całkowicie przepisanym klientem Riot, opartym na nowym SDK Matrix dla systemu Android. +\n +\nUwaga: Jest to wersja beta. RiotX jest obecnie w fazie aktywnego rozwoju i posiada ograniczenia oraz (mamy nadzieje że niewiele) błędy. Wszystkie opinie są mile widziane! +\n +\nRiotX wspiera: • Logowanie do istniejącego konta • Tworzenie pokoi oraz dołączanie do pokoi publicznych • Akceptowanie i odrzucanie zaproszeń • Wyświetlanie listy pokoi użytkowników • Wyświetlanie informacji o pokoju • Wysyłanie wiadomości tekstowych • Wysyłanie załączników • Odczytywanie i pisanie wiadomości w zaszyfrowanych pokojach • Kryptografia: Kopię zapasową kluczy E2E, zaawansowaną weryfikację urządzeń, żądanie udostępniania kluczy i odpowiedzi na nie • Powiadomienia push • Jasne, Ciemne oraz Czarne motywy +\n +\nNie wszystkie funkcje Riot są wdrożone w RiotX. Główne niedostępne (pojawią się już wkrótce!) funkcje: • Ustawienia pokoi (wyświetl listę członków pokoi) • Rozmowy • Widżety • … + + (edytowano) + + %1$s aby utworzyć konto. + Użyj starszej aplikacji + + + Edycje wiadomości + Nie znaleziono edycji + + Aktywuj gest przesunięcia, aby odpowiedzieć na osi czasu + + Link skopiowany do schowka + + Nie znaleziono, użyj Dodaj poprzez ID Matrix aby wyszukać na serwerze. + Zacznij pisać, aby uzyskać rezultaty + Dołączanie do pokoju… + + Wyświetl historię edycji + + Sprawdź Warunki + Bądź odkryty przez innych + Używaj Botów, mostów, widżetów i paczek naklejek + + Czytaj na + + + Obecnie używasz %1$s aby odkrywać i być odkrytym przez kontakty, które znasz. + Nie używasz serwera tożsamości. Aby odkrywać i być odkrywanym przez kontakty, które znasz, skonfiguruj jeden poniżej. + Rozpoznawalny adres e-mail + Opcje odkrywania pojawią się w momencie gdy dodasz adres e-mail. + Opcje odkrywania pojawią się w momencie gdy dodasz numer telefonu. + Odłączenie od serwera tożsamości oznacza, iż nie będziesz mógł(-ła) zostać odkryty(-ta) przez innych użytkowników i nie będziesz mógł(-ła) zapraszać innych za pomocą adresu e-mail oraz numeru telefonu. + Rozpoznawalne numery telefonu + Wysłaliśmy e-mail potwierdzający do %s, sprawdź swoją skrzynkę i naciśnij link potwierdzający + Oczekiwanie + + Wprowadź nowy serwer tożsamości + Nie można połączyć z serwerem tożsamości + Wprowadź adres serwera tożsamości + Serwer tożsamości nie posiada warunków usługi + Wybrany system tożsamości nie posiada jakichkolwiek warunków usługi. Kontynuuj jedynie, gdy ufasz właścicielowi usługi + Wiadomość tekstowa wysłana do %s. Proszę wprowadzić kod weryfikacyjny w niej zawarty. + + Udostępniasz adres e-mail lub numer telefonu serwerowi tożsamości %1$s. Musisz ponownie połączyć się z %2$s aby ich nie udostępniać. + Akceptuj Warunki Usługi serwera tożsamości (%s) aby pozwolić na bycie odkrytym za pomocą adresu e-mail lub numeru telefonu. + + Aktywuj szczegółowe dzienniki. + Dzienniki szczegółowe pomogą twórcom poprzez udostępnianie większej ilości danych, które zostaną wysłane, gdy potrząśniesz wściekle. Nawet jeżeli zostaną aktywowane, aplikacja nie zapisuje szczegółów wiadomości oraz innych danych prywatnych. + + + Spróbuj ponownie, gdy zaakceptujesz warunki usługi oraz warunki ogólne serwera domowego. + + Wygląda na to, iż serwer potrzebuje wiele czasu aby odpowiedzieć i może być to spowodowane zarówno słabą łącznością jak i problemem z serwerem. Spróbuj ponownie za chwilę. + + Otwórz panel nawigacji + Otwórz menu tworzenia pokoju + Zamknij menu tworzenia pokoju… + Zamknij baner kopii zapasowej kluczy + odczytano przez %1$s, %2$s oraz %3$d + odczytano przez %1$s, %2$s oraz %3$s + odczytano przez %1$s oraz %2$s + %s odczytał + + 1 użytkownik odczytał + %d użytkowników odczytało + + + + Wystąpił błąd poczas otrzymywania załącznika. + Audio + Nie można obsłużyć otrzymanych danych + + Zgłoszono jako nieodpowiedni + Zawartość została zgłoszona jako niewłaścwa. +\n +\nJeżeli nie chcesz widzieć treści od tego użytkownika, możesz go zablokować aby ukryć jego wiadomości + + Riot potrzebuje uprawnień aby zapisywać klucze E2E na dysku. +\n +\nPozwól na dostęp w następnym oknie aby móc eksportować klucze ręcznie. + + Opuść pokój + %1$s nie dokona(-ła) zmian + Wysyła wiadomość jako spoiler + Spoiler + Wprowadź słowa kluczowe aby znaleźć reakcję. + + Naciśnij długo na pokój aby wyświetlić więcej opcji + + + %1$s ustawił(-a) pokój dostępnym publicznie dla każdego, kto zna link. + %1$s ustawił(-a) pokój tylko dla zaproszonych. + Nieprztane wiadomości + + Wyzwól swoją komunikację + Czatuj z osobami bezpośrednio lub w grupach + Pozostaw konwersacje prywatnymi za pomocą szyfrowania + Rozszerz i dopasuj swoje doświadczenie + Rozpocznij + + Wybierz serwer + Tak jak adres e-mail, konta mają jeden dom, aczkolwiek możesz rozmawiać ze wszystkimi + Dołącz do milionów, za darmo, na największym publicznym serwerze + Hosting premium dla organizacji + Dowiedz się więcej + Inne + Ustawienia niestandardowe i zaawansowane + + Kontynuuj + Połącz z %1$s + Połącz z Modular + Połącz z serwerem niestandardowym + Zaloguj się do %1$s + Zarejestruj + Zaloguj się + Kontynuuj za pomocą logowania jednostopniowego + + Adres Modular + Adres + Hosting premium dla organizacji + Wprowadź adres Riot Modular lub serwera którego chcesz użyć + Wprowadź adres serwera lub Riot z którym chcesz się połączyć + + Wystąpił błąd podczas ładowania strony: %1$s (%2$d) + Aplikacja nie jest w stanie zalogować się do tego serwera domowego. Serwer domowy obsluguje następujące metody logowania: %1$s. +\n +\nCzy chcesz zalogować się używając klienta sieciowego\? + Przepraszamy, serwer nie obsługuje tworzenia nowych kont. + Aplikacja nie jest w stanie utworzyć konta na tym serwerze domowym. +\n +\nCzy chcesz zarejestrować się używając klienta sieciowego\? + + E-mail nie jest powiązany z kontem. + + Zresetuj hasło na %1$s + Wiadomość weryfikacyjna zostanie wysłana na adres e-mail aby potwierdzić ustawienie nowego hasła. + Dalej + E-mail + Nowe hasło + + Uwaga! + Zmiana hasła zresetuje wszystkie klucze szyfrowania end-to-end dla wszystkich twoich sesji, czyniąc zaszyfrowaną historię czasu nie do odczytania. Ustaw Kopię Zapasową Kluczy lub wyeksportuj klucze pokoju do innej sesji przed resetowaniem hasła. + Kontynuuj + + Adres e-mail nie został połączony z kontem + + Sprawdź swoją skrzynkę + E-mail weryfikacyjny został wysłany do %1$s. + Naciśnij na link aby potwierdzić nowe hasło. Po naciśnięciu na link, który je zawiera, naciśnij poniżej. + Zweryfikowałem(-łam) swój adres e-mail + + Sukces! + Hasło zostało zresetowane. + Zostałeś(-łaś) wylogowany(-na) ze wszystkich sesji i nie będziesz otrzymywać powiadomień push. Aby re-aktywować powiadomienia, zaloguj się ponownie na każdym z urządzeń. + Powróć do logowania + + Ostrzeżenie + Hasło wciąż nie zostało zmienione. +\n +\nZatrzymać proces zmiany hasła\? + + Ustaw adres e-mail + Ustaw e-mail aby odzyskać konto. Później, opcjonalnie, będziesz w stanie pozwolić na odkrycie Ciebie za pomocą Twojego adresu e-mail. + E-mail + E-mail (nieobowiązkowy) + Dalej + + Ustaw numer telefonu + Ustaw numer telefonu, aby pozwolić innym na odkrycie Ciebie za pomocą numeru telefonu. + Użyj formatu międzynarodowego. + Numer telefonu + Numer telefonu (nieobowiązkowy) + Dalej + + Potwierdź numer telefonu + Wysłaliśmy kod do %1$s. Wprowadź go poniżej, aby potwierdzić, że to Ty. + Wprowadź kod + Wyślij ponownie + Dalej + + Międzynarodowe numery telefonów muszą zaczynać się od \'+\' + Numer telefonu wydaje się być nieprawidłowy. Sprawdź + + Zaloguj się do %1$s + Nazwa użytkownika lub e-mail + Nazwa użytkownika + Hasło + Dalej + Nazwa użytkownika zajęta + Ostrzeżenie + Twoje konto wciąż nie zostalo utworzone. +\n +\nZatrzymać proces rejestracji\? + + Wybierz matrix.org + Wybierz modular + Wybierz niestandardowy serwer domowy + Wypełnij zadanie Captcha + Zaakceptuj warunki aby kontynuować + + Sprawdź swój e-mail + Wysłaliśmy e-mail do %1$s. +\nProszę nacisnąć na link który zawiera aby kontynuować tworzenie konta. + Wprowadzony kod jest nieprawidłowy. Sprawdź. + Nieaktualny serwer domowy + Ten serwer domowy pracuje pod kontrolą zbyt starej wersji, aby się z nim połączyć. Zapytaj administratora serwera domowego o aktualizację. + + + Zostało wysłane zbyt wiele żądań. Możesz spróbować ponownie za %1$d sekundę… + Zostało wysłane zbyt wiele żądań. Możesz spróbować ponownie za %1$d sekundy… + + + + Wylogowałeś(-łaś) się + Mogło to się stać z wielu powodów: +\n +\n• Zmieniłeś(-łaś) swoje hasło na innej sesji. +\n +\n• Usunęłaś swoją sesję z innej sesji. +\n +\n• Administrator Twojego serwera unieważnił dostęp ze względów bezpieczeństwa. + Zaloguj ponownie + + Wylogowałeś(-łaś) się + Zaloguj się + Administaror twojego serwera domowego (%1$s) wylogował cię z konta %2$s (%3$s). + Zaloguj się aby odzyskać klucze szyfrowania przechowywane wyłącznie na tym urządzeniu. Będziesz ich potrzebował aby odczytać zaszyfrowane wiadomości na każdym z urządzeń. + Zaloguj się + Hasło + Wyczyść dane osobowe + Ostrzeżenie: Twoje dane osobowe (włączając w to klucze szyfrujące) są wciąż przechowywane na tym urządzeniu. +\n +\nWyczyść je, jeżeli skończyłeś(-łaś) używać tego urządzenia, lub chcesz zalogować się na inne konto. + Wyczyść wszystkie dane + + Wyczyść dane + Wyczyścić wszystkie dane przechowywane na tym urządzeniu\? +\nZaloguj się ponownie aby uzyskać dostęp do danych konta i wiadomości. + Utracisz dostęp do zaszyfrowanych wiadomości do czasu, aż zalogujesz się aby odzyskać Twoje klucze szyfrujące. + Wyczyść dane + Aktualna sesja jest dla użytkownika %1$s, podajesz natomiast dane dla użytkownika %2$s. Nie jest to wspierane przez RiotX. +\nNa początku usuń dane, następnie zaloguj ponownie na innym koncie. + + Link matrix.to został zdeformowany + Opis zbyt krótki + + Synchronizacja wstępna… + + Zobacz wszystkie sesje + Wściekłe potrząśnięcie + Próg detekcji + Potrząśnij telefonem aby wypróbować próg detekcji + Potrząśnięcie wykryte! + Aktualna sesja + Inne sesje + + Wyświetlanie jedynie początkowych wyników, wprowadź więcej znaków… + + Bezproblemowy + RiotX może zawieszać się częściej gdy napotka na niespodziewany błąd + + Żądanie weryfikujące podany userID + Preparuje ¯\\_(ツ)_/¯ dla zwykłej wiadomości tekstowej + + Aktywuj szyfrowanie + Odkąd zostanie włączone, szyfrowanie nie może zostać wyłączone. + + Twoja domena adresu e-mail nie została dopuszczona do rejestracji na tym serwerze + + Niezaufane logowanie + Zgadzają się + Nie zgadzają się + Zweryfikuj użytkownika poprzez potwierdzenie unikalnego ciągu emoji, w tym samym porządku na jego ekranie. + Dla najlepszego bezpieczeństwa, użyj innych zaufanych form komunikacji lub zrób to osobiście. + Patrz na zieloną tarczę, aby upewnić się czy użytkownik jest zaufany. Zaufaj wszystkim użytkownikom w pokoju, aby upewnić się, że pokój jest bezpieczny. + + Nie jest bezpieczny + Jeden z poniższych mogł zostać narażony: +\n +\n- Serwer domowy +\n- Serwer domowy użytkownika, z którym jest on połączony +\n- Połączenie z Internetem Twoje lub innych użytkowników +\n- Urządzenie Twoje lub innych użytkowników + + Wideo. + Zdjęcie. + Dźwięk + Plik + + Oczekiwanie… + %s anulowana + Anulowałeś(-łaś) + %s zaakceptowana + Zaakceptowałeś(-łaś) + Żądanie weryfikacji wysłane + Żądanie weryfikacji + + + Zweryfikuj tą sesję + Zweryfikuj ręcznie + + Ty + + Zeskanuj kod z urządzenia innego użytkownika aby bezpiecznie zweryfikować siebie nawzajem + Zeskanuj ich kod + Nie można zeskanować + Jeżeli nie jesteś z tą osobą, zamiast tego porównaj emoji + + Zweryfikuj porównując emoji + + Zweryfikuj za pomocą Emoji + Jeżeli nie możesz zeskanować kodu powyżej, zweryfikuj porównując krótki, unikalny ciąg emoji. + + Obraz kodu QR + + Zweryfikuj %s + Zweryfikowano %s + Oczekiwanie na %s… + Dla wyższego poziomu bezpieczeństwa, zweryfikuj %s poprzez sprawdzenie jednorazowego kodu na obu urządzeniach. +\n +\nDla najwyższego bezpieczeństwa, zrób to osobiście. + Wiadomości w tym pokoju są zaszyfrowane end-to-end. +\n +\nTwoje wiadomości są zabezpieczone zamkami i jedynie Ty oraz Twój odbiorca posiadacie klucze, aby je odblokować. + Dowiedz się więcej + + Jedna osoba + %1$d osób + + + Opuszczanie pokoju… + + Administratorzy + Moderatorzy + Niestandardowy + Zaproszenia + Użytkownicy + + Administrator w %1$s + Moderator w %1$s + Niestandardowy (%1$d) w %2$s + + Przeskocz do znacznika odczytania + + RiotX nie obsługuje wydarzeń typu \'%1$s\' + RiotX nie obsługuje wiadomości typu \'%1$s\' + RiotX napotkał problem przy wyświetlaniu zawartości wydarzenia z ID \'%1$s\' + + Nie ignoruj + + Sesja nie jest w stanie podzielić się weryfikacją z innymi sesjami. +\nWeryfikacja zostanie zapisana lokalnie i udostępniona w przyszłych wersjach aplikacji. + + Ostatnie pokoje + Inne pokoje + + Wysyła wiadomość w odcieniach tęczy + Wysyła emoji w odcieniach tęczy + + Oś czasu + + Edycja wiadomości + + Odkąd zostanie włączone, szyfrowanie nie może zostać wyłączone. + + Aktywować szyfrowanie\? + Odkąd zostanie włączone, szyfrowanie w pokoju nie może zostać wyłączone. Wiadomości wysłane w zaszyfrowanym pokoju nie są widzane przez serwer, a jedynie przez uczestników w pokoju. +\nAktywowanie szyfrowania może uniemożliwić wielu botom i mostom prawidłowe działanie. + Aktywuj szyfrowanie + + Aby być bezpiecznym, zweryfikuj %s poprzez sprawdzenie jednorazowego kodu. + Aby być bezpiecznym, zrób to osobiście lub użyj innej metody komunikacji. + + Porównaj unikalny ciąg emoji, upewniając się, że pojawiają się w identycznym porządku. + Porównaj kod wyświetlany na ekranie innego użytkownika. + Wiadomości z użytkownikiem są szyfrowane end-to-end i nie mogą zostać odczytane przez osoby trzecie. + Nowa sesja została zweryfikowana. Posiadasz dostęp do zaszyfrowanych wiadomości, a inni użytkownicy będą ją widzieć jako zaufaną. + + + Podpis krzyżowy + Podpis krzyżowy jest aktywowany. +\nKlucze Prywatne znajdują się na urządzeniu. + Podpis krzyżowy aktywowany. +\nKlucze są zaufane. +\nKlucze prywatne nie są znane + Podpis krzyżowy jest aktywowany. +\nKlucze nie są zaufane + Podpis krzyżowy nie jest aktywowany + + + Aktywne Sesje + Pokaż wszystkie Sesje + Zarządzaj Sesjami + Wyloguj z tej sesji + + Brak dostępnej informacji o kryptografii + + Ta sesja jest zaufana dla bezpiecznej wymiany wiadomości, ponieważ ją zweryfikowałeś(-łaś): + Zweryfikuj tą sesję aby oznaczyć ją jako zaufaną i przyznać jej dostęp do zaszyfrowanych wiadomości. Jeżeli nie logowałeś(-łaś) się do tej sesji, twoje konto mogło zostać naruszone: + + + %d aktywna sesja + %d aktywnych sesji + + + + Zweryfikuj tą sesję + Inni użytkownicy mogą jemu nie ufać + Całkowite Bezpieczeństwo + + Otwórz obecną sesję i użyj jej do zweryfikowania obecnej, przyznając jej dostęp do zaszyfrowanych wiadomości. + + + Zweryfkuj + Zweryfikowano + Ostrzeżenie + + Uzyskanie sesji nie powiodło się + Sesje + Zaufany + Niezaufany + + Sesja jest zaufana dla bezpiecznej wymiany wiadomości ponieważ %1$s (%2$s) zweryfikował(-a) ją: + %1$s (%2$s) zalogował(-a) się używając nowej sesji: + Dopóki użytkownik ufa tej sesji, wiadomości wysłane do oraz od niej będą oznaczone ostrzeżeniami. Ewentualnie, możesz zweryfikować je ręcznie. + + + Inicjalizacja podpisu krzyżowego + Zresetuj Klucze + + Kod QR + + Tak + Nie + + Narzędzia programistyczne + Dane konta + + %d głos + %d głosów + + + + %d głos - wyniki końcowe + %d głosów - wyniki końcowe + + + Wybrana Opcja + Tworzy prostą ankietę + Nie masz dostępu do instniejącej sesji\? + Użyj klucza odzyskiwania lub hasła + + Nowa rejestracja + + Nie można odnaleźć tajemnej przestrzeni w pamięci + Wprowadź hasło tajemnej przestrzeni + Ostrzeżenie: + Powinieneś(-nnaś) uzyskać dostęp do tajnej przestrzeni jedynie z zaufanego urządzenia + Uzyskaj dostęp do historii bezpiecznych wiadomości i Twojego(-jej) tożsamości podpisu krzyżowego poprzez weryfikację innej sesji za pomocą hasła + + Usuń… + Czy chcesz wysłać załącznik do %1$s\? + + Wyślij obraz w oryginalnym rozmiarze + Wyślij obrazy w oryginalnym rozmiarze + + + + Potwierdź Usunięcie + Jesteś pewny(-na), że chcesz usunąć to wydarzenie\? Jeżeli usuniesz nazwę pokoju lub zmienisz temat, wciąż będzie możliwe cofnięcie zmiany. + Podaj przyczynę + Przyczyna reakcji + + Wydarzenie usunięte przez użytkownika, przyczyna: %1$s + Wydarzenie moderowane przez administratora pokoju, przyczyna: %1$s + + Klucze są już aktualne! + + Sprawdź + Zrezygnuj + + Niezgodność klucza + Niezgodność użytkowników + Nie używasz Serwera Tożsamości + Nie skonfigurowano serwera toższamości, który jest wymagany do resetowania hasła. + + Wygląda na to, że próbujesz podłączyć się do innego serwera domowego. Czy chcesz się wylogować\? + + Przysyła zaproszenie + Zaproszono przez %s + + Nie masz więcej nieprzeczytanych wiadomości + Witaj w domu! + Reakcje + Zgoda + Lubię to + Zobacz Reakcje + Zdarzenie usunięte przez użytkownika + Zdarzenie moderowane przez administratora pokoju + Katalog Pokoi + Szybkie Reakcje + + Ogólne + Zasady push + Brak zarejestrowanych bramek push + + Szyfrowanie miniatury… + Nie możesz czegoś odnaleźć\? + Utwórz nowy pokój + Otwórz katalog pokoi + + Nazwa lub ID (#przykład:matrix.org) + + Serwer toższamości + Odłącz od serwera tożsamości + Skonfiguruj serwer tożsamości + Zmień serwer tożsamości + Aparat + Obecnie nie ma połączenia z siecią + + Zablokuj użytkownika + + Złóż sugestię + Tryb programisty aktywuje ukryte funkcje i może również spowodować, że aplikacja będzie mniej stabilna. Tylko dla programistów! + Wiadomości w tym pokoju nie są szyfrowane end-to-end. + Przesłane pliki diff --git a/vector/src/main/res/values-pt/strings.xml b/vector/src/main/res/values-pt/strings.xml index 3508d1fd9b..56417b9a36 100755 --- a/vector/src/main/res/values-pt/strings.xml +++ b/vector/src/main/res/values-pt/strings.xml @@ -1065,17 +1065,17 @@ A visibilidade das mensagens no Matrix é parecida com a dos emails. O nosso esq backup de chaves Backup de chaves, imcompleto. Por favor, Aguarde. - Backup de chaves, em progresso. Caso pagina seja fechada, irá perder acesso as suas mensagens encriptadas. + Backup de chaves, em progresso. Caso página seja fechada, irá perder acesso as suas mensagens encriptadas. Backup de chaves, em progresso … Utilize Backup de chaves. Tem a certeza? Fazer Backup - Irá perder acesso as suas mensagens encriptadas, se não fizer backup de suas chaves, antes de abandonar esta pagina. + Irá perder acesso as suas mensagens encriptadas, se não fizer backup de suas chaves, antes de abandonar esta página. Ficar Utilize Backup de chaves - Se sair desta pagina, irá perder as suas mensagens encriptadas + Se sair desta página, irá perder as suas mensagens encriptadas Não quero as minhas mensagens encriptadas Backup de chaves, em progresso … @@ -1120,7 +1120,7 @@ A visibilidade das mensagens no Matrix é parecida com a dos emails. O nosso esq Razão Configurações avançadas para notificações - Importancia de notificações por evento + Importância de notificações por evento Diagnóstico de Notificações Diagnóstico de falhas diff --git a/vector/src/main/res/values-ru/strings.xml b/vector/src/main/res/values-ru/strings.xml index 9fbcc509be..427eeee332 100644 --- a/vector/src/main/res/values-ru/strings.xml +++ b/vector/src/main/res/values-ru/strings.xml @@ -1297,17 +1297,17 @@ Не удалось удалить резервную копию (%s) Удаление резервной копии… - Для Безопасного Восстановления Сообщений на этом устройстве, требуется пароль или ключ восстановления. - Резервная копия имеет невалидную подпись верифицированного устройства %s - Резервная копия имеет валидную подпись неверифицированного устройства %s - Резервная копия имеет валидную подпись верифицированного устройства %s. - Резервная копия имеет валидную подпись этого устройства. - Резервная копия подписана устройством с идентификатором: %s. - Ваши ключи не сохраняются с этого устройства. + Для Восстановления Сообщений в этой сессии, требуется пароль или ключ восстановления. + Резервная копия имеет недействительную подпись из подтвержденной сессии %s + "Резервная копия имеет действительную подпись из неподтвержденной сессии %s" + Резервная копия имеет действительно подпись из подтверждённой сессии %s. + Резервная копия имеет действительную подпись с этой сессии. + Резервная копия подписана сессией с идентификатором %s. + Резервные копии ключей этой сессии не сохраняются. - Резервное копирование ключей не активно на этом устройстве. - Резервное копирование ключей успешно настроено для этого устройства. + Резервное копирование ключей не активировано в этой сессии. + Резервное копирование ключей успешно настроено для этой сессии. Удалить резервную копию Восстановить из резервной копии @@ -1335,7 +1335,7 @@ Резервная копия восстановлена %s ! Ошибка получения информации о доверии для резервной копии (%s). - Резервная копия имеет невалиндую подпись неверифицированного устройства %s + Резервная копия имеет недействительную подпись из неподтвержденной сессии %s Не удалось получить последнюю версию ключей восстановления (%s). %d новый ключ был добавлен к этому устройству. @@ -1349,7 +1349,7 @@ Восстановлены резервные копии с %d ключами. Восстановлены резервные копии с %d ключами. - Восстановлены %1$d сеансовые ключи и добавлены %2$d новые ключи, которые не были известны этому устройству + Восстановлены %1$d сессионные ключи и добавлены %2$d новые ключи, которые не были известны этой сессии Невозможно расшифровать резервную копию с помощью этого ключа восстановления: убедитесь, что вы ввели правильный ключ. Невозможно расшифровать резервную копию с помощью этой парольной фразы: убедитесь, что вы ввели верную фразу. @@ -1484,10 +1484,10 @@ \nИмя сессии: %1$s \nПоследний раз в сети: %2$s \nЕсли вы не вошли с другой сессии, проигнорируйте этот запрос. - Непроверенное устройство запрашивает ключи шифрования. -\nИмя устройства: %1$s -\nПоследний раз в сети: %2$s -\nЕсли вы не вошли с другого устройства, проигнорируйте этот запрос. + Непроверенная сессия запрашивает ключи шифрования. +\nИмя сессии: %1$s +\nПоследний раз в сети: %2$s +\nЕсли вы не открывали новую сессию - проигнорируйте этот запрос. Подтвердить Поделиться @@ -1544,7 +1544,7 @@ Неизвестная ошибка Резервная копия существует на домашнем сервере - Похоже, что у вас уже есть резервное копирование ключа настройки с другого устройства. Вы хотите заменить его на тот, который вы создаете\? + "Похоже, у вас уже есть резервная копия ключа настройки из другой сессии. Хотите заменить его новым\?" Заменить Стоп @@ -1811,4 +1811,21 @@ ID комнаты + Используйте менеджер интеграций чтобы управлять ботами, мостами, виджетами и наборами стикеров. +\nМенеджеры интеграций получают данные о конфигурации, могут изменять виджеты, отправлять приглашения в комнаты и устанавливать права от вашего имени. + "Использование может оставить cookie на вашем устройстве и отправить данные в %s:" + Использование может отправить данные в %s: + Не удалось загрузить виджет. +\n%s + Отозвать доступ для меня + + Ваше отображаемое имя + URL вашего аватара + Ваш ID + Данный виджет запрашивает доступ к следующим ресурсам: + Запретить все + Использовать камеру + Использовать микрофон + Получать доступ к медиа, защищённым DRM + diff --git a/vector/src/main/res/values-sq/strings.xml b/vector/src/main/res/values-sq/strings.xml index 7ff5253530..15164a6e90 100644 --- a/vector/src/main/res/values-sq/strings.xml +++ b/vector/src/main/res/values-sq/strings.xml @@ -2103,8 +2103,8 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani Për siguri ekstra, verifikojeni %s duke parë kontrolluar në të dy pajisjet tuaja një kod njëpërdorimsh. \n \nPër sigurinë maksimale, bëjeni këtë ju vetë. - RiotX (ende) nuk trajton akte të llojit \'%1$s\' - RiotX (ende) nuk trajton mesazhe të llojit \'%1$s\' + RiotX nuk trajton akte të llojit \'%1$s\' + RiotX nuk trajton mesazhe të llojit \'%1$s\' RiotX ndeshi një problem kur vizatohej lëndë e aktit me ID \'%1$s\' Ky sesion s’është në gjendje të ndajë këtë verifikim me sesionet tuaj të tjerë. @@ -2120,7 +2120,7 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani \nKyçet privatë nuk njihen Verifikojeni këtë sesion që t’i vihet shenjë si i besuar & dhe t’i akordohet hyrje te mesazhe të fshehtëzuar. Nëse s’keni bërë hyrjen në këtë sesion, llogaria juaj mund të jetë komprometuar: - Hapni një sesion ekzistues & përdoreni për të verifikuar këtë, duke i akorduar hyrje te mesazhe të fshehtëzuar. Nëse s’hyni dot te një, përdorni kyçin ose fjalëkalimin tuaj për rimarrje. + Hapni një sesion ekzistues & përdoreni për të verifikuar këtë, duke i akorduar hyrje te mesazhe të fshehtëzuar. Jo i Besuar @@ -2129,8 +2129,49 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani Gatit CrossSigning Zeroji Kyçet - A e skanoi me sukses përdoruesi tjetër kodin QR\? Jo Humbi lidhja me shërbyesin + Emër përdoruesi + %s i verifikuar + Mjete Zhvilluesi + Të dhëna Llogarie + + %d votë + %d vota + + + %d votë - Përfundimet finale + %d vota - Përfundimet finale + + Mundësi e Përzgjedhur + Krijoni një pyetësor të thjeshtë + S’hyni dot në një sesion ekzistues\? + Përdorni kyçin ose frazëkalimin tuaj të rimarrjes + + Hyrje e Re + + S’gjenden dot të fshehta në depozitim + Jepni frazëkalimin e fshehtë për në depozitim + Kujdes: + Duhet të hyni në depozitim të fshehtë vetëm nga një pajisje e besuar + Për verifikim sesionesh të tjerë përmes dhënies së frazëkalimit tuaj, hyni te historiku i mesazheve tuaj të sigurt dhe identiteti juaj për cross-signing + + Hiqni… + Doni të dërgohet kjo bashkëngjitje te %1$s\? + + Dërgoje figurën në madhësinë origjinale + Dërgoji figurat në madhësinë origjinale + + + Ripohoni Heqjen + Jeni i sigurt se doni të hiqet (fshihet) kjo veprimtari\? Kini parasysh se, nëse fshini emrin ose ndryshimin e temës së një dhome, ndryshimi mund të zhbëhet. + Përfshini një arsye + Arsye për redaktimin + + Veprimtari e fshirë nga përdorues, arsye: %1$s + Veprimtari e moderuar nga përgjegjësi i dhomës, arsye: %1$s + + Kyçet janë tashmë të përditësuar! + diff --git a/vector/src/main/res/values-tr/strings.xml b/vector/src/main/res/values-tr/strings.xml index 53a5264879..c4e2105154 100644 --- a/vector/src/main/res/values-tr/strings.xml +++ b/vector/src/main/res/values-tr/strings.xml @@ -7,8 +7,8 @@ Koyu Tema Siyah Tema - Eşzamanlama (senkronizasyon) - Olaylar dinleniyor + Senkronize ediliyor… + Etkinlikler dinleniyor Sesli bildirimler Sessiz bildirimler @@ -50,15 +50,15 @@ Yeniden Adlandır İçeriği bildir Şu anki görüşme - Devam eden konferans görüşmesi. -\n%1$s veya %2$s olarak katıl. - Sesli + Devam eden konferans görüşmesi. +\n%1$s veya %2$s olarak katıl + Sesli arama Görüntülü Görüşme başlatılamıyor, lütfen sonra tekrar deneyin Eksik izinler nedeni ile bazı özellikler eksik olabilir… Bu odada bir konferans başlatma davetiyesi göndermek için izniniz olması gerekmektedir Arama gerçekleştirilemiyor - Cihaz ayrıntıları + Oturum bilgileri Konferans görüşmeleri şifreli (encrypted) odalarda desteklenmiyor Ne olursa olsun gönder veya @@ -88,7 +88,7 @@ Odalar Topluluklar - Odaları ara + Oda adlarını filtrele Favorileri filtrele Kişileri filtrele Oda adlarını filtrele @@ -185,7 +185,7 @@ Anahtar yedekleme tamamlanmadı, lütfen bekleyiniz… Oturumu şimdi kapatırsanız şifrelenmiş mesajlarınızı kaybedeceksiniz Anahtar yedekleme işlemi sürüyor. Şimdi oturumu kapatırsanız tüm şifrelenmiş mesajlarınıza erişiminizi kaybedeceksiniz. - Şifrelenmiş mesajlarınızı kaybetmeyi önlemek için Güvenli Anahtar Yedekleme\'nin tüm cihazlarınızda aktif olması gerekmektedir. + Şifrelenmiş mesajlarınıza erişimi kaybetmemek için Güvenli Anahtar Yedekleme\'nin tüm oturumlarınızda etkin olması gerekir. Şifrelenmiş mesajlarımı istemiyorum Anahtarlar yedekleniyor… Anahtar Yedekleme Kullan @@ -225,7 +225,7 @@ Eğer yeni kurtarma yöntemini siz ayarlamadıysanız, bir saldırgan hesabını Yedeği Sil Yedeklenmiş şifreleme anahtarlarınız sunucudan silinsin mi? Bundan sonra şifrelenmiş mesaj geçmişini okumak için kurtarma anahtarınızı kullanamayacaksınız. - Bu cihazda Anahtar Yedekleme özelliğini kullanmak için şimdi parolanızla veya kurtarma anahtarınızla geri yükleyin. + Bu oturumda Anahtar Yedekleme özelliğini kullanmak için şimdi parolanızla veya kurtarma anahtarınızla geri yükleyin. Hata Sistem Uyarıları @@ -266,9 +266,9 @@ Eğer yeni kurtarma yöntemini siz ayarlamadıysanız, bir saldırgan hesabını Yeni şifre mutlaka girilmeli. Eposta %s adresine gönderildi. Epostadaki yönergeleri takip ettikten sonra, aşağıdaki tuşa tıklayın. Eposta doğrulaması başarısız: epostanızdaki bağlantıya tıkladığınızdan emin olun - Şifreniz sıfırlandı. -\n -\nTüm cihazlarınızdan çıkış yaptınız ve mesaj bildirimleri almayacaksınız. Tekrar bildirimleri alabilmek için her cihazda tekrar giriş yapmalısınız. + Şifreniz sıfırlandı. +\n +\nTüm oturumlardan çıkış yaptınız ve mesaj bildirimleri almayacaksınız. Tekrar bildirimleri alabilmek için her cihazda tekrar giriş yapmalısınız. Lütfen anamakinenin ilkelerini gözden geçirin ve kabul edin: URL http[s]:// ile başlamalıdır @@ -288,16 +288,16 @@ Eğer yeni kurtarma yöntemini siz ayarlamadıysanız, bir saldırgan hesabını Kullanıcı adı zaten kullanımda Eposta bağlantısına hala tıklanmadı - Bu cihaza özel uçtan uca şifreleme anahtarı oluşturup, herkese açık anahtarı anamakineye yüklemek için giriş yapmanız gerekli. -\nBu bir kereye mahsus. + Bu oturuma özel uçtan uca şifreleme anahtarı oluşturup, herkese açık anahtarı anamakineye yüklemek için giriş yapmanız gerekli. +\nBu bir kereye mahsus. \nRahatsızlık için üzgünüz. - Diğer cihazlarından şifreleme anahtarlarını tekrar iste. + Diğer oturumlardan şifreleme anahtarlarını tekrar iste. Anahtar isteği gönderildi. İstek gönderildi - Riot\'u mesajları çözebilen farklı bir cihazda açarsanız ordan anahtarları bu cihaza gönderebilirsiniz. + Riot\'u mesajları çözebilen farklı bir cihazda açarsanız ordan anahtarları bu oturuma gönderebilirsiniz. Makbuz Listesini Oku @@ -309,7 +309,7 @@ Eğer yeni kurtarma yöntemini siz ayarlamadıysanız, bir saldırgan hesabını Farklı gönder - Orjinal + Orijinal Büyük Orta Küçük @@ -364,12 +364,10 @@ Eğer yeni kurtarma yöntemini siz ayarlamadıysanız, bir saldırgan hesabını Riot\'un görüntülü arama yapması için kameranıza ve mikrofonunuza erişmeye ihtiyacı var. \n \nLütfen çıkacak ekranda kamera ve mikrofona erişebilmesine izin verin. - Riot\'un eposta ve telefon numaralarına göre diğer Matrix kullanıcılarını bulmak için rehberinize erişmeye ihtiyacı var. -\n -\nLütfen çıkacak ekranda rehberinize erişebilmesine izin verin. - Riot\'un eposta ve telefon numaralarına göre diğer Matrix kullanıcılarını bulmak için rehberinize erişmeye ihtiyacı var. -\n -\nRiot rehberinize erişebilsin mi\? + Riot eposta ve telefon numaralarına göre diğer Matrix kullanıcılarını bulmak için rehberinizi kontrol edebilir. Eğer bu nedenle rehberinizi paylaşmak istiyorsanız, lütfen açılan ekranda erişime izin verin. + Riot\'un eposta ve telefon numaralarına göre diğer Matrix kullanıcılarını bulmak için rehberinizi kontrol edebilir. +\n +\nRehberinizi bu sebeple paylaşmayı kabul ediyor musunuz\? Üzgünüz. İsteğiniz, yetersiz izinlerden dolayı gerçekleştirilemedi @@ -437,7 +435,7 @@ Eğer yeni kurtarma yöntemini siz ayarlamadıysanız, bir saldırgan hesabını YÖNETİCİ ARAÇLARI ARAMA DİREKT SOHBETLER - CİHAZLAR + OTURUMLAR Davet Odadan ayrıl @@ -455,7 +453,7 @@ Eğer yeni kurtarma yöntemini siz ayarlamadıysanız, bir saldırgan hesabını \nBu işlem uygulamayı yeniden başlatak ve biraz zaman alacak. Kullanıcı ID, Ad ya da eposta Bahset - Cihaz Listesini Göster + Oturum Listesini Göster Bu işlemin geri dönüşü yok kullanıcıyı sen ile aynı erişim seviyesine getiriyorsun. \nEmin misin\? @@ -490,7 +488,7 @@ Eğer yeni kurtarma yöntemini siz ayarlamadıysanız, bir saldırgan hesabını Cevap gönder (şifrelenmemiş)… Sunucuya olan bağlantı koptu. Mesaj gönderilemedi. %1$s ya da %2$s yapılsın mı\? - Mesaj gönderilemedi çünkü bilinmeyen bir cihaz belirlendi. %1$s ya da %2$s yapalım mı\? + Mesaj gönderilemedi çünkü bilinmeyen bir oturum belirlendi. %1$s ya da %2$s yapalım mı\? Hepsini tekrar gönder Hepsini iptal et Gönderilmeyen mesajları tekrar gönder @@ -619,9 +617,9 @@ Eğer yeni kurtarma yöntemini siz ayarlamadıysanız, bir saldırgan hesabını \nLütfen hesap ayarlarınızı gözden geçirin. Etkinleştir - Cihaz Ayarları. - Bildirimler bu cihaz için etkinleştirilmiş. - Bildirimler bu cihaz için etkin değil. + Oturum Ayarları. + Bildirimler bu oturum için etkinleştirilmiş. + Bildirimler bu oturum için etkin değil. \nLütfen Riot ayarlarını gözden geçirin. Etkinleştir @@ -680,13 +678,13 @@ Eğer yeni kurtarma yöntemini siz ayarlamadıysanız, bir saldırgan hesabını Pil Optimizasyonu Riot Pil Optimizasyonundan etkilenmedi. - Eğer kullanıcı cihazını prize bağlanmamış sabit ve ekranı kapalı bir şekilde bir süre bırakırsa cihaz Derin uyku moduna geçer. Bu uygulamaların internete erişmesini engeller ve yapılacak işlerini, senkronizasyolarını, alarmını erteler + Eğer kullanıcı cihazını prize bağlanmamış sabit ve ekranı kapalı bir şekilde bir süre bırakırsa cihaz Derin uyku moduna geçer. Bu uygulamaların internete erişmesini engeller ve yapılacak işlerini, senkronizasyolarını, alarmını erteler. Optimizasyonu Göz ardı et Normal Azaltılmış gizlilik Uygulamanın arkaplanda çalışması için izne ihtiyacı var - • Bildirimler Google Bulut Mesajlaşma ile gönderildi + • Bildirimler Firebase Bulut Mesajlaşma ile gönderildi • Bildirimler sadece meta verileri içeriyor • Bildirimin mesaj içeriği direkt Matrix anamakinesinden güvenli bir şekilde bulundu • Bildirimler meta ve mesaj verileri içeriyor @@ -694,7 +692,7 @@ Eğer yeni kurtarma yöntemini siz ayarlamadıysanız, bir saldırgan hesabını Bildirim sesi Bildirim seslerini bu hesap için etkinleştir - Bildirim seslerini bu cihaz için etkinleştir + Bildirim seslerini bu oturum için etkinleştir Ekranı 3 saniyeliğine açık tut Sesli Bildirimleri Ayarla Arama Bildirimlerini Ayarla @@ -714,7 +712,7 @@ Eğer yeni kurtarma yöntemini siz ayarlamadıysanız, bir saldırgan hesabını Açılışta başlat Arkaplanda eşzamanlamayı etkinleştir Eşzamanlama talep süresi doldu - Her talep arasında gecikme + Her senkronizasyon arası gecikme saniye saniye @@ -742,7 +740,7 @@ Eğer yeni kurtarma yöntemini siz ayarlamadıysanız, bir saldırgan hesabını Ana ekran Bildirimlerini kaçırdığın odaları öne çıkar Okunmamış mesajlı odaları öne çıkar - Cihazlar + Oturumlar Satır içi URL önizlemesi Bağlantıları anamakine destekliyorsa sohbette önizle. Yazıyor bildirimleri gönder @@ -782,10 +780,10 @@ Eğer yeni kurtarma yöntemini siz ayarlamadıysanız, bir saldırgan hesabını Veri kaydetme modu Veri kaydetme modu belirli filtreler uygular bu sayede ilerki güncellemeler ve yazıyor bildirimleri filtrelenir. - Cihaz detayları + Oturum bilgileri ID - Ad - Cihaz Adı + Görünür Ad + Görünür Adı Güncelle Son görülme %1$s @ %2$s Bu işlem ek yetki gerektiriyor. @@ -812,9 +810,9 @@ Eğer yeni kurtarma yöntemini siz ayarlamadıysanız, bir saldırgan hesabını Şifre Şifreni değiştir - Eski şifre + Mevcut şifre Yeni şifre - Tekrar şifre + Yeni şifreyi onayla Şifreyi güncellerken hata oluştu Şifren güncellendi %s den/dan tüm mesajlar gösterilsin mi\? @@ -885,8 +883,8 @@ Eğer yeni kurtarma yöntemini siz ayarlamadıysanız, bir saldırgan hesabını Uçtan-uca Şifreleme Uçtan-uca Şifreleme etkin Şifrelemeyi etkinleştimek için çıkış yapman gerekiyor. - Sadece doğrulanmış cihazlara şifrele - Asla doğrulanmamış cihaz olan oda da şifrelenmiş mesaj gönderme. + Sadece doğrulanmış oturumlara şifrele + Asla doğrulanmamış oturum olan oda da şifrelenmiş mesaj gönderme. Bu oda yerel adrese sahip değil Yeni adres (örn. #foo:matrix.org) @@ -919,7 +917,7 @@ Eğer yeni kurtarma yöntemini siz ayarlamadıysanız, bir saldırgan hesabını Uçtan-uca şifreleme bilgilendirmesi - Olay Bilgilendirmesi + Etkinlik Bilgilendirmesi Kullanıcı idsi Curve25519 kimlik anahtarı Alınmış Ed25519 parmak izi anahtarı @@ -927,11 +925,11 @@ Eğer yeni kurtarma yöntemini siz ayarlamadıysanız, bir saldırgan hesabını Oturum ID\'si Çözme hatası - Göndericinin cihaz bilgisi - Cihaz adı - Ad - Cihaz ID\'si - Cihaz anahtarı + Göndericinin oturum bilgisi + Görünür Ad + Görünür Ad + Kimlik + Oturum anahtarı Doğrulama Ed25519 parmak izi @@ -951,15 +949,15 @@ Eğer yeni kurtarma yöntemini siz ayarlamadıysanız, bir saldırgan hesabını Oda anahtarlarını içe aktar Oda anahtarını yerel dosyadan içe aktar İçe aktar - Sadece doğrulanmış cihazlara şifrele - Asla bu cihazdan doğrulanmamış cihazlara şifrelenmiş mesaj gönderme. + Sadece doğrulanmış oturumlara şifrele + Asla bu cihazdan doğrulanmamış oturumlara şifrelenmiş mesaj gönderme. %1$d/%2$d anahtar başarı ile içe aktarıldı. Doğrulanmamış Doğrulanmış Karalistede - bilinmeyen cihaz + bilinmeyen oturum yok Doğrula @@ -967,21 +965,21 @@ Eğer yeni kurtarma yöntemini siz ayarlamadıysanız, bir saldırgan hesabını Karalisteye ekle Karalisteden sil - Cihazı doğrula - Bu cihazın güvenilir olduğunu doğrulamak için, lütfen sahibi ile iletişime geçin (örn. yüz yüze ya da telefonla arama) ve onlara Kullanıcı Ayarları altında hangi anahtarın aşağıdaki anahtar ile uyuştuğunu sorun: - Eğer uyuşursa aşağıdaki doğrulama tuşuna basın. Eğer uyuşmaz ise biri bu cihazı engelliyor demektir bu durumda sen o kişi karalisteye eklemelisin. Gelecekte doğrulama işlemi daha gelişmiş olacak. + Oturumu doğrula + Bu oturumun güvenilir olduğunu doğrulamak için, lütfen sahibi ile iletişime geçin (örn. yüz yüze ya da telefonla arama) ve onlara Kullanıcı Ayarları altında hangi anahtarın aşağıdaki anahtar ile uyuştuğunu sorun: + Eğer uyuşursa aşağıdaki doğrulama tuşuna basın. Eğer uyuşmaz ise biri bu oturum engelliyor demektir bu durumda sen o kişi karalisteye eklemelisin. Gelecekte doğrulama işlemi daha gelişmiş olacak. Anahtarın uyuştuğunu doğruluyorum - Riot artık uçtan-uca şifrelemeyi destekliyor ancak etkinleştirmen için tekrar giriş yapman gerekli. -\n -\nBunu şimdi yada sonra uygulama ayarlarından yapabilirsin. + Riot artık uçtan-uca şifrelemeyi destekliyor ancak etkinleştirmen için tekrar giriş yapman gerekli. +\n +\nBunu şimdi ya da sonra uygulama ayarlarından yapabilirsin. - Oda bilinmeyen cihaz barındırıyor - Bu oda doğrulanmamış bilinmeyen cihaz barındırıyor. -\nBu cihazların kullanıcıların sahip olduğu cihaz olduklarının garantisi yok. -\nDevam etmeden önce her cihaz için doğrulama işlemi yapmanızı öneririz, fakat isterseniz mesajları doğrulamadan da tekrar gönderebilirsiniz. -\n -\nBilinmeyen Cihazlar: + Oda bilinmeyen oturum barındırıyor + Bu oda doğrulanmamış bilinmeyen oturum barındırıyor. +\nBu oturumların kullanıcıların sahip olduğu cihaz olduklarının garantisi yok. +\nDevam etmeden önce her oturum için doğrulama işlemi yapmanızı öneririz, fakat isterseniz mesajları doğrulamadan da tekrar gönderebilirsiniz. +\n +\nBilinmeyen Oturumlar: Bir oda dizini seç Sunucu müsait değil ya da aşırı yüklenmiş @@ -1045,8 +1043,8 @@ Eğer yeni kurtarma yöntemini siz ayarlamadıysanız, bir saldırgan hesabını Sesli mesaj gönder Bu seçenek mesajları kaydetmek için üçüncü parti bir uygulama gerektiriyor. - %s adlı şifreleme anahatarlarını isteyebilen yeni bir cihaz eklediniz. - %s adlı doğrulanmamış cihaz şifreleme anahtarlarını istiyor. + %s adlı şifreleme anahatarlarını isteyebilen yeni bir oturum eklediniz. + %s adlı doğrulanmamış oturum şifreleme anahtarlarını istiyor. Doğrulamayı başlat Doğrulamadan paylaş İsteği göz ardı et @@ -1236,11 +1234,11 @@ Neden Riot.im’i seçmeliyim? Lütfen bir kopya oluşturun Kurtarma anahtarını bununla paylaş… - Parola kullanılarak Kurtarma anahtarı oluşturuluyor bu işlem bir kaç saniye sürecek. + Parola kullanılarak Kurtarma anahtarı oluşturuluyor bu işlem birkaç saniye sürecek. Kurtarma Anahtarı Beklenmeyen hata Yedekleme Başlatıldı - Şifreleme anahtarın arkaplanda anamakineye yedeklenecek. İlk yedekleme bir kaç dakika sürecek. + Şifreleme anahtarın arkaplanda anamakineye yedeklenecek. İlk yedekleme birkaç dakika sürecek. Emin misin\? @@ -1269,14 +1267,14 @@ Neden Riot.im’i seçmeliyim? Yedekleme bu kurtarma anahtarı ile çözülemez: lütfen doğru kurtarma anahtarını girdiğinizi doğrulayın. Yedekleme İşlendi %s ! - %1$d oturum anahtarı onarıldı ve bu cihaz tarafından bilinmeyen %2$d yeni anahtar eklendi + %1$d oturum anahtarı onarıldı ve bu oturum tarafından bilinmeyen %2$d yeni anahtar eklendi Yedekleme %d anahtarı ile işlendi. Yedekleme %d anahtarları ile işlendi. - %d tane yeni anahtar bu cihaza eklendi. - %d tane yeni anahtar bu cihaza eklendi. + %d yeni anahtar bu oturuma eklendi. + %d tane yeni anahtar bu oturuma eklendi. Son sürüm onarma anahtarı alınırken hata oluştu (%s). @@ -1286,16 +1284,16 @@ Neden Riot.im’i seçmeliyim? Yedeklemeden onar Yedeklemeyi Sil - Anahtar Yedekleme bu cihaz için doğru bir şekilde ile ayarlandı. - Anahtar Yedekleme bu cihazda etkin değil. - Anahtarlarınız bu cihazdan yenilenemiyor. + Anahtar Yedekleme bu oturum için doğru bir şekilde ile ayarlandı. + Anahtar Yedekleme bu oturumda etkin değil. + Anahtarlarınız bu oturumdan yenilenemiyor. - Yedekleme %s ID\'li bilinmeyen cihazdan gelen bir imzaya sahip. - Yedekleme bu cihazdan geçerli imzaya sahip. - Yedekleme %s adlı doğrulanmış cihazdan geçerli imzaya sahip. - Yedekleme %s adlı doğrulanmamış cihazdan geçerli imzaya sahip - Yedekleme %s adlı doğrulanmış cihazdan geçersiz imzaya sahip - Yedekleme %s adlı doğrulanmamış cihazdan geçersiz imzaya sahip + Yedekleme %s ID\'li bilinmeyen oturumdan gelen bir imzaya sahip. + Yedekleme bu oturumdan geçerli imzaya sahip. + Yedekleme %s adlı doğrulanmış oturumdan geçerli imzaya sahip. + Yedekleme %s adlı doğrulanmamış oturumdan geçerli imzaya sahip + Yedekleme %s adlı doğrulanmış oturumdan geçersiz imzaya sahip + Yedekleme %s adlı doğrulanmamış oturumdan geçersiz imzaya sahip Yedekleme için güven bilgisi alınırken hata oluştu (%s). @@ -1307,26 +1305,268 @@ Neden Riot.im’i seçmeliyim? Algoritma İmza - İşlem başlatılıyor - Cihazı doğrula + Hizmet başlatılıyor + Oturumu doğrula Hiç (Yok) Geri Al Bağlantıyı Kes - Önemseme + Yoksay Gözden Geçir Reddet Okunmuş olarak işaretle - Arama yanlış yapılandırılmış odadan dolayı başarısız oldu + Arama yanlış yapılandırılmış sunucudan dolayı başarısız oldu %s kullanmayı deneyin - Yeniden sorma + Tekrar sorma Tek oturum açma ile giriş yap - Hesap kurtarması için email ayarla, ve sonradan da isteğe bağlı olarak başklarının seni tanıyan kişilerin bulması için kullan. + Hesap kurtarma epostası kaydedin ve daha sonra isteğe bağlı olarak sizi tanıyan kişilerin sizi bulabilmesini sağlayın. Bu adrese erişilemiyor, lütfen kontrol et - Bu uygulamanın arkaplanda anasunucuya bağlanması gerekmez, bu durum batarya kullanımını azaltır. + Bu uygulamanın arkaplanda anasunucuya bağlanması gerekmez, bu durum batarya kullanımını azaltır Enter ile mesaj gönder Klavyedeki Enter tuşuna bastığında alt satıra geçmek yerine mesajı gönder + Latn + + Hiçbir kimlik sunucusu yapılandırılmamış. + + Lütfen çağrıların düzgün çalışması için anasunucuzun (%1$s) yetkilisine DÖNÜŞ sunucusunu yapılandırmasını söyleyin. +\n +\nAlternatif olarak %2$s herkese açık sunucuyu kullanmayı da deneyebilirsiniz, ancak bu o kadar da güvenilir olmayacak ve IP adresiniz sunucu ile paylaşılacaktır. Bunu ayrıca Ayarlardan da düzenleyebilirsiniz. + Telefon numarası kaydedin ve daha sonra isteğe bağlı olarak sizi tanıyan kişilerin sizi bulabilmesini sağlayın. + Hesap kurtarma epostası kaydedin. Daha sonra isteğe bağlı eposta veya telefon nuraması kullanarak sizi tanıyan kişilerin sizi bulabilmesini sağlayın. + Hesap kurtarma epostası kaydedin. Daha sonra isteğe bağlı eposta veya telefon nuraması kullanarak sizi tanıyan kişilerin sizi bulabilmesini sağlayın. + Cihazın saldırılara açık, eski bir TLS güvenlik protokolü kullanıyor; güvenliğin için bağlantın engellendi + İkincil çağrı yardımcı sunucusuna izin ver + Anasunucunuz çağrı yardımcı sunucusu vermez ise %s çağrı yardımcı sunucusu olarak kullanılacaktır (Çağrıda IP adresiniz paylaşılacaktır) + Bu eylemi gerçekleştirebilmek için ayarlarınızdan bir kimlik sunucusu ekleyin. + Şifreni doğrula + Riot, cihazın sınırlı kaynaklarını (pil) koruyacak şekilde arka planda senkronize olur. +\nCihazınızın kaynak durumuna bağlı olarak, senkronizasyon işletim sistemi tarafından ertelenebilir. + Gerçek zamanlı için optimize + Riot periyodik olarak belirli bir zamanda (ayarlanabilir) arka planda senkronize olur. +\nBu pil ve radyo kullanımını etkileyecek ve riotun olayları dinlediğini belirten kalıcı bir bildirim gösterecektir. + Arka plan senkronizasyonu yok + Tercih Edilen Senkronize Aralığı + %s +\nSenkronizasyon kaynağın (pil) yada cihazın durumuna (uyku) bağlı olarak değişebilir. + Entegrasyonlar + Botları, köprüleri, widget\'ları ve etiket paketlerini yönetmek için bir entegrasyon yöneticisi kullanın. +\nEntegrasyon yöneticileri yapılandırma verilerini alır ve widget\'ları değiştirebilir, oda davetlerini gönderebilir ve sizin adınıza yetki seviyelerini ayarlayabilir. + Keşif + Keşif ayarlarını düzenle. + Entegrasyonlara izin ver + Entegre Yöneticisi + + Şifreyi güncelle + Şifre geçersiz + Şifreler uyuşmuyor + + Medya + Varsayılan sıkıştırma + Seç + Varsayılan medya kaynağı + Seç + Deklanşör sesi çal + + Görünür ad (iletişim kurduğunuz kişilere görünen) + Bir oturumun görünür adı iletişim kurduğunuz kişilere görünür + bilinmeyen ip + + %1$s: 1 mesaj + %1$s: %2$d tane mesaj + + + %d bildirim + %d tane bildirim + + + Yeni Etkinlik + Oda + Yeni Mesajlar + Yeni Davet + Ben + ** Görderme başarısız - lütfen alan aç + + Widget + Widget Yükle + Bu widget\'ı ekleyen: + Bunu kullanmak çerezler oluşturabilir ve %s ile veri paylaşabilir: + Bunu kullanmak %s ile veri paylaşabilir: + Widget yüklenemedi. +\n%s + Widget\'ı yenile + Tarayıcıda Aç + Benim için erişimi iptal et + + Görünür adın + Avatarının URL\'si + Kullanıcı kimliğin + Teman + Widget kimliği + Oda kimliği + + + Üzgünüz, jitsi konferans aramaları eski cihazlarda desteklenmiyor (Android 5.0 altı) + Bu widget şu kaynakları kullanmak istiyor: + İzin ver + Hepsini reddet + Kamera kullan + Mikrofon kullan + DRM korumalı içerik oynat + + Hiçbir entegrasyon düzenleyicisi yapılandırılmamış. + Devam etmek için Hizmet Şartlarını kabul etmeniz gerekir. + + Yeni oturum şifreleme anahtarlarını talep ediyor. Oturum adı: %1$s +\nSon görülme: %2$s +\nEğer farklı bir oturumda giriş yapmadıysanız bu talebi reddedin. + Bir doğrulanmamış oturum şifreleme anahtarlarını talep ediyor. +\nOturum adı: %1$s +\nSon görülme: %2$s +\nEğer farklı bir oturumda giriş yapmadıysanız bu talebi reddedin. + + Doğrula + Paylaş + Anahtar Paylaşma Talebi + Reddet + + Ana sunucunuzda bir yedek zaten var + Başka bir oturumda zaten kurum anahtarı yapmışsın gibi görünüyor. Bunları şu anki oluşturulanlar ile değiştirmek ister misin\? + Değiştir + Dur + + Yedek durumu kontrol ediliyor + Geçersiz anasunucu keşif cevabı + Sunucu Ayarlarını Otomatik Doldur + Riot userld alan adı için özel sunucu yapılandırması buldu \"%1$s\": +\n%2$s + Yapılandırmayı kullan + + Geçersiz ya da süresi dolmuş girdilerden dolayı çıkış yaptınız. + + Kısa metin dizinisi karşılaştırarak doğrulayın. + Yüksek güvenlik için bunu yüz yüze ya da farklı güvenilir iletişim yolu kullanarak yapmanızı tavsiye ederiz. + Doğrulamaya Başla + Gelen Doğrulama Talebi + Güvenilir olarak işaretlemek için bu oturumu doğrulayın. Partnerlerin oturumlarına güvenmek, uçtan uca şifreli mesajlar kullanırken size ekstra huzur verir. + Bu oturumun doğrulanmak oturumu kendinizde ve ayrıca partnerinizde güvenilir olarak işaretleyecektir. + + Partnerinizin ekranında aşağıdaki emojinin göründüğünü onaylayarak bu oturumu doğrulayın + Partnerinizin ekranında aşağıdaki sayıların göründüğünü onaylayarak bu oturumu doğrulayın + + Gelen bir doğrulama talebi aldın. + Talebi görüntüle + Partnerinizin kabul etmesi bekleniyor… + + Doğrulandı! + Bu oturumu başarıyla doğruladın. + Bu kullanıcı ile güvenli mesajlar uçtan uca şifrelenir ve üçüncü şahıslar tarafından okunamaz. + Anlaşıldı + + Hiçbir şey gözükmüyor mu\? Şimdilik bütün platformlar interaktif doğrulamayı desteklemiyor. Eski doğrulamayı kullanın. + Eski doğrulama yöntemini kullan. + + Anahtar Doğrulaması + Talep İptal edildi + Diğer grup doğrulamayı iptal etti. +\n%s + Doğrulama iptal edildi. +\nSebep: %s + + Etkileşimli Oturum Doğrulama + Doğrulama Talebi + %s oturumunu doğrulamak istiyor + + Kullanıcı doğrulamayı iptal etti + Doğrulama işlemi zaman aşımına uğradı + Oturum bu işlemi bilmiyor + Oturum anahtar anlaşması, hash, MAC, yada SAS yöntemi üzerinde anlaşamaz + Hash tahaddütü eşleşmedi + SAS eşleşmedi + Oturum beklenmeyen bir mesaj aldı + Geçersiz bir mesaj alındı + Anahtar uyuşmazlığı + Kullanıcı uyuşmazlığı + Bilinmeyen Hata + + Herhangi bir kimlik sunucusu kullanmıyorsunuz + Hiçbir kimlik sunucusu yapılandırılmamış, bu şifrenizi değiştirebilmeniz için gerekli. + + Riot\'un önceki sürümlerinde, kimlik sunucunuzun (%1$s) hesabınıza erişebilmesini sağlayan bir güvenlik hatası vardı. %2$s sunucusuna güveniyorsanız, bunu göz ardı edebilirsiniz; aksi takdirde lütfen çıkış yapın ve tekrar giriş yapın. +\n +\nDaha fazla ayrıntıya buradan bakabilirsiniz: +\nhttps://medium.com/@RiotChat/36b4792ea0d6 + + Başka bir anasunucuya bağlanmaya çalışıyorsunuz gibi görünüyor. Çıkış yapmak ister misiniz\? + + Düzenle + Yanıtla + + Tekrar dene + Uygulamayı kullanmaya başlamak için bir odaya katıl. + Sana bir davet gönderdi + %s seni davet etti + + Her şey tamam! + Hiç okunmamış mesajınız bulunmuyor + Eve hoşgeldiniz! + Okunmamış mesajları burada bulursun + Tartışmalar + Doğrudan mesajların burada görünecek + Odalar + Odalar burada görünecek + + Tepkiler + Bencede + Beğendim + Bir Tepki Ekle + Tekpileri Görüntüle + Tepkiler + + Etkinlik kullanıcı tarafından silindi + Etkinlik oda yöneticisi tarafından yönetildi + %2$s tarihinde %1$s tarafından düzenlendi + + + Hatalı etkinlik, görüntülenemiyor + Yeni Oda Oluştur + Internet yok. Lütfen internet bağlantını kontrol et. + Değiştir + Ağı değiştir + Lütfen bekleyin… + Tüm Topluluklar + + Bu oda önizlenemez + RiotX henüz herkese-açık odaları önizlemeyi desteklemiyor + + Odalar + Doğrudan Mesajlar + + Yeni Oda + OLUŞTUR + Oda Adı + Herkese Açık + Herkes bu odaya katılabilir + Oda Dizini + Bu odayı oda dizininde yayınla + + Güven bilgisi alınırken hata oluştu + Anahtar yedek verileri alınırken hata oluştu + + Betaya hoşgeldiniz! + RiotX geliştirme sürecindeyken bazı özellikler eksik olabilir ve hatalar ile karşılaşabilirsiniz. + Bu geçerli bir Matrix sunucu adresi değil + Bu URL ile ev-sunucusuna erişilemiyor, lütfen kontrol edin + Bunu Riot mobil ile yapamazsınız + Kimlik doğrulama gereklidir + + + Arkaplan Senkronizasyon Modu (Deneysel) + Batarya için optimize edildi + Uygulama arka plandayken gelen mesajlar için uyarılmayacaksınız. + Ayarlar güncellenemedi. + + diff --git a/vector/src/main/res/values-zh-rCN/strings.xml b/vector/src/main/res/values-zh-rCN/strings.xml index ea1506ca99..4311c2cc24 100644 --- a/vector/src/main/res/values-zh-rCN/strings.xml +++ b/vector/src/main/res/values-zh-rCN/strings.xml @@ -1373,7 +1373,7 @@ Riot 在后台时的工作将被显著的限制,这可能会影响消息通知 抱歉,旧设备 (Android 系统版本低于 5.0)不支持使用 Jitsi 创建电话会议 - 验证设备 + 验证会话 未知 IP 一个新设备正在请求加密密钥。 diff --git a/vector/src/main/res/values-zh-rTW/strings.xml b/vector/src/main/res/values-zh-rTW/strings.xml index 8078ca8efa..479265bb22 100644 --- a/vector/src/main/res/values-zh-rTW/strings.xml +++ b/vector/src/main/res/values-zh-rTW/strings.xml @@ -2033,8 +2033,8 @@ Matrix 中的消息可見度類似于電子郵件。我們忘記您的郵件意 跳至讀取回條 - RiotX 尚無法處理類型為「%1$s」的事件 - RiotX 尚無法處理類型為「%1$s」的訊息 + RiotX 無法處理類型為「%1$s」的事件 + RiotX 無法處理類型為「%1$s」的訊息 在彩現 id「%1$s」事件的內容時,RiotX 遇到問題 取消忽略 @@ -2097,7 +2097,7 @@ Matrix 中的消息可見度類似于電子郵件。我們忘記您的郵件意 其他使用者可能不會信任它 全面的安全性 - 開啟既有的工作階段並使用它來驗證這個,讓它可以存取已加密的訊息。如果您無法存取,請使用您的復原金鑰或通關密語。 + 開啟既有的工作階段並使用它來驗證這個,讓它可以存取已加密的訊息。 驗證 @@ -2119,9 +2119,46 @@ Matrix 中的消息可見度類似于電子郵件。我們忘記您的郵件意 QR code - 其他使用者是否掃苗 QR code 成功? 到伺服器的連線已遺失 + 使用者名稱 + 開發者工具 + 帳號資料 + + %d 投票 + + + %d 投票 - 最後結果 + + 已選取的選項 + 建立簡易投票 + 無法存取既有的工作階段? + 使用您的復原金鑰或通關密語 + + 新登入 + + 在儲存空間中找不到秘密 + 輸入秘密儲存空間通關密語 + 警告: + 您僅能從受信任的裝置存取秘密儲存空間 + 透過輸入通關密語來存取您的安全訊息歷史與您的交叉簽章身份以驗證其他工作階段 + + 移除…… + 您想要傳送此附件到 %1$s 嗎? + + 使用原始大小傳送圖片 + + + 確認移除 + 您確定您想要移除(刪除)此活動嗎?注意,如果您刪除聊天室名稱或變更主題,則可能會撤銷變更。 + 包含理由 + 修改原因 + + 被使用者刪除的活動,理由:%1$s + 由聊天室管理員管理的活動,理由:%1$s + + 金鑰已為最新! + diff --git a/vector/src/main/res/values/attrs.xml b/vector/src/main/res/values/attrs.xml index 73b741d74b..27d53fe90e 100644 --- a/vector/src/main/res/values/attrs.xml +++ b/vector/src/main/res/values/attrs.xml @@ -105,4 +105,13 @@ + + + + + + + + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 6c56211e57..6b0591595a 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -301,7 +301,7 @@ You need to log back in to generate end-to-end encryption keys for this session and submit the public key to your homeserver.\nThis is a once off.\nSorry for the inconvenience. - Re-request encryption keys from your other sessions. + Re-request encryption keys from your other sessions. Key request sent. @@ -1461,7 +1461,7 @@ Why choose Riot.im? Never lose encrypted messages Use Key Backup - New encrypted messages keys + New secure message keys Manage in Key Backup Backing up keys… @@ -2126,7 +2126,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming Other users may not trust it Complete Security - Open an existing session & use it to verify this one, granting it access to encrypted messages. + Use an existing session to verify this one, granting it access to encrypted messages. Verify @@ -2148,7 +2148,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming QR code - Did the other user successfully scan the QR code? + Almost there! Is %s showing the same shield? Yes No @@ -2166,8 +2166,8 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming Selected Option Creates a simple poll - Can‘t access an existing session? - Use your recovery key or passphrase + Use a recovery method + If you can’t access an existing session New Sign In @@ -2194,4 +2194,108 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming Keys are already up to date! + RiotX Android + + Key Requests + + Unlock encrypted messages history + + Refresh + + New Session + Tap to review & verify + Use this session to verify your new one, granting it access to encrypted messages. + This wasn’t me + Your account may be compromised + + If you cancel, you won’t be able to read encrypted messages on this device, and other users won’t trust it + If you cancel, you won’t be able to read encrypted messages on your new device, and other users won’t trust it + You won’t verify %1$s (%2$s) if you cancel now. Start again in their user profile. + + + One of the following may be compromised:\n\n- Your password\n- Your homeserver\n- This device, or the other device\n- The internet connection either device is using\n\nWe recommend you change your password & recovery key in Settings immediately. + + + Verify your devices from Settings. + Verification Cancelled + + Message Password + Message Key + Account Password + + + Set a %s + Generate a Message Key + + + Confirm %s + + + Enter your %s to continue. + + + Secure & unlock encrypted messages and trust with a %s. + + Enter your %s again to confirm it. + Don’t re-use your account password. + + + This might take several seconds, please be patient. + Setting up recovery. + Your recovery key + You‘re done! + Keep it safe + Finish + + + Use this %1$s as a safety net in case you forget your %2$s. + + Publishing created identity keys + Generating secure key from passphrase + Defining SSSS default Key + Synchronizing Master key + Synchronizing User key + Synchronizing Self Signing key + Setting Up Key Backup + + + + Your %2$s & %1$s are now set.\n\nKeep them safe! You’ll need them to unlock encrypted messages and secure information if you lose all of your active sessions. + + + Print it and store it somewhere safe + Save it on a USB key or backup drive + Copy it to your personal cloud storage + + You cannot do that from mobile + + Setting a Message Password lets you secure & unlock encrypted messages and trust.\n\nIf you don’t want to set a Message Password, generate a Message Key instead. + Setting a Message Password lets you secure & unlock encrypted messages and trust. + + + Encryption enabled + Messages in this room are end-to-end encrypted. Learn more & verify users in their profile. + Encryption not enabled + The encryption used by this room is not supported + + %s created and configured the room. + + Almost there! Is the other device showing the same shield? + Almost there! Waiting for confirmation… + Waiting for %s… + + Failed to import keys + + Notifications configuration + Messages containing @room + Encrypted messages in one-to-one chats + Encrypted messages in group chats + When rooms are upgraded + Troubleshoot + Set notification importance by event + + Sends a message as plain text, without interpreting it as markdown + + Incorrect username and/or password. The entered password starts or ends with spaces, please check it. + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index cb2a29de28..025491f94b 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -6,19 +6,12 @@ - + Message… - Notifications configuration - Messages containing @room - Encrypted messages in one-to-one chats - Encrypted messages in group chats - When rooms are upgraded - Troubleshoot - Set notification importance by event - Refresh + @@ -33,8 +26,7 @@ - Sends a message as plain text, without interpreting it as markdown + - Incorrect username and/or password. The entered password starts or ends with spaces, please check it. diff --git a/vector/src/main/res/values/theme_dark.xml b/vector/src/main/res/values/theme_dark.xml index 9bfdcc8231..09775d4d41 100644 --- a/vector/src/main/res/values/theme_dark.xml +++ b/vector/src/main/res/values/theme_dark.xml @@ -36,8 +36,6 @@ @drawable/highlighted_message_background_dark - @color/riotx_header_panel_border_mobile_dark - @color/riotx_accent @color/primary_color_dark_light diff --git a/vector/src/main/res/values/theme_light.xml b/vector/src/main/res/values/theme_light.xml index 3a479c9368..c63dfa9057 100644 --- a/vector/src/main/res/values/theme_light.xml +++ b/vector/src/main/res/values/theme_light.xml @@ -35,8 +35,6 @@ @drawable/highlighted_message_background_light - @color/riotx_header_panel_border_mobile_light - @color/riotx_accent diff --git a/vector/src/main/res/xml/vector_settings_advanced_settings.xml b/vector/src/main/res/xml/vector_settings_advanced_settings.xml index 34cd8743be..e92aae3ff9 100644 --- a/vector/src/main/res/xml/vector_settings_advanced_settings.xml +++ b/vector/src/main/res/xml/vector_settings_advanced_settings.xml @@ -65,13 +65,20 @@ app:fragment="im.vector.riotx.features.settings.push.PushRulesFragment" /> - + + + \ No newline at end of file