diff --git a/CHANGES.md b/CHANGES.md index 87645fad10..21b846c137 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -29,6 +29,9 @@ Improvements 🙌: - Cross-Signing | Hide Use recovery key when 4S is not setup (#1007) - Cross-Signing | Trust account xSigning keys by entering Recovery Key (select file or copy) #1199 - Invite member(s) to an existing room #1276 + - E2E timeline decoration (#1279) + - Manage Session Settings / Cross Signing update (#1295) + - Cross-Signing | Review sessions toast update old vs new (#1293, #1306) Bugfix 🐛: - Fix summary notification staying after "mark as read" @@ -44,6 +47,9 @@ Bugfix 🐛: - RiotX now uses as many threads as it needs to do work and send messages (#1221) - Fix issue with media path (#1227) - Add user to direct chat by user id (#1065) + - Use correct URL for SSO connection (#1178) + - Emoji completion :tada: does not completes to 🎉 like on web (#1285) + - Fix bad Shield Logic for DM (#963) Translations 🗣: - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 176a6ee9c1..b36843adee 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,6 +13,24 @@ Dedicated room for RiotX: [![RiotX Android Matrix room #riot-android:matrix.org] Please set the "hard wrap" setting of Android Studio to 160 chars, this is the setting we use internally to format the source code (Menu `Settings/Editor/Code Style` then `Hard wrap at`). Please ensure that your using the project formatting rules (which are in the project at .idea/codeStyles/), and format the file before committing them. +### Template + +An Android Studio template has been added to the project to help creating all files needed when adding a new screen to the application. Fragment, ViewModel, Activity, etc. + +To install the template (to be done only once): +- Go to folder `./tools/template`. +- Run the script `./configure.sh`. +- Restart Android Studio. + +To create a new screen: +- First create a new package in your code. +- Then right click on the package, and select `New/New Vector/RiotX Feature`. +- Follow the Wizard, especially replace `Main` by something more relevant to your feature. +- Click on `Finish`. +- Remainning steps are described as TODO in the generated files, or will be pointed out by the compilator, or at runtime :) + +Note that if the templates are modified, the only things to do is to restart Android Studio for the change to take effect. + ## Compilation For now, the Matrix SDK and the RiotX application are in the same project. So there is no specific thing to do, this project should compile without any special action. diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt index dc95a3e40d..e92da1e424 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt @@ -31,6 +31,8 @@ import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.toOptional import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo +import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo +import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent import io.reactivex.Observable import io.reactivex.Single @@ -58,6 +60,13 @@ class RxSession(private val session: Session) { } } + fun liveMyDeviceInfo(): Observable> { + return session.cryptoService().getLiveMyDevicesInfo().asObservable() + .startWithCallable { + session.cryptoService().getMyDevicesInfo() + } + } + fun liveSyncState(): Observable { return session.getSyncStateLive().asObservable() } @@ -123,6 +132,13 @@ class RxSession(private val session: Session) { } } + fun liveCrossSigningPrivateKeys(): Observable> { + return session.cryptoService().crossSigningService().getLiveCrossSigningPrivateKeys().asObservable() + .startWithCallable { + session.cryptoService().crossSigningService().getCrossSigningPrivateKeys().toOptional() + } + } + fun liveAccountData(types: Set): Observable> { return session.getLiveAccountDataEvents(types).asObservable() .startWithCallable { diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/CryptoStoreHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/CryptoStoreHelper.kt index 6b0ebbf6a4..7b4020bf04 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/CryptoStoreHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/CryptoStoreHelper.kt @@ -20,6 +20,8 @@ import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStore import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreModule +import im.vector.matrix.android.internal.crypto.store.db.mapper.CrossSigningKeysMapper +import im.vector.matrix.android.internal.di.MoshiProvider import io.realm.RealmConfiguration import kotlin.random.Random @@ -31,6 +33,7 @@ internal class CryptoStoreHelper { .name("test.realm") .modules(RealmCryptoStoreModule()) .build(), + crossSigningKeysMapper = CrossSigningKeysMapper(MoshiProvider.providesMoshi()), credentials = createCredential()) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Constants.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Constants.kt new file mode 100644 index 0000000000..af6e2277f4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Constants.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.api.auth + +/** + * Path to use when the client does not supported any or all login flows + * Ref: https://matrix.org/docs/spec/client_server/latest#login-fallback + * */ +const val LOGIN_FALLBACK_PATH = "/_matrix/static/client/login/" + +/** + * Path to use when the client does not supported any or all registration flows + * Not documented + */ +const val REGISTER_FALLBACK_PATH = "/_matrix/static/client/register/" + +/** + * Path to use when the client want to connect using SSO + * Ref: https://matrix.org/docs/spec/client_server/latest#sso-client-login + */ +const val SSO_FALLBACK_PATH = "/_matrix/client/r0/login/sso/redirect" + +const val SSO_REDIRECT_URL_PARAM = "redirectUrl" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt index 3a068af076..7b5dffb21e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt @@ -16,6 +16,10 @@ package im.vector.matrix.android.api.failure +import im.vector.matrix.android.api.extensions.tryThis +import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse +import im.vector.matrix.android.internal.di.MoshiProvider +import java.io.IOException import javax.net.ssl.HttpsURLConnection fun Throwable.is401() = @@ -29,6 +33,7 @@ fun Throwable.isTokenError() = fun Throwable.shouldBeRetried(): Boolean { return this is Failure.NetworkConnection + || this is IOException || (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED) } @@ -37,3 +42,18 @@ fun Throwable.isInvalidPassword(): Boolean { && error.code == MatrixError.M_FORBIDDEN && error.message == "Invalid password" } + +/** + * Try to convert to a RegistrationFlowResponse. Return null in the cases it's not possible + */ +fun Throwable.toRegistrationFlowResponse(): RegistrationFlowResponse? { + return if (this is Failure.OtherServerError && this.httpCode == 401) { + tryThis { + MoshiProvider.providesMoshi() + .adapter(RegistrationFlowResponse::class.java) + .fromJson(this.errorBody) + } + } else { + null + } +} 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 e6fbaaf9a6..2c96465313 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 @@ -98,7 +98,9 @@ interface CryptoService { fun removeRoomKeysRequestListener(listener: GossipingRequestListener) - fun getDevicesList(callback: MatrixCallback) + fun fetchDevicesList(callback: MatrixCallback) + fun getMyDevicesInfo() : List + fun getLiveMyDevicesInfo() : LiveData> fun getDeviceInfo(deviceId: String, callback: MatrixCallback) 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 4085e1233d..f1c998cee5 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 @@ -55,6 +55,8 @@ interface CrossSigningService { fun getCrossSigningPrivateKeys(): PrivateKeysInfo? + fun getLiveCrossSigningPrivateKeys(): LiveData> + fun canCrossSign(): Boolean fun trustUser(otherUserId: String, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt index 6171b2633b..1ad6112f2c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt @@ -46,10 +46,10 @@ data class RoomSummary constructor( val readMarkerId: String? = null, val userDrafts: List = emptyList(), val isEncrypted: Boolean, + val encryptionEventTs: Long?, val inviterId: String? = null, val typingRoomMemberIds: List = emptyList(), val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS, - // TODO Plug it val roomEncryptionTrustLevel: RoomEncryptionTrustLevel? = null ) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegisterTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegisterTask.kt index 69976b063f..473b960d94 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegisterTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegisterTask.kt @@ -18,8 +18,8 @@ package im.vector.matrix.android.internal.auth.registration import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.failure.toRegistrationFlowResponse import im.vector.matrix.android.internal.auth.AuthAPI -import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task @@ -39,25 +39,9 @@ internal class DefaultRegisterTask( apiCall = authAPI.register(params.registrationParams) } } catch (throwable: Throwable) { - if (throwable is Failure.OtherServerError && throwable.httpCode == 401) { - // Parse to get a RegistrationFlowResponse - val registrationFlowResponse = try { - MoshiProvider.providesMoshi() - .adapter(RegistrationFlowResponse::class.java) - .fromJson(throwable.errorBody) - } catch (e: Exception) { - null - } - // check if the server response can be cast - if (registrationFlowResponse != null) { - throw Failure.RegistrationFlowError(registrationFlowResponse) - } else { - throw throwable - } - } else { - // Other error - throw throwable - } + throw throwable.toRegistrationFlowResponse() + ?.let { Failure.RegistrationFlowError(it) } + ?: throwable } } } 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 50ffb3082a..1efdffdb06 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 @@ -112,6 +112,7 @@ internal abstract class CryptoModule { @SessionScope fun providesRealmConfiguration(@SessionFilesDirectory directory: File, @UserMd5 userMd5: String, + realmCryptoStoreMigration: RealmCryptoStoreMigration, realmKeysUtils: RealmKeysUtils): RealmConfiguration { return RealmConfiguration.Builder() .directory(directory) @@ -121,7 +122,7 @@ internal abstract class CryptoModule { .name("crypto_store.realm") .modules(RealmCryptoStoreModule()) .schemaVersion(RealmCryptoStoreMigration.CRYPTO_STORE_SCHEMA_VERSION) - .migration(RealmCryptoStoreMigration) + .migration(realmCryptoStoreMigration) .build() } 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 a789814958..1d3c0f4dcd 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 @@ -251,15 +251,33 @@ internal class DefaultCryptoService @Inject constructor( return myDeviceInfoHolder.get().myDevice } - override fun getDevicesList(callback: MatrixCallback) { + override fun fetchDevicesList(callback: MatrixCallback) { getDevicesTask .configureWith { // this.executionThread = TaskThread.CRYPTO - this.callback = callback + this.callback = object : MatrixCallback { + override fun onFailure(failure: Throwable) { + callback.onFailure(failure) + } + + override fun onSuccess(data: DevicesListResponse) { + // Save in local DB + cryptoStore.saveMyDevicesInfo(data.devices ?: emptyList()) + callback.onSuccess(data) + } + } } .executeBy(taskExecutor) } + override fun getLiveMyDevicesInfo(): LiveData> { + return cryptoStore.getLiveMyDevicesInfo() + } + + override fun getMyDevicesInfo(): List { + return cryptoStore.getMyDevicesInfo() + } + override fun getDeviceInfo(deviceId: String, callback: MatrixCallback) { getDeviceInfoTask .configureWith(GetDeviceInfoTask.Params(deviceId)) { @@ -318,6 +336,8 @@ internal class DefaultCryptoService @Inject constructor( cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { internalStart(isInitialSync) } + // Just update + fetchDevicesList(NoOpMatrixCallback()) } private suspend fun internalStart(isInitialSync: Boolean) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/ComputeTrustTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/ComputeTrustTask.kt index 841de92130..121479ad66 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/ComputeTrustTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/ComputeTrustTask.kt @@ -19,6 +19,7 @@ import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel import im.vector.matrix.android.api.extensions.orFalse import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore +import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import kotlinx.coroutines.withContext @@ -26,17 +27,28 @@ import javax.inject.Inject internal interface ComputeTrustTask : Task { data class Params( - val userIds: List + val activeMemberUserIds: List, + val isDirectRoom: Boolean ) } internal class DefaultComputeTrustTask @Inject constructor( private val cryptoStore: IMXCryptoStore, + @UserId private val userId: String, private val coroutineDispatchers: MatrixCoroutineDispatchers ) : ComputeTrustTask { override suspend fun execute(params: ComputeTrustTask.Params): RoomEncryptionTrustLevel = withContext(coroutineDispatchers.crypto) { - val allTrustedUserIds = params.userIds + // 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 (params.isDirectRoom) { + params.activeMemberUserIds.filter { it != userId } + } else { + params.activeMemberUserIds + } + + val allTrustedUserIds = listToCheck .filter { userId -> getUserCrossSigningKeys(userId)?.isTrusted() == true } if (allTrustedUserIds.isEmpty()) { @@ -60,7 +72,7 @@ internal class DefaultComputeTrustTask @Inject constructor( if (hasWarning) { RoomEncryptionTrustLevel.Warning } else { - if (params.userIds.size == allTrustedUserIds.size) { + if (listToCheck.size == allTrustedUserIds.size) { // all users are trusted and all devices are verified RoomEncryptionTrustLevel.Trusted } else { 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 2166e4be3a..2fee8130fb 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 @@ -470,6 +470,10 @@ internal class DefaultCrossSigningService @Inject constructor( return cryptoStore.getCrossSigningPrivateKeys() } + override fun getLiveCrossSigningPrivateKeys(): LiveData> { + return cryptoStore.getLiveCrossSigningPrivateKeys() + } + override fun canCrossSign(): Boolean { return checkSelfTrust().isVerified() && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null && cryptoStore.getCrossSigningPrivateKeys()?.user != null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/SessionToCryptoRoomMembersUpdate.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/SessionToCryptoRoomMembersUpdate.kt index 04f63f945a..2f1cb77a21 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/SessionToCryptoRoomMembersUpdate.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/SessionToCryptoRoomMembersUpdate.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.crypto.crosssigning data class SessionToCryptoRoomMembersUpdate( val roomId: String, + val isDirect: Boolean, val userIds: List ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/ShieldTrustUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/ShieldTrustUpdater.kt index c4c49a5940..f1e387beb3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/ShieldTrustUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/ShieldTrustUpdater.kt @@ -15,18 +15,20 @@ */ package im.vector.matrix.android.internal.crypto.crosssigning +import im.vector.matrix.android.api.extensions.orFalse import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntity import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields +import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater +import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper import im.vector.matrix.android.internal.task.TaskExecutor -import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.createBackgroundHandler import io.realm.Realm import io.realm.RealmConfiguration +import kotlinx.coroutines.android.asCoroutineDispatcher import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import timber.log.Timber @@ -38,13 +40,13 @@ internal class ShieldTrustUpdater @Inject constructor( private val eventBus: EventBus, private val computeTrustTask: ComputeTrustTask, private val taskExecutor: TaskExecutor, - private val coroutineDispatchers: MatrixCoroutineDispatchers, @SessionDatabase private val sessionRealmConfiguration: RealmConfiguration, private val roomSummaryUpdater: RoomSummaryUpdater ) { companion object { private val BACKGROUND_HANDLER = createBackgroundHandler("SHIELD_CRYPTO_DB_THREAD") + private val BACKGROUND_HANDLER_DISPATCHER = BACKGROUND_HANDLER.asCoroutineDispatcher() } private val backgroundSessionRealm = AtomicReference() @@ -76,14 +78,11 @@ internal class ShieldTrustUpdater @Inject constructor( if (!isStarted.get()) { return } - taskExecutor.executorScope.launch(coroutineDispatchers.crypto) { - val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(update.userIds)) + taskExecutor.executorScope.launch(BACKGROUND_HANDLER_DISPATCHER) { + val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(update.userIds, update.isDirect)) // We need to send that back to session base - - BACKGROUND_HANDLER.post { - backgroundSessionRealm.get()?.executeTransaction { realm -> - roomSummaryUpdater.updateShieldTrust(realm, update.roomId, updatedTrust) - } + backgroundSessionRealm.get()?.executeTransaction { realm -> + roomSummaryUpdater.updateShieldTrust(realm, update.roomId, updatedTrust) } } } @@ -93,45 +92,31 @@ internal class ShieldTrustUpdater @Inject constructor( if (!isStarted.get()) { return } - onCryptoDevicesChange(update.userIds) } private fun onCryptoDevicesChange(users: List) { - BACKGROUND_HANDLER.post { - val impactedRoomsId = backgroundSessionRealm.get()?.where(RoomMemberSummaryEntity::class.java) - ?.`in`(RoomMemberSummaryEntityFields.USER_ID, users.toTypedArray()) - ?.findAll() - ?.map { it.roomId } - ?.distinct() + taskExecutor.executorScope.launch(BACKGROUND_HANDLER_DISPATCHER) { + val realm = backgroundSessionRealm.get() ?: return@launch + val distinctRoomIds = realm.where(RoomMemberSummaryEntity::class.java) + .`in`(RoomMemberSummaryEntityFields.USER_ID, users.toTypedArray()) + .distinct(RoomMemberSummaryEntityFields.ROOM_ID) + .findAll() + .map { it.roomId } - val map = HashMap>() - impactedRoomsId?.forEach { roomId -> - backgroundSessionRealm.get()?.let { realm -> - RoomMemberSummaryEntity.where(realm, roomId) - .findAll() - .let { results -> - map[roomId] = results.map { it.userId } - } - } - } - - map.forEach { entry -> - val roomId = entry.key - val userList = entry.value - taskExecutor.executorScope.launch { - withContext(coroutineDispatchers.crypto) { - try { - // Can throw if the crypto database has been closed in between, in this case log and ignore? - val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(userList)) - BACKGROUND_HANDLER.post { - backgroundSessionRealm.get()?.executeTransaction { realm -> - roomSummaryUpdater.updateShieldTrust(realm, roomId, updatedTrust) - } - } - } catch (failure: Throwable) { - Timber.e(failure) + distinctRoomIds.forEach { roomId -> + val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() + if (roomSummary?.isEncrypted.orFalse()) { + val allActiveRoomMembers = RoomMemberHelper(realm, roomId).getActiveRoomMemberIds() + try { + val updatedTrust = computeTrustTask.execute( + ComputeTrustTask.Params(allActiveRoomMembers, roomSummary?.isDirect == true) + ) + realm.executeTransaction { + roomSummaryUpdater.updateShieldTrust(it, roomId, updatedTrust) } + } catch (failure: Throwable) { + Timber.e(failure) } } } 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 b124f7590e..fc6e2cc436 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 @@ -29,7 +29,8 @@ data class CryptoDeviceInfo( override val signatures: Map>? = null, val unsigned: JsonDict? = null, var trustLevel: DeviceTrustLevel? = null, - var isBlocked: Boolean = false + var isBlocked: Boolean = false, + val firstTimeSeenLocalTs: Long? = null ) : CryptoInfo { val isVerified: Boolean diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoInfoMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoInfoMapper.kt index 4459d508ff..f3ddfb8faa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoInfoMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoInfoMapper.kt @@ -61,20 +61,4 @@ internal object CryptoInfoMapper { signatures = keyInfo.signatures ) } - - fun RestDeviceInfo.toCryptoModel(): CryptoDeviceInfo { - return map(this) - } - - fun CryptoDeviceInfo.toRest(): RestDeviceInfo { - return map(this) - } - -// fun RestKeyInfo.toCryptoModel(): CryptoCrossSigningKey { -// return map(this) -// } - - fun CryptoCrossSigningKey.toRest(): RestKeyInfo { - return map(this) - } } 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 0d1026b69f..18c85f78fb 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 @@ -32,6 +32,7 @@ import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper +import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity import org.matrix.olm.OlmAccount @@ -218,6 +219,9 @@ internal interface IMXCryptoStore { // TODO temp fun getLiveDeviceList(): LiveData> + fun getMyDevicesInfo() : List + fun getLiveMyDevicesInfo() : LiveData> + fun saveMyDevicesInfo(info: List) /** * Store the crypto algorithm for a room. * @@ -405,6 +409,7 @@ internal interface IMXCryptoStore { fun storeUSKPrivateKey(usk: String?) fun getCrossSigningPrivateKeys(): PrivateKeysInfo? + fun getLiveCrossSigningPrivateKeys(): LiveData> fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo? 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 642c466e42..ab4f4df354 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 @@ -62,6 +62,7 @@ fun doRealmTransaction(realmConfiguration: RealmConfiguration, action: (Realm) - realm.executeTransaction { action.invoke(it) } } } + fun doRealmTransactionAsync(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) { Realm.getInstance(realmConfiguration).use { realm -> realm.executeTransactionAsync { action.invoke(it) } @@ -79,31 +80,26 @@ fun serializeForRealm(o: Any?): String? { val baos = ByteArrayOutputStream() val gzis = CompatUtil.createGzipOutputStream(baos) val out = ObjectOutputStream(gzis) - - out.writeObject(o) - out.close() - + out.use { + it.writeObject(o) + } return Base64.encodeToString(baos.toByteArray(), Base64.DEFAULT) } /** * Do the opposite of serializeForRealm. */ +@Suppress("UNCHECKED_CAST") fun deserializeFromRealm(string: String?): T? { if (string == null) { return null } - val decodedB64 = Base64.decode(string.toByteArray(), Base64.DEFAULT) val bais = ByteArrayInputStream(decodedB64) val gzis = GZIPInputStream(bais) val ois = ObjectInputStream(gzis) - - @Suppress("UNCHECKED_CAST") - val result = ois.readObject() as T - - ois.close() - - return result + return ois.use { + it.readObject() as T + } } 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 a6f3f5d593..c57dff046b 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 @@ -36,16 +36,17 @@ 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 import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper +import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody import im.vector.matrix.android.internal.crypto.model.toEntity 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.mapper.CrossSigningKeysMapper 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 @@ -59,6 +60,7 @@ import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossiping 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.MyDeviceLastSeenInfoEntity 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 @@ -91,6 +93,7 @@ import kotlin.collections.set @SessionScope internal class RealmCryptoStore @Inject constructor( @CryptoDatabase private val realmConfiguration: RealmConfiguration, + private val crossSigningKeysMapper: CrossSigningKeysMapper, private val credentials: Credentials) : IMXCryptoStore { /* ========================================================================================== @@ -200,9 +203,9 @@ internal class RealmCryptoStore @Inject constructor( } override fun getDeviceId(): String { - return doRealmQueryAndCopy(realmConfiguration) { - it.where().findFirst() - }?.deviceId ?: "" + return doWithRealm(realmConfiguration) { + it.where().findFirst()?.deviceId + } ?: "" } override fun saveOlmAccount() { @@ -256,24 +259,25 @@ internal class RealmCryptoStore @Inject constructor( } override fun getUserDevice(userId: String, deviceId: String): CryptoDeviceInfo? { - return doRealmQueryAndCopy(realmConfiguration) { + return doWithRealm(realmConfiguration) { it.where() .equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId)) .findFirst() - }?.let { - CryptoMapper.mapToModel(it) + ?.let { deviceInfo -> + CryptoMapper.mapToModel(deviceInfo) + } } } override fun deviceWithIdentityKey(identityKey: String): CryptoDeviceInfo? { - return doRealmQueryAndCopy(realmConfiguration) { + return doWithRealm(realmConfiguration) { it.where() .equalTo(DeviceInfoEntityFields.IDENTITY_KEY, identityKey) .findFirst() + ?.let { deviceInfo -> + CryptoMapper.mapToModel(deviceInfo) + } } - ?.let { - CryptoMapper.mapToModel(it) - } } override fun storeUserDevices(userId: String, devices: Map?) { @@ -285,10 +289,16 @@ internal class RealmCryptoStore @Inject constructor( UserEntity.getOrCreate(realm, userId) .let { u -> // Add the devices + val currentKnownDevices = u.devices.toList() + val new = devices.map { entry -> entry.value.toEntity() } + new.forEach { entity -> + // Maintain first time seen + val existing = currentKnownDevices.firstOrNull { it.deviceId == entity.deviceId && it.identityKey == entity.identityKey } + entity.firstTimeSeenLocalTs = existing?.firstTimeSeenLocalTs ?: System.currentTimeMillis() + realm.insertOrUpdate(entity) + } // Ensure all other devices are deleted u.devices.deleteAllFromRealm() - val new = devices.map { entry -> entry.value.toEntity() } - new.forEach { realm.insertOrUpdate(it) } u.devices.addAll(new) } } @@ -309,36 +319,19 @@ internal class RealmCryptoStore @Inject constructor( } else { CrossSigningInfoEntity.getOrCreate(realm, userId).let { signingInfo -> // What should we do if we detect a change of the keys? - val existingMaster = signingInfo.getMasterKey() if (existingMaster != null && existingMaster.publicKeyBase64 == masterKey.unpaddedBase64PublicKey) { - // update signatures? - existingMaster.putSignatures(masterKey.signatures) - existingMaster.usages = masterKey.usages?.toTypedArray()?.let { RealmList(*it) } - ?: RealmList() + crossSigningKeysMapper.update(existingMaster, masterKey) } else { - val keyEntity = realm.createObject(KeyInfoEntity::class.java).apply { - this.publicKeyBase64 = masterKey.unpaddedBase64PublicKey - this.usages = masterKey.usages?.toTypedArray()?.let { RealmList(*it) } - ?: RealmList() - this.putSignatures(masterKey.signatures) - } + val keyEntity = crossSigningKeysMapper.map(masterKey) signingInfo.setMasterKey(keyEntity) } val existingSelfSigned = signingInfo.getSelfSignedKey() if (existingSelfSigned != null && existingSelfSigned.publicKeyBase64 == selfSigningKey.unpaddedBase64PublicKey) { - // update signatures? - existingSelfSigned.putSignatures(selfSigningKey.signatures) - existingSelfSigned.usages = selfSigningKey.usages?.toTypedArray()?.let { RealmList(*it) } - ?: RealmList() + crossSigningKeysMapper.update(existingSelfSigned, selfSigningKey) } else { - val keyEntity = realm.createObject(KeyInfoEntity::class.java).apply { - this.publicKeyBase64 = selfSigningKey.unpaddedBase64PublicKey - this.usages = selfSigningKey.usages?.toTypedArray()?.let { RealmList(*it) } - ?: RealmList() - this.putSignatures(selfSigningKey.signatures) - } + val keyEntity = crossSigningKeysMapper.map(selfSigningKey) signingInfo.setSelfSignedKey(keyEntity) } @@ -346,21 +339,12 @@ internal class RealmCryptoStore @Inject constructor( if (userSigningKey != null) { val existingUSK = signingInfo.getUserSigningKey() if (existingUSK != null && existingUSK.publicKeyBase64 == userSigningKey.unpaddedBase64PublicKey) { - // update signatures? - existingUSK.putSignatures(userSigningKey.signatures) - existingUSK.usages = userSigningKey.usages?.toTypedArray()?.let { RealmList(*it) } - ?: RealmList() + crossSigningKeysMapper.update(existingUSK, userSigningKey) } else { - val keyEntity = realm.createObject(KeyInfoEntity::class.java).apply { - this.publicKeyBase64 = userSigningKey.unpaddedBase64PublicKey - this.usages = userSigningKey.usages?.toTypedArray()?.let { RealmList(*it) } - ?: RealmList() - this.putSignatures(userSigningKey.signatures) - } + val keyEntity = crossSigningKeysMapper.map(userSigningKey) signingInfo.setUserSignedKey(keyEntity) } } - userEntity.crossSigningInfoEntity = signingInfo } } @@ -369,14 +353,35 @@ internal class RealmCryptoStore @Inject constructor( } override fun getCrossSigningPrivateKeys(): PrivateKeysInfo? { - return doRealmQueryAndCopy(realmConfiguration) { realm -> - realm.where().findFirst() - }?.let { - PrivateKeysInfo( - master = it.xSignMasterPrivateKey, - selfSigned = it.xSignSelfSignedPrivateKey, - user = it.xSignUserPrivateKey - ) + return doWithRealm(realmConfiguration) { realm -> + realm.where() + .findFirst() + ?.let { + PrivateKeysInfo( + master = it.xSignMasterPrivateKey, + selfSigned = it.xSignSelfSignedPrivateKey, + user = it.xSignUserPrivateKey + ) + } + } + } + + override fun getLiveCrossSigningPrivateKeys(): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm + .where() + }, + { + PrivateKeysInfo( + master = it.xSignMasterPrivateKey, + selfSigned = it.xSignSelfSignedPrivateKey, + user = it.xSignUserPrivateKey + ) + } + ) + return Transformations.map(liveData) { + it.firstOrNull().toOptional() } } @@ -400,16 +405,18 @@ internal class RealmCryptoStore @Inject constructor( } 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 - } + return doWithRealm(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 + } + } } } @@ -430,24 +437,30 @@ internal class RealmCryptoStore @Inject constructor( } override fun getUserDevices(userId: String): Map? { - return doRealmQueryAndCopy(realmConfiguration) { + return doWithRealm(realmConfiguration) { it.where() .equalTo(UserEntityFields.USER_ID, userId) .findFirst() + ?.devices + ?.map { deviceInfo -> + CryptoMapper.mapToModel(deviceInfo) + } + ?.associateBy { cryptoDevice -> + cryptoDevice.deviceId + } } - ?.devices - ?.map { CryptoMapper.mapToModel(it) } - ?.associateBy { it.deviceId } } override fun getUserDeviceList(userId: String): List? { - return doRealmQueryAndCopy(realmConfiguration) { + return doWithRealm(realmConfiguration) { it.where() .equalTo(UserEntityFields.USER_ID, userId) .findFirst() + ?.devices + ?.map { deviceInfo -> + CryptoMapper.mapToModel(deviceInfo) + } } - ?.devices - ?.map { CryptoMapper.mapToModel(it) } } override fun getLiveDeviceList(userId: String): LiveData> { @@ -496,6 +509,52 @@ internal class RealmCryptoStore @Inject constructor( } } + override fun getMyDevicesInfo(): List { + return monarchy.fetchAllCopiedSync { + it.where() + }.map { + DeviceInfo( + deviceId = it.deviceId, + lastSeenIp = it.lastSeenIp, + lastSeenTs = it.lastSeenTs, + displayName = it.displayName + ) + } + } + + override fun getLiveMyDevicesInfo(): LiveData> { + return monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm.where() + }, + { entity -> + DeviceInfo( + deviceId = entity.deviceId, + lastSeenIp = entity.lastSeenIp, + lastSeenTs = entity.lastSeenTs, + displayName = entity.displayName + ) + } + ) + } + + override fun saveMyDevicesInfo(info: List) { + val entities = info.map { + MyDeviceLastSeenInfoEntity( + lastSeenTs = it.lastSeenTs, + lastSeenIp = it.lastSeenIp, + displayName = it.displayName, + deviceId = it.deviceId + ) + } + monarchy.writeAsync { realm -> + realm.where().findAll().deleteAllFromRealm() + entities.forEach { + realm.insertOrUpdate(it) + } + } + } + override fun storeRoomAlgorithm(roomId: String, algorithm: String) { doRealmTransaction(realmConfiguration) { CryptoRoomEntity.getOrCreate(it, roomId).algorithm = algorithm @@ -503,17 +562,16 @@ internal class RealmCryptoStore @Inject constructor( } override fun getRoomAlgorithm(roomId: String): String? { - return doRealmQueryAndCopy(realmConfiguration) { - CryptoRoomEntity.getById(it, roomId) + return doWithRealm(realmConfiguration) { + CryptoRoomEntity.getById(it, roomId)?.algorithm } - ?.algorithm } override fun shouldEncryptForInvitedMembers(roomId: String): Boolean { - return doRealmQueryAndCopy(realmConfiguration) { - CryptoRoomEntity.getById(it, roomId) + return doWithRealm(realmConfiguration) { + CryptoRoomEntity.getById(it, roomId)?.shouldEncryptForInvitedMembers } - ?.shouldEncryptForInvitedMembers ?: false + ?: false } override fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) { @@ -577,24 +635,24 @@ internal class RealmCryptoStore @Inject constructor( } override fun getLastUsedSessionId(deviceKey: String): String? { - return doRealmQueryAndCopy(realmConfiguration) { + return doWithRealm(realmConfiguration) { it.where() .equalTo(OlmSessionEntityFields.DEVICE_KEY, deviceKey) .sort(OlmSessionEntityFields.LAST_RECEIVED_MESSAGE_TS, Sort.DESCENDING) .findFirst() + ?.sessionId } - ?.sessionId } override fun getDeviceSessionIds(deviceKey: String): MutableSet { - return doRealmQueryAndCopyList(realmConfiguration) { + return doWithRealm(realmConfiguration) { it.where() .equalTo(OlmSessionEntityFields.DEVICE_KEY, deviceKey) .findAll() + .mapNotNull { sessionEntity -> + sessionEntity.sessionId + } } - .mapNotNull { - it.sessionId - } .toMutableSet() } @@ -641,12 +699,12 @@ internal class RealmCryptoStore @Inject constructor( // If not in cache (or not found), try to read it from realm if (inboundGroupSessionToRelease[key] == null) { - doRealmQueryAndCopy(realmConfiguration) { + doWithRealm(realmConfiguration) { it.where() .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) .findFirst() + ?.getInboundGroupSession() } - ?.getInboundGroupSession() ?.let { inboundGroupSessionToRelease[key] = it } @@ -660,13 +718,13 @@ internal class RealmCryptoStore @Inject constructor( * so there is no need to use or update `inboundGroupSessionToRelease` for native memory management */ override fun getInboundGroupSessions(): MutableList { - return doRealmQueryAndCopyList(realmConfiguration) { + return doWithRealm(realmConfiguration) { it.where() .findAll() + .mapNotNull { inboundGroupSessionEntity -> + inboundGroupSessionEntity.getInboundGroupSession() + } } - .mapNotNull { - it.getInboundGroupSession() - } .toMutableList() } @@ -755,13 +813,14 @@ internal class RealmCryptoStore @Inject constructor( } override fun inboundGroupSessionsToBackup(limit: Int): List { - return doRealmQueryAndCopyList(realmConfiguration) { + return doWithRealm(realmConfiguration) { it.where() .equalTo(OlmInboundGroupSessionEntityFields.BACKED_UP, false) .limit(limit.toLong()) .findAll() - }.mapNotNull { inboundGroupSession -> - inboundGroupSession.getInboundGroupSession() + .mapNotNull { inboundGroupSession -> + inboundGroupSession.getInboundGroupSession() + } } } @@ -785,10 +844,9 @@ internal class RealmCryptoStore @Inject constructor( } override fun getGlobalBlacklistUnverifiedDevices(): Boolean { - return doRealmQueryAndCopy(realmConfiguration) { - it.where().findFirst() - }?.globalBlacklistUnverifiedDevices - ?: false + return doWithRealm(realmConfiguration) { + it.where().findFirst()?.globalBlacklistUnverifiedDevices + } ?: false } override fun setRoomsListBlacklistUnverifiedDevices(roomIds: List) { @@ -811,28 +869,28 @@ internal class RealmCryptoStore @Inject constructor( } override fun getRoomsListBlacklistUnverifiedDevices(): MutableList { - return doRealmQueryAndCopyList(realmConfiguration) { + return doWithRealm(realmConfiguration) { it.where() .equalTo(CryptoRoomEntityFields.BLACKLIST_UNVERIFIED_DEVICES, true) .findAll() + .mapNotNull { cryptoRoom -> + cryptoRoom.roomId + } } - .mapNotNull { - it.roomId - } .toMutableList() } override fun getDeviceTrackingStatuses(): MutableMap { - return doRealmQueryAndCopyList(realmConfiguration) { + return doWithRealm(realmConfiguration) { it.where() .findAll() + .associateBy { user -> + user.userId!! + } + .mapValues { entry -> + entry.value.deviceTrackingStatus + } } - .associateBy { - it.userId!! - } - .mapValues { - it.value.deviceTrackingStatus - } .toMutableMap() } @@ -847,12 +905,12 @@ internal class RealmCryptoStore @Inject constructor( } override fun getDeviceTrackingStatus(userId: String, defaultValue: Int): Int { - return doRealmQueryAndCopy(realmConfiguration) { + return doWithRealm(realmConfiguration) { it.where() .equalTo(UserEntityFields.USER_ID, userId) .findFirst() + ?.deviceTrackingStatus } - ?.deviceTrackingStatus ?: defaultValue } @@ -1089,63 +1147,65 @@ internal class RealmCryptoStore @Inject constructor( } override fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest? { - return doRealmQueryAndCopyList(realmConfiguration) { realm -> + return doWithRealm(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() + .mapNotNull { entity -> + entity.toIncomingGossipingRequest() as? IncomingRoomKeyRequest + } + .firstOrNull() + } } override fun getPendingIncomingRoomKeyRequests(): List { - return doRealmQueryAndCopyList(realmConfiguration) { + return doWithRealm(realmConfiguration) { it.where() .equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) .equalTo(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, GossipingRequestState.PENDING.name) .findAll() + .map { entity -> + IncomingRoomKeyRequest( + userId = entity.otherUserId, + deviceId = entity.otherDeviceId, + requestId = entity.requestId, + requestBody = entity.getRequestedKeyInfo(), + localCreationTimestamp = entity.localCreationTimestamp + ) + } } - .map { entity -> - IncomingRoomKeyRequest( - userId = entity.otherUserId, - deviceId = entity.otherDeviceId, - requestId = entity.requestId, - requestBody = entity.getRequestedKeyInfo(), - localCreationTimestamp = entity.localCreationTimestamp - ) - } } override fun getPendingIncomingGossipingRequests(): List { - return doRealmQueryAndCopyList(realmConfiguration) { + return doWithRealm(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 - ) + .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?) { @@ -1183,9 +1243,9 @@ internal class RealmCryptoStore @Inject constructor( * Cross Signing * ========================================================================================== */ override fun getMyCrossSigningInfo(): MXCrossSigningInfo? { - return doRealmQueryAndCopy(realmConfiguration) { - it.where().findFirst() - }?.userId?.let { + return doWithRealm(realmConfiguration) { + it.where().findFirst()?.userId + }?.let { getCrossSigningInfo(it) } } @@ -1304,33 +1364,24 @@ internal class RealmCryptoStore @Inject constructor( } override fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? { - return doRealmQueryAndCopy(realmConfiguration) { realm -> - realm.where(CrossSigningInfoEntity::class.java) + return doWithRealm(realmConfiguration) { realm -> + val crossSigningInfo = realm.where(CrossSigningInfoEntity::class.java) .equalTo(CrossSigningInfoEntityFields.USER_ID, userId) .findFirst() - }?.let { xsignInfo -> - mapCrossSigningInfoEntity(xsignInfo) + if (crossSigningInfo == null) { + null + } else { + mapCrossSigningInfoEntity(crossSigningInfo) + } } } private fun mapCrossSigningInfoEntity(xsignInfo: CrossSigningInfoEntity): MXCrossSigningInfo { + val userId = xsignInfo.userId ?: "" return MXCrossSigningInfo( - userId = xsignInfo.userId ?: "", + userId = userId, crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull { - val pubKey = it.publicKeyBase64 ?: return@mapNotNull null - CryptoCrossSigningKey( - userId = xsignInfo.userId ?: "", - keys = mapOf("ed25519:$pubKey" to pubKey), - usages = it.usages.map { it }, - signatures = it.getSignatures(), - trustLevel = it.trustLevelEntity?.let { - DeviceTrustLevel( - crossSigningVerified = it.crossSignedVerified ?: false, - locallyVerified = it.locallyVerified ?: false - ) - } - - ) + crossSigningKeysMapper.map(userId, it) } ) } @@ -1341,26 +1392,7 @@ internal class RealmCryptoStore @Inject constructor( realm.where() .equalTo(UserEntityFields.USER_ID, userId) }, - { entity -> - MXCrossSigningInfo( - userId = userId, - crossSigningKeys = entity.crossSigningKeys.mapNotNull { - val pubKey = it.publicKeyBase64 ?: return@mapNotNull null - CryptoCrossSigningKey( - userId = userId, - keys = mapOf("ed25519:$pubKey" to pubKey), - usages = it.usages.map { it }, - signatures = it.getSignatures(), - trustLevel = it.trustLevelEntity?.let { - DeviceTrustLevel( - crossSigningVerified = it.crossSignedVerified ?: false, - locallyVerified = it.locallyVerified ?: false - ) - } - ) - } - ) - } + { mapCrossSigningInfoEntity(it) } ) return Transformations.map(liveData) { it.firstOrNull().toOptional() @@ -1402,17 +1434,8 @@ internal class RealmCryptoStore @Inject constructor( // existing.crossSigningKeys.forEach { it.deleteFromRealm() } val xkeys = RealmList() info.crossSigningKeys.forEach { cryptoCrossSigningKey -> - xkeys.add( - realm.createObject(KeyInfoEntity::class.java).also { keyInfoEntity -> - keyInfoEntity.publicKeyBase64 = cryptoCrossSigningKey.unpaddedBase64PublicKey - keyInfoEntity.usages = cryptoCrossSigningKey.usages?.let { RealmList(*it.toTypedArray()) } - ?: RealmList() - keyInfoEntity.putSignatures(cryptoCrossSigningKey.signatures) - // TODO how to handle better, check if same keys? - // reset trust - keyInfoEntity.trustLevelEntity = null - } - ) + val keyEntity = crossSigningKeysMapper.map(cryptoCrossSigningKey) + xkeys.add(keyEntity) } existing.crossSigningKeys = xkeys } 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 d5972b5686..c1897c76d9 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 @@ -18,14 +18,17 @@ package im.vector.matrix.android.internal.crypto.store.db import com.squareup.moshi.Moshi import com.squareup.moshi.Types +import im.vector.matrix.android.api.extensions.tryThis import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.store.db.mapper.CrossSigningKeysMapper 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.MyDeviceLastSeenInfoEntityFields 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 @@ -33,11 +36,14 @@ import im.vector.matrix.android.internal.di.SerializeNulls import io.realm.DynamicRealm import io.realm.RealmMigration import timber.log.Timber +import javax.inject.Inject -internal object RealmCryptoStoreMigration : RealmMigration { +internal class RealmCryptoStoreMigration @Inject constructor(private val crossSigningKeysMapper: CrossSigningKeysMapper) : RealmMigration { // Version 1L added Cross Signing info persistence - const val CRYPTO_STORE_SCHEMA_VERSION = 3L + companion object { + const val CRYPTO_STORE_SCHEMA_VERSION = 5L + } override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { Timber.v("Migrating Realm Crypto from $oldVersion to $newVersion") @@ -45,6 +51,8 @@ internal object RealmCryptoStoreMigration : RealmMigration { if (oldVersion <= 0) migrateTo1(realm) if (oldVersion <= 1) migrateTo2(realm) if (oldVersion <= 2) migrateTo3(realm) + if (oldVersion <= 3) migrateTo4(realm) + if (oldVersion <= 4) migrateTo5(realm) } private fun migrateTo1(realm: DynamicRealm) { @@ -193,4 +201,38 @@ internal object RealmCryptoStoreMigration : RealmMigration { ?.addField(CryptoMetadataEntityFields.KEY_BACKUP_RECOVERY_KEY, String::class.java) ?.addField(CryptoMetadataEntityFields.KEY_BACKUP_RECOVERY_KEY_VERSION, String::class.java) } + + private fun migrateTo4(realm: DynamicRealm) { + Timber.d("Updating KeyInfoEntity table") + val keyInfoEntities = realm.where("KeyInfoEntity").findAll() + try { + keyInfoEntities.forEach { + val stringSignatures = it.getString(KeyInfoEntityFields.SIGNATURES) + val objectSignatures: Map>? = deserializeFromRealm(stringSignatures) + val jsonSignatures = crossSigningKeysMapper.serializeSignatures(objectSignatures) + it.setString(KeyInfoEntityFields.SIGNATURES, jsonSignatures) + } + } catch (failure: Throwable) { + } + } + + private fun migrateTo5(realm: DynamicRealm) { + realm.schema.create("MyDeviceLastSeenInfoEntity") + .addField(MyDeviceLastSeenInfoEntityFields.DEVICE_ID, String::class.java) + .addPrimaryKey(MyDeviceLastSeenInfoEntityFields.DEVICE_ID) + .addField(MyDeviceLastSeenInfoEntityFields.DISPLAY_NAME, String::class.java) + .addField(MyDeviceLastSeenInfoEntityFields.LAST_SEEN_IP, String::class.java) + .addField(MyDeviceLastSeenInfoEntityFields.LAST_SEEN_TS, Long::class.java) + .setNullable(MyDeviceLastSeenInfoEntityFields.LAST_SEEN_TS, true) + + val now = System.currentTimeMillis() + realm.schema.get("DeviceInfoEntity") + ?.addField(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, Long::class.java) + ?.setNullable(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, true) + ?.transform { deviceInfoEntity -> + tryThis { + deviceInfoEntity.setLong(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, now) + } + } + } } 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 3da91c6268..a8eb1db612 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 @@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.crypto.store.db.model.GossipingEventEnt 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.MyDeviceLastSeenInfoEntity import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntity @@ -48,6 +49,7 @@ import io.realm.annotations.RealmModule TrustLevelEntity::class, GossipingEventEntity::class, IncomingGossipingRequestEntity::class, - OutgoingGossipingRequestEntity::class + OutgoingGossipingRequestEntity::class, + MyDeviceLastSeenInfoEntity::class ]) internal class RealmCryptoStoreModule diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/mapper/CrossSigningKeysMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/mapper/CrossSigningKeysMapper.kt new file mode 100644 index 0000000000..0e2c9c7eb7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/mapper/CrossSigningKeysMapper.kt @@ -0,0 +1,85 @@ +/* + * 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.mapper + +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +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.store.db.model.KeyInfoEntity +import io.realm.RealmList +import timber.log.Timber +import javax.inject.Inject + +internal class CrossSigningKeysMapper @Inject constructor(moshi: Moshi) { + + private val signaturesAdapter = moshi.adapter>>(Types.newParameterizedType( + Map::class.java, + String::class.java, + Any::class.java + )) + + fun update(keyInfo: KeyInfoEntity, cryptoCrossSigningKey: CryptoCrossSigningKey) { + // update signatures? + keyInfo.signatures = serializeSignatures(cryptoCrossSigningKey.signatures) + keyInfo.usages = cryptoCrossSigningKey.usages?.toTypedArray()?.let { RealmList(*it) } + ?: RealmList() + } + + fun map(userId: String?, keyInfo: KeyInfoEntity?): CryptoCrossSigningKey? { + val pubKey = keyInfo?.publicKeyBase64 ?: return null + return CryptoCrossSigningKey( + userId = userId ?: "", + keys = mapOf("ed25519:$pubKey" to pubKey), + usages = keyInfo.usages.map { it }, + signatures = deserializeSignatures(keyInfo.signatures), + trustLevel = keyInfo.trustLevelEntity?.let { + DeviceTrustLevel( + crossSigningVerified = it.crossSignedVerified ?: false, + locallyVerified = it.locallyVerified ?: false + ) + } + ) + } + + fun map(keyInfo: CryptoCrossSigningKey): KeyInfoEntity { + return KeyInfoEntity().apply { + publicKeyBase64 = keyInfo.unpaddedBase64PublicKey + usages = keyInfo.usages?.let { RealmList(*it.toTypedArray()) } ?: RealmList() + signatures = serializeSignatures(keyInfo.signatures) + // TODO how to handle better, check if same keys? + // reset trust + trustLevelEntity = null + } + } + + fun serializeSignatures(signatures: Map>?): String { + return signaturesAdapter.toJson(signatures) + } + + fun deserializeSignatures(signatures: String?): Map>? { + if (signatures == null) { + return null + } + return try { + signaturesAdapter.fromJson(signatures) + } catch (failure: Throwable) { + Timber.e(failure) + null + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CryptoMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CryptoMapper.kt index 5a4938d1fe..d222fe0eed 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CryptoMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CryptoMapper.kt @@ -104,7 +104,8 @@ object CryptoMapper { Timber.e(failure) null } - } + }, + firstTimeSeenLocalTs = deviceInfoEntity.firstTimeSeenLocalTs ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/DeviceInfoEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/DeviceInfoEntity.kt index 98f931a455..7f16ad6357 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/DeviceInfoEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/DeviceInfoEntity.kt @@ -34,7 +34,12 @@ internal open class DeviceInfoEntity(@PrimaryKey var primaryKey: String = "", var keysMapJson: String? = null, var signatureMapJson: String? = null, var unsignedMapJson: String? = null, - var trustLevelEntity: TrustLevelEntity? = null + var trustLevelEntity: TrustLevelEntity? = null, + /** + * We use that to make distinction between old devices (there before mine) + * and new ones. Used for example to detect new unverified login + */ + var firstTimeSeenLocalTs: Long? = null ) : RealmObject() { // // Deserialize data diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/KeyInfoEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/KeyInfoEntity.kt index c40c752fbe..3ced818449 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/KeyInfoEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/KeyInfoEntity.kt @@ -16,8 +16,6 @@ package im.vector.matrix.android.internal.crypto.store.db.model -import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm -import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm import io.realm.RealmList import io.realm.RealmObject @@ -31,15 +29,4 @@ internal open class KeyInfoEntity( */ var signatures: String? = null, var trustLevelEntity: TrustLevelEntity? = null -) : RealmObject() { - - // Deserialize data - fun getSignatures(): Map>? { - return deserializeFromRealm(signatures) - } - - // Serialize data - fun putSignatures(deviceInfo: Map>?) { - signatures = serializeForRealm(deviceInfo) - } -} +) : RealmObject() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/MyDeviceLastSeenInfoEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/MyDeviceLastSeenInfoEntity.kt new file mode 100644 index 0000000000..04d258ed5f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/MyDeviceLastSeenInfoEntity.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.store.db.model + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class MyDeviceLastSeenInfoEntity( + /**The device id*/ + @PrimaryKey var deviceId: String? = null, + /** The device display name*/ + var displayName: String? = null, + /** The last time this device has been seen. */ + var lastSeenTs: Long? = null, + /** The last ip address*/ + var lastSeenIp: String? = null +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt index caa8cb9668..763e852cd1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt @@ -16,6 +16,7 @@ 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.model.OlmInboundGroupSessionWrapper import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm @@ -36,7 +37,7 @@ internal open class OlmInboundGroupSessionEntity( : RealmObject() { fun getInboundGroupSession(): OlmInboundGroupSessionWrapper? { - return deserializeFromRealm(olmInboundGroupSessionData) + return tryThis { deserializeFromRealm(olmInboundGroupSessionData) } } fun putInboundGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper?) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceTask.kt index fbbaa0e0f7..809ec0be44 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceTask.kt @@ -17,10 +17,9 @@ package im.vector.matrix.android.internal.crypto.tasks import im.vector.matrix.android.api.failure.Failure -import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse +import im.vector.matrix.android.api.failure.toRegistrationFlowResponse import im.vector.matrix.android.internal.crypto.api.CryptoApi import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceParams -import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task import org.greenrobot.eventbus.EventBus @@ -43,25 +42,9 @@ internal class DefaultDeleteDeviceTask @Inject constructor( apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams()) } } catch (throwable: Throwable) { - if (throwable is Failure.OtherServerError && throwable.httpCode == 401) { - // Parse to get a RegistrationFlowResponse - val registrationFlowResponse = try { - MoshiProvider.providesMoshi() - .adapter(RegistrationFlowResponse::class.java) - .fromJson(throwable.errorBody) - } catch (e: Exception) { - null - } - // check if the server response can be casted - if (registrationFlowResponse != null) { - throw Failure.RegistrationFlowError(registrationFlowResponse) - } else { - throw throwable - } - } else { - // Other error - throw throwable - } + throw throwable.toRegistrationFlowResponse() + ?.let { Failure.RegistrationFlowError(it) } + ?: throwable } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SendToDeviceTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SendToDeviceTask.kt index 58c461888b..361fd25cee 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SendToDeviceTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SendToDeviceTask.kt @@ -52,6 +52,8 @@ internal class DefaultSendToDeviceTask @Inject constructor( params.transactionId ?: Random.nextInt(Integer.MAX_VALUE).toString(), sendToDeviceBody ) + isRetryable = true + maxRetryCount = 3 } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadSigningKeysTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadSigningKeysTask.kt index 0a69039219..a66a20617c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadSigningKeysTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadSigningKeysTask.kt @@ -17,14 +17,13 @@ package im.vector.matrix.android.internal.crypto.tasks import im.vector.matrix.android.api.failure.Failure -import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse +import im.vector.matrix.android.api.failure.toRegistrationFlowResponse import im.vector.matrix.android.internal.crypto.api.CryptoApi import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey import im.vector.matrix.android.internal.crypto.model.rest.KeysQueryResponse import im.vector.matrix.android.internal.crypto.model.rest.UploadSigningKeysBody import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth import im.vector.matrix.android.internal.crypto.model.toRest -import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task import org.greenrobot.eventbus.EventBus @@ -65,37 +64,25 @@ internal class DefaultUploadSigningKeysTask @Inject constructor( } return } catch (throwable: Throwable) { - if (throwable is Failure.OtherServerError - && throwable.httpCode == 401 + val registrationFlowResponse = throwable.toRegistrationFlowResponse() + if (registrationFlowResponse != null && params.userPasswordAuth != null /* Avoid infinite loop */ && params.userPasswordAuth.session.isNullOrEmpty() ) { - try { - MoshiProvider.providesMoshi() - .adapter(RegistrationFlowResponse::class.java) - .fromJson(throwable.errorBody) - } catch (e: Exception) { - null - }?.let { - // Retry with authentication - try { - val req = executeRequest(eventBus) { - apiCall = cryptoApi.uploadSigningKeys( - uploadQuery.copy(auth = params.userPasswordAuth.copy(session = it.session)) - ) - } - if (req.failures?.isNotEmpty() == true) { - throw UploadSigningKeys(req.failures) - } - return - } catch (failure: Throwable) { - throw failure - } + // Retry with authentication + val req = executeRequest(eventBus) { + apiCall = cryptoApi.uploadSigningKeys( + uploadQuery.copy(auth = params.userPasswordAuth.copy(session = registrationFlowResponse.session)) + ) } + if (req.failures?.isNotEmpty() == true) { + throw UploadSigningKeys(req.failures) + } + } else { + // Other error + throw throwable } - // Other error - throw throwable } } } 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 7fd97d0231..689829b8e3 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 @@ -138,7 +138,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction( override fun onVerificationAccept(accept: ValidVerificationInfoAccept) { Timber.v("## SAS O: onVerificationAccept id:$transactionId") - if (state != VerificationTxState.Started) { + if (state != VerificationTxState.Started && state != VerificationTxState.SendingStart) { Timber.e("## SAS O: received accept request from invalid state $state") cancel(CancelCode.UnexpectedMessage) return @@ -148,7 +148,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction( || !KNOWN_HASHES.contains(accept.hash) || !KNOWN_MACS.contains(accept.messageAuthenticationCode) || accept.shortAuthenticationStrings.intersect(KNOWN_SHORT_CODES).isEmpty()) { - Timber.e("## SAS O: received accept request from invalid state") + Timber.e("## SAS O: received invalid accept") cancel(CancelCode.UnknownMethod) return } 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 290fc88878..59a5b80b99 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 @@ -117,6 +117,7 @@ internal class VerificationTransportToDevice( onDone: (() -> Unit)?) { Timber.d("## SAS sending msg type $type") Timber.v("## SAS sending msg info $verificationInfo") + val stateBeforeCall = tx?.state val tx = tx ?: return val contentMap = MXUsersDevicesMap() val toSendToDeviceObject = verificationInfo.toSendToDeviceObject() @@ -132,7 +133,11 @@ internal class VerificationTransportToDevice( if (onDone != null) { onDone() } else { - tx.state = nextState + // we may have received next state (e.g received accept in sending_start) + // We only put next state if the state was what is was before we started + if (tx.state == stateBeforeCall) { + tx.state = nextState + } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt index 2f3cdb9545..20651069b0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt @@ -53,6 +53,7 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa canonicalAlias = roomSummaryEntity.canonicalAlias, aliases = roomSummaryEntity.aliases.toList(), isEncrypted = roomSummaryEntity.isEncrypted, + encryptionEventTs = roomSummaryEntity.encryptionEventTs, typingRoomMemberIds = roomSummaryEntity.typingUserIds.toList(), breadcrumbsIndex = roomSummaryEntity.breadcrumbsIndex, roomEncryptionTrustLevel = roomSummaryEntity.roomEncryptionTrustLevel, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt index 7009e762fb..5236cd26e6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt @@ -48,6 +48,7 @@ internal open class RoomSummaryEntity( // this is required for querying var flatAliases: String = "", var isEncrypted: Boolean = false, + var encryptionEventTs: Long? = 0, var typingUserIds: RealmList = RealmList(), var roomEncryptionTrustLevelStr: String? = null, var inviterId: String? = null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt index 9714f42bb0..ea036f775b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.network import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.failure.shouldBeRetried import kotlinx.coroutines.CancellationException import kotlinx.coroutines.delay import org.greenrobot.eventbus.EventBus @@ -46,7 +47,7 @@ internal class Request(private val eventBus: EventBus?) { throw response.toFailure(eventBus) } } catch (exception: Throwable) { - if (isRetryable && currentRetryCount++ < maxRetryCount && exception is IOException) { + if (isRetryable && currentRetryCount++ < maxRetryCount && exception.shouldBeRetried()) { delay(currentDelay) currentDelay = (currentDelay * 2L).coerceAtMost(maxDelay) return execute() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/ChangePasswordTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/ChangePasswordTask.kt index 6400253d9f..60cf92e5d1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/ChangePasswordTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/ChangePasswordTask.kt @@ -16,9 +16,7 @@ package im.vector.matrix.android.internal.session.account -import im.vector.matrix.android.api.failure.Failure -import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse -import im.vector.matrix.android.internal.di.MoshiProvider +import im.vector.matrix.android.api.failure.toRegistrationFlowResponse import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task @@ -45,31 +43,20 @@ internal class DefaultChangePasswordTask @Inject constructor( apiCall = accountAPI.changePassword(changePasswordParams) } } catch (throwable: Throwable) { - if (throwable is Failure.OtherServerError - && throwable.httpCode == 401 + val registrationFlowResponse = throwable.toRegistrationFlowResponse() + + if (registrationFlowResponse != null /* Avoid infinite loop */ && changePasswordParams.auth?.session == null) { - try { - MoshiProvider.providesMoshi() - .adapter(RegistrationFlowResponse::class.java) - .fromJson(throwable.errorBody) - } catch (e: Exception) { - null - }?.let { - // Retry with authentication - try { - executeRequest(eventBus) { - apiCall = accountAPI.changePassword( - changePasswordParams.copy(auth = changePasswordParams.auth?.copy(session = it.session)) - ) - } - return - } catch (failure: Throwable) { - throw failure - } + // Retry with authentication + executeRequest(eventBus) { + apiCall = accountAPI.changePassword( + changePasswordParams.copy(auth = changePasswordParams.auth?.copy(session = registrationFlowResponse.session)) + ) } + } else { + throw throwable } - throw throwable } } } 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 f4886c72da..6e0adccfb9 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 @@ -136,6 +136,7 @@ internal class RoomSummaryUpdater @Inject constructor( roomSummaryEntity.aliases.addAll(roomAliases) roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|") roomSummaryEntity.isEncrypted = encryptionEvent != null + roomSummaryEntity.encryptionEventTs = encryptionEvent?.originServerTs roomSummaryEntity.typingUserIds.clear() roomSummaryEntity.typingUserIds.addAll(ephemeralResult?.typingUserIds.orEmpty()) @@ -152,7 +153,7 @@ internal class RoomSummaryUpdater @Inject constructor( if (updateMembers) { val otherRoomMembers = RoomMemberHelper(realm, roomId) - .queryRoomMembersEvent() + .queryActiveRoomMembersEvent() .notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId) .findAll() .asSequence() @@ -161,15 +162,7 @@ internal class RoomSummaryUpdater @Inject constructor( roomSummaryEntity.otherMemberIds.clear() roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers) if (roomSummaryEntity.isEncrypted) { - // 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)) + eventBus.post(SessionToCryptoRoomMembersUpdate(roomId, roomSummaryEntity.isDirect, roomSummaryEntity.otherMemberIds.toList() + userId)) } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt index b56594bd16..b3db4d0961 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt @@ -53,8 +53,6 @@ internal class DefaultSyncTask @Inject constructor( private suspend fun doSync(params: SyncTask.Params) { Timber.v("Sync task started on Thread: ${Thread.currentThread().name}") - // Maybe refresh the home server capabilities data we know - getHomeServerCapabilitiesTask.execute(Unit) val requestParams = HashMap() var timeout = 0L @@ -73,6 +71,9 @@ internal class DefaultSyncTask @Inject constructor( initialSyncProgressService.endAll() initialSyncProgressService.startTask(R.string.initial_sync_start_importing_account, 100) } + // Maybe refresh the home server capabilities data we know + getHomeServerCapabilitiesTask.execute(Unit) + val syncResponse = executeRequest(eventBus) { apiCall = syncAPI.sync(requestParams) } 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 aaa92ee398..792fb309f9 100644 --- a/matrix-sdk-android/src/main/res/values-hu/strings.xml +++ b/matrix-sdk-android/src/main/res/values-hu/strings.xml @@ -4,72 +4,72 @@ %1$s küldött egy képet. %s meghívója - %1$s meghívta %2$s -t + %1$s meghívta: %2$s %1$s meghívott %1$s csatlakozott %1$s kilépett %1$s elutasította a meghívást - %1$s kidobta %2$s -t - %1$s feloldotta tiltását %2$s -nak/nek - %1$s kitiltotta %2$s -t - %1$s visszavonta %2$s\'s meghívását - %1$s megváltoztatták a felhasználó képüket - %1$s megváltoztatták a megjelenő nevüket erre: %2$s - %1$s megváltoztatták a megjelenő nevüket erről %2$s erre %3$s - %1$s eltávolították a megjelenő nevüket (%2$s) + %1$s kidobta: %2$s + %1$s feloldotta %2$s tiltását + %1$s kitiltotta: %2$s + %1$s visszavonta %2$s meghívását + %1$s megváltoztatta a profilképét + %1$s megváltoztatta a megjelenő nevét erre: %2$s + %1$s megváltoztatta a megjelenítendő nevét erről: %2$s, erre: %3$s + %1$s eltávolította a megjelenítendő nevét (%2$s) %1$s megváltoztatta a témát erre: %2$s %1$s megváltoztatta a szoba nevét erre: %2$s %s videóhívást kezdeményezett. %s hanghívást kezdeményezett. - %s elfogadta a hívást. + %s fogadta a hívást. %s befejezte a hívást. %1$s láthatóvá tette a jövőbeli előzményeket %2$s számára - az összes szoba tag, onnantól, hogy meg lettek hívva. - az összes szoba tag, onnantól, hogy csatlakoztak. - az összes szoba tag. + az összes szobatag, onnantól, hogy meg lettek hívva. + az összes szobatag, onnantól, hogy csatlakoztak. + az összes szobatag. bárki. ismeretlen (%s). - %1$s bekapcsolta a végtől végig titkosítást (%2$s) + %1$s bekapcsolta a végpontok közötti titkosítást (%2$s) %1$s hanghívás konferenciát kérelmezett Hanghívás konferencia elindult Hanghívás konferencia befejeződött - (profilképp is meg lett változtatva) + (a profilkép is megváltozott) %1$s eltávolította a szoba nevét %1$s eltávolította a szoba témáját - %1$s megváltoztatták a profiljukat %2$s - "%1$s meghívót küldött %2$s -nak/-nek hogy csatlakozzon a szobához" - %1$s elfogadta a meghívót a %2$s -hoz + %1$s megváltoztatta a(z) %2$s profilját + %1$s meghívót küldött %2$s számára, hogy csatlakozzon a szobához + %1$s elfogadta a meghívót ebbe: %2$s ** Visszafejtés sikertelen: %s ** A küldő eszköze nem küldte el a kulcsokat ehhez az üzenethez. - Szerkesztés sikertelen + Kitakarás sikertelen Üzenet küldése sikertelen Kép feltöltése sikertelen - Hálózat hiba + Hálózati hiba Matrix hiba - Jelenleg nem lehetséges újracsatlakozni egy üres szobába. + Jelenleg nem lehetséges újracsatlakozni egy üres szobához. Titkosított üzenet - Email cím + E-mail cím Telefonszám %1$s küldött egy matricát. Válasz erre: - kép elküldve. - videó elküldve. - hangfájl elküldve. - fájl elküldve. + képet küldött. + videót küldött. + hangfájlt küldött. + fájlt küldött. - %s meghívott + Meghívó tőle: %s Meghívó egy szobába %1$s és %2$s Üres szoba @@ -171,7 +171,7 @@ Üzenet küldése… Küldő sor ürítése - %1$s visszavonta a meghívót a belépéshez ebbe a szobába: %2$s + %1$s visszavonta %2$s meghívását, hogy csatlakozzon a szobához %1$s meghívója. Ok: %2$s %1$s meghívta őt: %2$s. Ok: %3$s %1$s meghívott. Ok: %2$s diff --git a/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml b/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml index 6e3ced3048..38affc0599 100644 --- a/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml +++ b/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml @@ -170,4 +170,39 @@ %1$s 撤回了对 %2$s 邀请 置顶 + %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 移除了此聊天室的 %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/strings_RiotX.xml b/matrix-sdk-android/src/main/res/values/strings_RiotX.xml index d781ec5f1e..6eb46fd7df 100644 --- a/matrix-sdk-android/src/main/res/values/strings_RiotX.xml +++ b/matrix-sdk-android/src/main/res/values/strings_RiotX.xml @@ -1,4 +1,32 @@ + + diff --git a/matrix-sdk-android/src/sharedTest/java/im/vector/matrix/android/test/shared/TestRules.kt b/matrix-sdk-android/src/sharedTest/java/im/vector/matrix/android/test/shared/TestRules.kt index 77408322c1..bf073ecbc3 100644 --- a/matrix-sdk-android/src/sharedTest/java/im/vector/matrix/android/test/shared/TestRules.kt +++ b/matrix-sdk-android/src/sharedTest/java/im/vector/matrix/android/test/shared/TestRules.kt @@ -18,7 +18,7 @@ package im.vector.matrix.android.test.shared import net.lachlanmckee.timberjunit.TimberTestRule -fun createTimberTestRule(): TimberTestRule { +internal fun createTimberTestRule(): TimberTestRule { return TimberTestRule.builder() .showThread(false) .showTimestamp(false) diff --git a/tools/templates/RiotXFeature/globals.xml.ftl b/tools/templates/RiotXFeature/globals.xml.ftl new file mode 100644 index 0000000000..7cafd80483 --- /dev/null +++ b/tools/templates/RiotXFeature/globals.xml.ftl @@ -0,0 +1,6 @@ + + + <#include "root://activities/common/common_globals.xml.ftl" /> + + + diff --git a/tools/templates/RiotXFeature/recipe.xml.ftl b/tools/templates/RiotXFeature/recipe.xml.ftl new file mode 100644 index 0000000000..88160fd0f3 --- /dev/null +++ b/tools/templates/RiotXFeature/recipe.xml.ftl @@ -0,0 +1,37 @@ + +<#import "root://activities/common/kotlin_macros.ftl" as kt> + + + + + + <#if createActivity> + + + + + + + + + + + + + + + + + <#if createViewEvents> + + + + + diff --git a/tools/templates/RiotXFeature/root/res/layout/fragment.xml.ftl b/tools/templates/RiotXFeature/root/res/layout/fragment.xml.ftl new file mode 100644 index 0000000000..539c40f3f9 --- /dev/null +++ b/tools/templates/RiotXFeature/root/res/layout/fragment.xml.ftl @@ -0,0 +1,20 @@ + + + + + + diff --git a/tools/templates/RiotXFeature/root/src/app_package/Action.kt.ftl b/tools/templates/RiotXFeature/root/src/app_package/Action.kt.ftl new file mode 100644 index 0000000000..7492907e6c --- /dev/null +++ b/tools/templates/RiotXFeature/root/src/app_package/Action.kt.ftl @@ -0,0 +1,5 @@ +package ${escapeKotlinIdentifiers(packageName)} + +import im.vector.riotx.core.platform.VectorViewModelAction + +sealed class ${actionClass}: VectorViewModelAction \ No newline at end of file diff --git a/tools/templates/RiotXFeature/root/src/app_package/Activity.kt.ftl b/tools/templates/RiotXFeature/root/src/app_package/Activity.kt.ftl new file mode 100644 index 0000000000..fdac319482 --- /dev/null +++ b/tools/templates/RiotXFeature/root/src/app_package/Activity.kt.ftl @@ -0,0 +1,49 @@ +package ${escapeKotlinIdentifiers(packageName)} + +import android.content.Context +import android.content.Intent +import androidx.appcompat.widget.Toolbar +import im.vector.riotx.R +import im.vector.riotx.core.extensions.addFragment +import im.vector.riotx.core.platform.ToolbarConfigurable +import im.vector.riotx.core.platform.VectorBaseActivity + +//TODO: add this activity to manifest +class ${activityClass} : VectorBaseActivity(), ToolbarConfigurable { + + companion object { + + <#if createFragmentArgs> + private const val EXTRA_FRAGMENT_ARGS = "EXTRA_FRAGMENT_ARGS" + + fun newIntent(context: Context, args: ${fragmentArgsClass}): Intent { + return Intent(context, ${activityClass}::class.java).apply { + putExtra(EXTRA_FRAGMENT_ARGS, args) + } + } + <#else> + fun newIntent(context: Context): Intent { + return Intent(context, ${activityClass}::class.java) + } + + } + + override fun getLayoutRes() = R.layout.activity_simple + + override fun initUiAndData() { + if (isFirstCreation()) { + <#if createFragmentArgs> + val fragmentArgs: ${fragmentArgsClass} = intent?.extras?.getParcelable(EXTRA_FRAGMENT_ARGS) + ?: return + addFragment(R.id.simpleFragmentContainer, ${fragmentClass}::class.java, fragmentArgs) + <#else> + addFragment(R.id.simpleFragmentContainer, ${fragmentClass}::class.java) + + } + } + + override fun configure(toolbar: Toolbar) { + configureToolbar(toolbar) + } + +} diff --git a/tools/templates/RiotXFeature/root/src/app_package/Fragment.kt.ftl b/tools/templates/RiotXFeature/root/src/app_package/Fragment.kt.ftl new file mode 100644 index 0000000000..df69078d9d --- /dev/null +++ b/tools/templates/RiotXFeature/root/src/app_package/Fragment.kt.ftl @@ -0,0 +1,47 @@ +package ${escapeKotlinIdentifiers(packageName)} + +import android.os.Bundle +<#if createFragmentArgs> +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize +import com.airbnb.mvrx.args + +import android.view.View +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.riotx.R +import im.vector.riotx.core.platform.VectorBaseFragment +import javax.inject.Inject + +<#if createFragmentArgs> +@Parcelize +data class ${fragmentArgsClass}() : Parcelable + + +//TODO: add this fragment into FragmentModule +class ${fragmentClass} @Inject constructor( + private val viewModelFactory: ${viewModelClass}.Factory +) : VectorBaseFragment(), ${viewModelClass}.Factory by viewModelFactory { + + <#if createFragmentArgs> + private val fragmentArgs: ${fragmentArgsClass} by args() + + private val viewModel: ${viewModelClass} by fragmentViewModel() + + override fun getLayoutResId() = R.layout.${fragmentLayout} + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + // Initialize your view, subscribe to viewModel... + } + + override fun onDestroyView() { + super.onDestroyView() + // Clear your view, unsubscribe... + } + + override fun invalidate() = withState(viewModel) { state -> + //TODO + } + +} diff --git a/tools/templates/RiotXFeature/root/src/app_package/ViewEvents.kt.ftl b/tools/templates/RiotXFeature/root/src/app_package/ViewEvents.kt.ftl new file mode 100644 index 0000000000..4d7d2ee450 --- /dev/null +++ b/tools/templates/RiotXFeature/root/src/app_package/ViewEvents.kt.ftl @@ -0,0 +1,5 @@ +package ${escapeKotlinIdentifiers(packageName)} + +import im.vector.riotx.core.platform.VectorViewEvents + +sealed class ${viewEventsClass} : VectorViewEvents \ No newline at end of file diff --git a/tools/templates/RiotXFeature/root/src/app_package/ViewModel.kt.ftl b/tools/templates/RiotXFeature/root/src/app_package/ViewModel.kt.ftl new file mode 100644 index 0000000000..f4090b40e6 --- /dev/null +++ b/tools/templates/RiotXFeature/root/src/app_package/ViewModel.kt.ftl @@ -0,0 +1,44 @@ +package ${escapeKotlinIdentifiers(packageName)} + +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.riotx.core.platform.VectorViewModel + +<#if createViewEvents> +<#else> +import im.vector.riotx.core.platform.EmptyViewEvents + + +class ${viewModelClass} @AssistedInject constructor(@Assisted initialState: ${viewStateClass}) + <#if createViewEvents> + : VectorViewModel<${viewStateClass}, ${actionClass}, ${viewEventsClass}>(initialState) { + <#else> + : VectorViewModel<${viewStateClass}, ${actionClass}, EmptyViewEvents>(initialState) { + + + @AssistedInject.Factory + interface Factory { + fun create(initialState: ${viewStateClass}): ${viewModelClass} + } + + companion object : MvRxViewModelFactory<${viewModelClass}, ${viewStateClass}> { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: ${viewStateClass}): ${viewModelClass}? { + val factory = when (viewModelContext) { + is FragmentViewModelContext -> viewModelContext.fragment as? Factory + is ActivityViewModelContext -> viewModelContext.activity as? Factory + } + return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") + } + } + + override fun handle(action: ${actionClass}) { + //TODO + } + +} diff --git a/tools/templates/RiotXFeature/root/src/app_package/ViewState.kt.ftl b/tools/templates/RiotXFeature/root/src/app_package/ViewState.kt.ftl new file mode 100644 index 0000000000..55e1f5f549 --- /dev/null +++ b/tools/templates/RiotXFeature/root/src/app_package/ViewState.kt.ftl @@ -0,0 +1,5 @@ +package ${escapeKotlinIdentifiers(packageName)} + +import com.airbnb.mvrx.MvRxState + +data class ${viewStateClass}() : MvRxState \ No newline at end of file diff --git a/tools/templates/RiotXFeature/template.xml b/tools/templates/RiotXFeature/template.xml new file mode 100644 index 0000000000..33d2edfc70 --- /dev/null +++ b/tools/templates/RiotXFeature/template.xml @@ -0,0 +1,121 @@ + + diff --git a/tools/templates/configure.sh b/tools/templates/configure.sh new file mode 100755 index 0000000000..eb2aa0dbec --- /dev/null +++ b/tools/templates/configure.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +# +# Copyright 2020 New Vector Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +echo "Configure RiotX Template..." +{ +ln -s $(pwd)/RiotXFeature /Applications/Android\ Studio.app/Contents/plugins/android/lib/templates/other +} && { + echo "Please restart Android Studio." +} diff --git a/vector/build.gradle b/vector/build.gradle index 2a898f5621..69608cf712 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -235,6 +235,15 @@ android { kotlinOptions { jvmTarget = "1.8" } + + sourceSets { + androidTest { + java.srcDirs += "src/sharedTest/java" + } + test { + java.srcDirs += "src/sharedTest/java" + } + } } dependencies { @@ -250,6 +259,7 @@ dependencies { def daggerVersion = '2.25.4' def autofill_version = "1.0.0" def work_version = '2.3.3' + def arch_version = '2.1.0' implementation project(":matrix-sdk-android") implementation project(":matrix-sdk-android-rx") @@ -378,10 +388,18 @@ dependencies { // TESTS testImplementation 'junit:junit:4.12' testImplementation 'org.amshove.kluent:kluent-android:1.44' + // Plant Timber tree for test + testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1' + androidTestImplementation 'androidx.test:core:1.2.0' androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test:rules:1.2.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'org.amshove.kluent:kluent-android:1.44' + androidTestImplementation "androidx.arch.core:core-testing:$arch_version" + // Plant Timber tree for test + androidTestImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1' } if (getGradle().getStartParameter().getTaskRequests().toString().contains("Gplay")) { diff --git a/vector/src/androidTest/java/im/vector/riotx/InstrumentedTest.kt b/vector/src/androidTest/java/im/vector/riotx/InstrumentedTest.kt new file mode 100644 index 0000000000..c34a3ef67b --- /dev/null +++ b/vector/src/androidTest/java/im/vector/riotx/InstrumentedTest.kt @@ -0,0 +1,32 @@ +/* + * 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 + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import im.vector.riotx.test.shared.createTimberTestRule +import org.junit.Rule + +interface InstrumentedTest { + + @Rule + fun timberTestRule() = createTimberTestRule() + + fun context(): Context { + return ApplicationProvider.getApplicationContext() + } +} diff --git a/vector/src/androidTest/java/im/vector/riotx/features/reactions/data/EmojiDataSourceTest.kt b/vector/src/androidTest/java/im/vector/riotx/features/reactions/data/EmojiDataSourceTest.kt new file mode 100644 index 0000000000..a3e45c3cf2 --- /dev/null +++ b/vector/src/androidTest/java/im/vector/riotx/features/reactions/data/EmojiDataSourceTest.kt @@ -0,0 +1,95 @@ +/* + * 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.reactions.data + +import im.vector.riotx.InstrumentedTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import kotlin.system.measureTimeMillis + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class EmojiDataSourceTest : InstrumentedTest { + + @Test + fun checkParsingTime() { + val time = measureTimeMillis { + EmojiDataSource(context().resources) + } + + assertTrue("Too long to parse", time < 100) + } + + @Test + fun checkNumberOfResult() { + val emojiDataSource = EmojiDataSource(context().resources) + + assertEquals("Wrong number of emojis", 1545, emojiDataSource.rawData.emojis.size) + assertEquals("Wrong number of categories", 8, emojiDataSource.rawData.categories.size) + assertEquals("Wrong number of aliases", 57, emojiDataSource.rawData.aliases.size) + } + + @Test + fun searchTestEmptySearch() { + val emojiDataSource = EmojiDataSource(context().resources) + + assertEquals("Empty search should return 1545 results", 1545, emojiDataSource.filterWith("").size) + } + + @Test + fun searchTestNoResult() { + val emojiDataSource = EmojiDataSource(context().resources) + + assertTrue("Should not have result", emojiDataSource.filterWith("noresult").isEmpty()) + } + + @Test + fun searchTestOneResult() { + val emojiDataSource = EmojiDataSource(context().resources) + + assertEquals("Should have 1 result", 1, emojiDataSource.filterWith("france").size) + } + + @Test + fun searchTestManyResult() { + val emojiDataSource = EmojiDataSource(context().resources) + + assertTrue("Should have many result", emojiDataSource.filterWith("fra").size > 1) + } + + @Test + fun testTada() { + val emojiDataSource = EmojiDataSource(context().resources) + + val result = emojiDataSource.filterWith("tada") + + assertEquals("Should find tada emoji", 1, result.size) + assertEquals("Should find tada emoji", "🎉", result[0].emoji) + } + + @Test + fun testQuickReactions() { + val emojiDataSource = EmojiDataSource(context().resources) + + assertEquals("Should have 8 quick reactions", 8, emojiDataSource.getQuickReactions().size) + } +} diff --git a/vector/src/main/java/im/vector/riotx/core/dialogs/ManuallyVerifyDialog.kt b/vector/src/main/java/im/vector/riotx/core/dialogs/ManuallyVerifyDialog.kt new file mode 100644 index 0000000000..9863f7030f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/dialogs/ManuallyVerifyDialog.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.core.dialogs + +import android.app.Activity +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import im.vector.matrix.android.api.extensions.getFingerprintHumanReadable +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo +import im.vector.riotx.R + +object ManuallyVerifyDialog { + + fun show(activity: Activity, cryptoDeviceInfo: CryptoDeviceInfo, onVerified: (() -> Unit)) { + val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_device_verify, null) + val builder = AlertDialog.Builder(activity) + .setTitle(R.string.cross_signing_verify_by_text) + .setView(dialogLayout) + .setPositiveButton(R.string.encryption_information_verify) { _, _ -> + onVerified() + } + .setNegativeButton(R.string.cancel, null) + + dialogLayout.findViewById(R.id.encrypted_device_info_device_name)?.let { + it.text = cryptoDeviceInfo.displayName() + } + + dialogLayout.findViewById(R.id.encrypted_device_info_device_id)?.let { + it.text = cryptoDeviceInfo.deviceId + } + + dialogLayout.findViewById(R.id.encrypted_device_info_device_key)?.let { + it.text = cryptoDeviceInfo.getFingerprintHumanReadable() + } + + builder.show() + } +} diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt index bda4426c45..e82e8b3856 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt @@ -30,6 +30,10 @@ import io.reactivex.Single abstract class VectorViewModel(initialState: S) : BaseMvRxViewModel(initialState, false) { + interface Factory { + fun create(state: S): BaseMvRxViewModel + } + // Used to post transient events to the View protected val _viewEvents = PublishDataSource() val viewEvents: DataSource = _viewEvents diff --git a/vector/src/main/java/im/vector/riotx/core/ui/list/GenericButtonItem.kt b/vector/src/main/java/im/vector/riotx/core/ui/list/GenericButtonItem.kt new file mode 100644 index 0000000000..c0fdb010a5 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/ui/list/GenericButtonItem.kt @@ -0,0 +1,65 @@ +/* + * 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.list + +import android.view.View +import androidx.annotation.ColorInt +import androidx.annotation.DrawableRes +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.google.android.material.button.MaterialButton +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel +import im.vector.riotx.features.themes.ThemeUtils + +/** + * A generic button list item. + */ +@EpoxyModelClass(layout = R.layout.item_generic_button) +abstract class GenericButtonItem : VectorEpoxyModel() { + + @EpoxyAttribute + var text: String? = null + + @EpoxyAttribute + var itemClickAction: View.OnClickListener? = null + + @EpoxyAttribute + @ColorInt + var textColor: Int? = null + + @EpoxyAttribute + @DrawableRes + var iconRes: Int? = null + + override fun bind(holder: Holder) { + holder.button.text = text + val textColor = textColor ?: ThemeUtils.getColor(holder.view.context, R.attr.riotx_text_primary) + holder.button.setTextColor(textColor) + if (iconRes != null) { + holder.button.setIconResource(iconRes!!) + } else { + holder.button.icon = null + } + + itemClickAction?.let { holder.view.setOnClickListener(it) } + } + + class Holder : VectorEpoxyHolder() { + val button by bind(R.id.itemGenericItemButton) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt index a5ec0591f5..a9a0cee0d6 100644 --- a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt @@ -34,8 +34,10 @@ import im.vector.riotx.core.utils.deleteAllFiles import im.vector.riotx.features.home.HomeActivity import im.vector.riotx.features.login.LoginActivity import im.vector.riotx.features.notifications.NotificationDrawerManager +import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.signout.hard.SignedOutActivity import im.vector.riotx.features.signout.soft.SoftLogoutActivity +import im.vector.riotx.features.ui.UiStateRepository import kotlinx.android.parcel.Parcelize import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -78,6 +80,8 @@ class MainActivity : VectorBaseActivity() { @Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var sessionHolder: ActiveSessionHolder @Inject lateinit var errorFormatter: ErrorFormatter + @Inject lateinit var vectorPreferences: VectorPreferences + @Inject lateinit var uiStateRepository: UiStateRepository override fun injectWith(injector: ScreenComponent) { injector.inject(this) @@ -127,7 +131,7 @@ class MainActivity : VectorBaseActivity() { // Just do the local cleanup Timber.w("Account deactivated, start app") sessionHolder.clearActiveSession() - doLocalCleanup() + doLocalCleanup(clearPreferences = true) startNextActivityAndFinish() } args.clearCredentials -> session.signOut( @@ -136,7 +140,7 @@ class MainActivity : VectorBaseActivity() { override fun onSuccess(data: Unit) { Timber.w("SIGN_OUT: success, start app") sessionHolder.clearActiveSession() - doLocalCleanup() + doLocalCleanup(clearPreferences = true) startNextActivityAndFinish() } @@ -147,7 +151,7 @@ class MainActivity : VectorBaseActivity() { args.clearCache -> session.clearCache( object : MatrixCallback { override fun onSuccess(data: Unit) { - doLocalCleanup() + doLocalCleanup(clearPreferences = false) session.startSyncing(applicationContext) startNextActivityAndFinish() } @@ -164,10 +168,15 @@ class MainActivity : VectorBaseActivity() { Timber.w("Ignoring invalid token global error") } - private fun doLocalCleanup() { + private fun doLocalCleanup(clearPreferences: Boolean) { GlobalScope.launch(Dispatchers.Main) { // On UI Thread Glide.get(this@MainActivity).clearMemory() + + if (clearPreferences) { + vectorPreferences.clearPreferences() + uiStateRepository.reset() + } withContext(Dispatchers.IO) { // On BG thread Glide.get(this@MainActivity).clearDiskCache() 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 ddb50628d6..0159dc7c3a 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 @@ -27,14 +27,13 @@ import im.vector.matrix.android.api.session.crypto.verification.SasVerificationT 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.IncomingRequestCancellation +import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest 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 @@ -75,7 +74,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context, privat session = null } - override fun onSecretShareRequest(request: IncomingSecretShareRequest) : Boolean { + 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() @@ -124,19 +123,11 @@ class KeyRequestHandler @Inject constructor(private val context: Context, privat deviceInfo.trustLevel = DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false) // can we get more info on this device? - session?.cryptoService()?.getDevicesList(object : MatrixCallback { - override fun onSuccess(data: DevicesListResponse) { - data.devices?.find { it.deviceId == deviceId }?.let { - postAlert(context, userId, deviceId, true, deviceInfo, it) - } ?: run { - postAlert(context, userId, deviceId, true, deviceInfo) - } - } - - override fun onFailure(failure: Throwable) { - postAlert(context, userId, deviceId, true, deviceInfo) - } - }) + session?.cryptoService()?.getMyDevicesInfo()?.firstOrNull { it.deviceId == deviceId }?.let { + postAlert(context, userId, deviceId, true, deviceInfo, it) + } ?: kotlin.run { + postAlert(context, userId, deviceId, true, deviceInfo) + } } else { postAlert(context, userId, deviceId, false, deviceInfo) } 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 index 34a49e852e..d09eafee58 100644 --- 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 @@ -95,6 +95,14 @@ class BootstrapConfirmPassphraseFragment @Inject constructor( sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) } .disposeOnDestroyView() + + bootstrapSubmit.clicks() + .debounce(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + submit() + } + .disposeOnDestroyView() } private fun submit() = withState(sharedViewModel) { state -> @@ -113,8 +121,6 @@ class BootstrapConfirmPassphraseFragment @Inject constructor( } 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) 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 index 75ddb737f4..b70902631a 100644 --- 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 @@ -18,6 +18,7 @@ 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.failure.toRegistrationFlowResponse 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 @@ -28,13 +29,11 @@ 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.internal.auth.data.LoginFlowTypes -import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding 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.keysbackup.util.extractCurveKeyFromRecoveryKey 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.ViewModelTask @@ -230,15 +229,10 @@ class BootstrapCrossSigningTask @Inject constructor( 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) { + } else { + val registrationFlowResponse = failure.toRegistrationFlowResponse() + if (registrationFlowResponse != null) { + if (registrationFlowResponse.flows?.any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true } != true) { // can't do this from here return BootstrapResult.UnsupportedAuthFlow() } 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 index 35e2e1373c..952b0e5d03 100644 --- 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 @@ -90,6 +90,14 @@ class BootstrapEnterPassphraseFragment @Inject constructor( sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) } .disposeOnDestroyView() + + bootstrapSubmit.clicks() + .debounce(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + submit() + } + .disposeOnDestroyView() } private fun submit() = withState(sharedViewModel) { state -> @@ -108,8 +116,6 @@ class BootstrapEnterPassphraseFragment @Inject constructor( } 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) 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 ccd3e6578a..2bd815b6df 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 @@ -66,7 +66,7 @@ class IncomingVerificationRequestHandler @Inject constructor( uid, context.getString(R.string.sas_incoming_request_notif_title), context.getString(R.string.sas_incoming_request_notif_content, name), - R.drawable.shield, + R.drawable.ic_shield_black, shouldBeDisplayedIn = { activity -> if (activity is VectorBaseActivity) { // TODO a bit too hugly :/ 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 e07150ed4f..7a3d38f649 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 @@ -165,7 +165,9 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { } else { otherUserShield.setImageResource(R.drawable.ic_shield_warning) } - otherUserNameText.text = getString(R.string.complete_security) + otherUserNameText.text = getString( + if (state.selfVerificationMode) R.string.crosssigning_verify_this_session else R.string.crosssigning_verify_session + ) otherUserShield.isVisible = true } else { avatarRenderer.render(matrixItem, otherUserAvatarImageView) @@ -241,7 +243,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { } when (state.qrTransactionState) { - is VerificationTxState.QrScannedByOther -> { + is VerificationTxState.QrScannedByOther -> { showFragment(VerificationQrScannedByOtherFragment::class, Bundle()) return@withState } @@ -252,19 +254,19 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { }) return@withState } - is VerificationTxState.Verified -> { + is VerificationTxState.Verified -> { showFragment(VerificationConclusionFragment::class, Bundle().apply { putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(true, null, state.isMe)) }) return@withState } - is VerificationTxState.Cancelled -> { + is VerificationTxState.Cancelled -> { showFragment(VerificationConclusionFragment::class, Bundle().apply { putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(false, state.qrTransactionState.cancelCode.value, state.isMe)) }) return@withState } - else -> Unit + else -> Unit } // At this point there is no SAS transaction for this request 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 6a2b7825ac..7a003c3722 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 @@ -423,7 +423,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( } } catch (failure: Throwable) { // Just ignore for now - Timber.v("## Failed to restore backup after SSSS recovery") + Timber.e(failure, "## Failed to restore backup after SSSS recovery") } } } 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 ac2e2b7fa9..60c974c291 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 @@ -167,20 +167,26 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { val crossSigningEnabledOnAccount = myCrossSigningKeys != null if (!crossSigningEnabledOnAccount && !sharedActionViewModel.isAccountCreation) { - // We need to ask - promptSecurityEvent( - session, - R.string.upgrade_security, - R.string.security_prompt_text - ) { - it.navigator.upgradeSessionSecurity(it) + // Do not propose for SSO accounts, because we do not support yet confirming account credentials using SSO + if (session.getHomeServerCapabilities().canChangePassword) { + // We need to ask + promptSecurityEvent( + session, + R.string.upgrade_security, + R.string.security_prompt_text + ) { + it.navigator.upgradeSessionSecurity(it) + } + } else { + // Do not do it again + sharedActionViewModel.hasDisplayedCompleteSecurityPrompt = true } } else if (myCrossSigningKeys?.isTrusted() == false) { // We need to ask promptSecurityEvent( session, - R.string.complete_security, - R.string.crosssigning_verify_this_session + R.string.crosssigning_verify_this_session, + R.string.confirm_your_identity ) { it.navigator.waitSessionVerification(it) } 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 564b030081..b2638e65fc 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,4 +1,3 @@ - /* * Copyright 2019 New Vector Ltd * @@ -31,6 +30,7 @@ import com.google.android.material.bottomnavigation.BottomNavigationMenuView import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.riotx.R import im.vector.riotx.core.extensions.commitTransactionNow import im.vector.riotx.core.glide.GlideApp @@ -43,6 +43,7 @@ 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.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS import im.vector.riotx.features.workers.signout.SignOutViewModel import kotlinx.android.synthetic.main.fragment_home_detail.* import timber.log.Timber @@ -61,7 +62,7 @@ class HomeDetailFragment @Inject constructor( private val unreadCounterBadgeViews = arrayListOf() private val viewModel: HomeDetailViewModel by fragmentViewModel() - private val unknownDeviceDetectorSharedViewModel : UnknownDeviceDetectorSharedViewModel by activityViewModel() + private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel() private lateinit var sharedActionViewModel: HomeSharedActionViewModel @@ -87,39 +88,82 @@ class HomeDetailFragment @Inject constructor( 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_with_info, newest.displayName ?: "", newest.deviceId ?: ""), - 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 {} - } - ) + unknownDeviceDetectorSharedViewModel.subscribe { state -> + state.unknownSessions.invoke()?.let { unknownDevices -> +// Timber.v("## Detector Triggerred in fragment - ${unknownDevices.firstOrNull()}") + if (unknownDevices.firstOrNull()?.currentSessionTrust == true) { + val uid = "review_login" + alertManager.cancelAlert(uid) + val olderUnverified = unknownDevices.filter { !it.isNew } + val newest = unknownDevices.firstOrNull { it.isNew }?.deviceInfo + if (newest != null) { + promptForNewUnknownDevices(uid, state, newest) + } else if (olderUnverified.isNotEmpty()) { + // In this case we prompt to go to settings to review logins + promptToReviewChanges(uid, state, olderUnverified.map { it.deviceInfo }) + } } } } } + private fun promptForNewUnknownDevices(uid: String, state: UnknownDevicesState, newest: DeviceInfo) { + val user = state.myMatrixItem + alertManager.postVectorAlert( + VerificationVectorAlert( + uid = uid, + title = getString(R.string.new_session), + description = getString(R.string.new_session_review_with_info, newest.displayName ?: "", newest.deviceId ?: ""), + 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 ?: "") + unknownDeviceDetectorSharedViewModel.handle( + UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) } ?: emptyList()) + ) + } + dismissedAction = Runnable { + unknownDeviceDetectorSharedViewModel.handle( + UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) } ?: emptyList()) + ) + } + } + ) + } + + private fun promptToReviewChanges(uid: String, state: UnknownDevicesState, oldUnverified: List) { + val user = state.myMatrixItem + alertManager.postVectorAlert( + VerificationVectorAlert( + uid = uid, + title = getString(R.string.review_logins), + description = getString(R.string.verify_other_sessions), + iconId = R.drawable.ic_shield_warning + ).apply { + matrixItem = user + colorInt = ContextCompat.getColor(requireActivity(), R.color.riotx_accent) + contentAction = Runnable { + (weakCurrentActivity?.get() as? VectorBaseActivity)?.let { + // mark as ignored to avoid showing it again + unknownDeviceDetectorSharedViewModel.handle( + UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(oldUnverified.mapNotNull { it.deviceId }) + ) + it.navigator.openSettings(it, EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS) + } + } + dismissedAction = Runnable { + unknownDeviceDetectorSharedViewModel.handle( + UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(oldUnverified.mapNotNull { it.deviceId }) + ) + } + } + ) + } + private fun onGroupChange(groupSummary: GroupSummary?) { groupSummary?.let { // Use GlideApp with activity context to avoid the glideRequests to be paused diff --git a/vector/src/main/java/im/vector/riotx/features/home/UnknownDeviceDetectorSharedViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/UnknownDeviceDetectorSharedViewModel.kt new file mode 100644 index 0000000000..a05e9ee985 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/UnknownDeviceDetectorSharedViewModel.kt @@ -0,0 +1,156 @@ +/* + * 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.Success +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.ViewModelContext +import im.vector.matrix.android.api.NoOpMatrixCallback +import im.vector.matrix.android.api.extensions.orFalse +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.util.MatrixItem +import im.vector.matrix.android.api.util.Optional +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.rest.DeviceInfo +import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo +import im.vector.matrix.rx.rx +import im.vector.riotx.core.di.HasScreenInjector +import im.vector.riotx.core.platform.EmptyViewEvents +import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.platform.VectorViewModelAction +import im.vector.riotx.features.settings.VectorPreferences +import io.reactivex.Observable +import io.reactivex.functions.Function3 +import timber.log.Timber +import java.util.concurrent.TimeUnit + +data class UnknownDevicesState( + val myMatrixItem: MatrixItem.UserItem? = null, + val unknownSessions: Async> = Uninitialized +) : MvRxState + +data class DeviceDetectionInfo( + val deviceInfo: DeviceInfo, + val isNew: Boolean, + val currentSessionTrust: Boolean +) + +class UnknownDeviceDetectorSharedViewModel( + session: Session, + private val vectorPreferences: VectorPreferences, + initialState: UnknownDevicesState) + : VectorViewModel(initialState) { + + sealed class Action : VectorViewModelAction { + data class IgnoreDevice(val deviceIds: List) : Action() + } + + private val ignoredDeviceList = ArrayList() + + init { + + val currentSessionTs = session.cryptoService().getCryptoDeviceInfo(session.myUserId).firstOrNull { + it.deviceId == session.sessionParams.credentials.deviceId + }?.firstTimeSeenLocalTs ?: System.currentTimeMillis() + Timber.v("## Detector - Current Session first time seen $currentSessionTs") + + ignoredDeviceList.addAll( + vectorPreferences.getUnknownDeviceDismissedList().also { + Timber.v("## Detector - Remembered ignored list $it") + } + ) + + Observable.combineLatest, List, Optional, List>( + session.rx().liveUserCryptoDevices(session.myUserId), + session.rx().liveMyDeviceInfo(), + session.rx().liveCrossSigningPrivateKeys(), + Function3 { cryptoList, infoList, pInfo -> +// Timber.v("## Detector trigger ${cryptoList.map { "${it.deviceId} ${it.trustLevel}" }}") +// Timber.v("## Detector trigger canCrossSign ${pInfo.get().selfSigned != null}") + infoList + .filter { info -> + // filter verified session, by checking the crypto device info + cryptoList.firstOrNull { info.deviceId == it.deviceId }?.isVerified?.not().orFalse() + } + // filter out ignored devices + .filter { !ignoredDeviceList.contains(it.deviceId) } + .sortedByDescending { it.lastSeenTs } + .map { deviceInfo -> + val deviceKnownSince = cryptoList.firstOrNull { it.deviceId == deviceInfo.deviceId }?.firstTimeSeenLocalTs ?: 0 + DeviceDetectionInfo( + deviceInfo, + deviceKnownSince > currentSessionTs + 60_000, // short window to avoid false positive, + pInfo.getOrNull()?.selfSigned != null // adding this to pass distinct when cross sign change + ) + } + } + ) + .distinctUntilChanged() + .execute { async -> +// Timber.v("## Detector trigger passed distinct") + copy( + myMatrixItem = session.getUser(session.myUserId)?.toMatrixItem(), + unknownSessions = async + ) + } + + session.rx().liveUserCryptoDevices(session.myUserId) + .distinct() + .throttleLast(5_000, TimeUnit.MILLISECONDS) + .subscribe { + // If we have a new crypto device change, we might want to trigger refresh of device info + session.cryptoService().fetchDevicesList(NoOpMatrixCallback()) + }.disposeOnClear() + + // trigger a refresh of lastSeen / last Ip + session.cryptoService().fetchDevicesList(NoOpMatrixCallback()) + } + + override fun handle(action: Action) { + when (action) { + is Action.IgnoreDevice -> { + ignoredDeviceList.addAll(action.deviceIds) + // local echo + withState { state -> + state.unknownSessions.invoke()?.let { detectedSessions -> + val updated = detectedSessions.filter { !action.deviceIds.contains(it.deviceInfo.deviceId) } + setState { + copy(unknownSessions = Success(updated)) + } + } + } + } + } + } + + override fun onCleared() { + vectorPreferences.storeUnknownDeviceDismissedList(ignoredDeviceList) + super.onCleared() + } + + companion object : MvRxViewModelFactory { + + override fun create(viewModelContext: ViewModelContext, state: UnknownDevicesState): UnknownDeviceDetectorSharedViewModel? { + val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession() + return UnknownDeviceDetectorSharedViewModel(session, VectorPreferences(viewModelContext.activity()), state) + } + } +} 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 deleted file mode 100644 index 6d10c53493..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/home/UnknwonDeviceDetectorSharedViewModel.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (c) 2020 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.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 java.util.concurrent.TimeUnit - -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) - .debounce(600, TimeUnit.MILLISECONDS) - .distinct() - .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/composer/TextComposerView.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt index 4391009b08..9e2a6c0f05 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,7 +26,6 @@ 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 @@ -172,7 +171,7 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib RoomEncryptionTrustLevel.Warning -> R.drawable.ic_shield_warning else -> R.drawable.ic_shield_black } - composerShieldImageView.setImageDrawable(ContextCompat.getDrawable(context, shieldRes)) + composerShieldImageView.setImageResource(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/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt index d2cdf37eab..e77d9ec73f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt @@ -29,6 +29,7 @@ import im.vector.riotx.core.epoxy.dividerItem import im.vector.riotx.core.resources.StringProvider 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.item.E2EDecoration import im.vector.riotx.features.home.room.detail.timeline.tools.createLinkMovementMethod import im.vector.riotx.features.home.room.detail.timeline.tools.linkify import javax.inject.Inject @@ -72,6 +73,29 @@ class MessageActionsEpoxyController @Inject constructor( } } + when (state.informationData.e2eDecoration) { + E2EDecoration.WARN_IN_CLEAR -> { + bottomSheetSendStateItem { + id("e2e_clear") + showProgress(false) + text(stringProvider.getString(R.string.unencrypted)) + drawableStart(R.drawable.ic_shield_warning_small) + } + } + E2EDecoration.WARN_SENT_BY_UNVERIFIED, + E2EDecoration.WARN_SENT_BY_UNKNOWN -> { + bottomSheetSendStateItem { + id("e2e_unverified") + showProgress(false) + text(stringProvider.getString(R.string.encrypted_unverified)) + drawableStart(R.drawable.ic_shield_warning_small) + } + } + else -> { + // nothing + } + } + // Quick reactions if (state.canReact() && state.quickStates is Success) { // Separator 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 0758e34495..6b44b9f3d3 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 @@ -18,19 +18,23 @@ package im.vector.riotx.features.home.room.detail.timeline.helper +import im.vector.matrix.android.api.extensions.orFalse 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.Room 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.crypto.model.event.EncryptedEventContent import im.vector.matrix.android.internal.session.room.VerificationState import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.utils.getColorFromUserId +import im.vector.riotx.features.home.room.detail.timeline.item.E2EDecoration import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.PollResponseData import im.vector.riotx.features.home.room.detail.timeline.item.ReactionInfoData @@ -72,6 +76,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId)) } + val room = event.root.roomId?.let { session.getRoom(it) } + val e2eDecoration = getE2EDecoration(room, event) return MessageInformationData( eventId = eventId, senderId = event.root.senderId ?: "", @@ -111,10 +117,59 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses ?: VerificationState.REQUEST ReferencesInfoData(verificationState) }, - sentByMe = event.root.senderId == session.myUserId + sentByMe = event.root.senderId == session.myUserId, + e2eDecoration = e2eDecoration ) } + private fun getE2EDecoration(room: Room?, event: TimelineEvent): E2EDecoration { + return if (room?.isEncrypted() == true + // is user verified + && session.cryptoService().crossSigningService().getUserCrossSigningKeys(event.root.senderId ?: "")?.isTrusted() == true) { + val ts = room.roomSummary()?.encryptionEventTs ?: 0 + val eventTs = event.root.originServerTs ?: 0 + if (event.isEncrypted()) { + // Do not decorate failed to decrypt, or redaction (we lost sender device info) + if (event.root.getClearType() == EventType.ENCRYPTED || event.root.isRedacted()) { + E2EDecoration.NONE + } else { + val sendingDevice = event.root.content + .toModel() + ?.deviceId + ?.let { deviceId -> + session.cryptoService().getDeviceInfo(event.root.senderId ?: "", deviceId) + } + when { + sendingDevice == null -> { + // For now do not decorate this with warning + // maybe it's a deleted session + E2EDecoration.NONE + } + sendingDevice.trustLevel == null -> { + E2EDecoration.WARN_SENT_BY_UNKNOWN + } + sendingDevice.trustLevel?.isVerified().orFalse() -> { + E2EDecoration.NONE + } + else -> { + E2EDecoration.WARN_SENT_BY_UNVERIFIED + } + } + } + } else { + if (EventType.isStateEvent(event.root.type)) { + // Do not warn for state event, they are always in clear + E2EDecoration.NONE + } else { + // If event is in clear after the room enabled encryption we should warn + if (eventTs > ts) E2EDecoration.WARN_IN_CLEAR else E2EDecoration.NONE + } + } + } else { + E2EDecoration.NONE + } + } + /** * Tiles type message never show the sender information (like verification request), so we should repeat it for next message * even if same sender diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt index 149b5e74ad..e62de05518 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt @@ -92,6 +92,18 @@ abstract class AbsBaseMessageItem : BaseEventItem holder.reactionsContainer.setOnLongClickListener(baseAttributes.itemLongClickListener) } + when (baseAttributes.informationData.e2eDecoration) { + E2EDecoration.NONE -> { + holder.e2EDecorationView.isVisible = false + } + E2EDecoration.WARN_IN_CLEAR, + E2EDecoration.WARN_SENT_BY_UNVERIFIED, + E2EDecoration.WARN_SENT_BY_UNKNOWN -> { + holder.e2EDecorationView.setImageResource(R.drawable.ic_shield_warning) + holder.e2EDecorationView.isVisible = true + } + } + holder.view.setOnClickListener(baseAttributes.itemClickListener) holder.view.setOnLongClickListener(baseAttributes.itemLongClickListener) } @@ -110,6 +122,7 @@ abstract class AbsBaseMessageItem : BaseEventItem abstract class Holder(@IdRes stubId: Int) : BaseEventItem.BaseHolder(stubId) { val reactionsContainer by bind(R.id.reactionsContainer) + val e2EDecorationView by bind(R.id.messageE2EDecoration) } /** diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt index 8d4ae81201..088577d03a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt @@ -40,7 +40,8 @@ data class MessageInformationData( val hasPendingEdits: Boolean = false, val readReceipts: List = emptyList(), val referencesInfoData: ReferencesInfoData? = null, - val sentByMe : Boolean + val sentByMe : Boolean, + val e2eDecoration: E2EDecoration = E2EDecoration.NONE ) : Parcelable { val matrixItem: MatrixItem @@ -75,4 +76,11 @@ data class PollResponseData( val isClosed: Boolean = false ) : Parcelable +enum class E2EDecoration { + NONE, + WARN_IN_CLEAR, + WARN_SENT_BY_UNVERIFIED, + WARN_SENT_BY_UNKNOWN +} + fun ReadReceiptData.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt index ec98ea10ed..a4d5d273e7 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt @@ -19,6 +19,7 @@ package im.vector.riotx.features.home.room.detail.timeline.item import android.view.View import android.widget.ImageView import android.widget.TextView +import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R @@ -45,6 +46,18 @@ abstract class NoticeItem : BaseEventItem() { holder.view.setOnLongClickListener(attributes.itemLongClickListener) holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener) holder.avatarImageView.onClick(attributes.avatarClickListener) + + when (attributes.informationData.e2eDecoration) { + E2EDecoration.NONE -> { + holder.e2EDecorationView.isVisible = false + } + E2EDecoration.WARN_IN_CLEAR, + E2EDecoration.WARN_SENT_BY_UNVERIFIED, + E2EDecoration.WARN_SENT_BY_UNKNOWN -> { + holder.e2EDecorationView.setImageResource(R.drawable.ic_shield_warning) + holder.e2EDecorationView.isVisible = true + } + } } override fun getEventIds(): List { @@ -56,6 +69,7 @@ abstract class NoticeItem : BaseEventItem() { class Holder : BaseHolder(STUB_ID) { val avatarImageView by bind(R.id.itemNoticeAvatarView) val noticeTextView by bind(R.id.itemNoticeTextView) + val e2EDecorationView by bind(R.id.messageE2EDecoration) } data class Attributes( diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/tools/EventRenderingTools.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/tools/EventRenderingTools.kt index 097a95bb27..d3d7260206 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/tools/EventRenderingTools.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/tools/EventRenderingTools.kt @@ -31,6 +31,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import im.vector.riotx.core.utils.isValidUrl fun CharSequence.findPillsAndProcess(scope: CoroutineScope, processBlock: (PillImageSpan) -> Unit) { scope.launch(Dispatchers.Main) { @@ -59,14 +60,16 @@ fun CharSequence.linkify(callback: TimelineEventController.UrlClickCallback?): C fun createLinkMovementMethod(urlClickCallback: TimelineEventController.UrlClickCallback?): EvenBetterLinkMovementMethod { return EvenBetterLinkMovementMethod(object : EvenBetterLinkMovementMethod.OnLinkClickListener { override fun onLinkClicked(textView: TextView, span: ClickableSpan, url: String, actualText: String): Boolean { - return urlClickCallback?.onUrlClicked(url, actualText) == true + // Always return false if the url is not valid, so the EvenBetterLinkMovementMethod can fallback to default click listener. + return url.isValidUrl() && urlClickCallback?.onUrlClicked(url, actualText) == true } }) .apply { // We need also to fix the case when long click on link will trigger long click on cell setOnLinkLongClickListener { tv, url -> // Long clicks are handled by parent, return true to block android to do something with url - if (urlClickCallback?.onUrlLongClicked(url) == true) { + // Always return false if the url is not valid, so the EvenBetterLinkMovementMethod can fallback to default click listener. + if (url.isValidUrl() && urlClickCallback?.onUrlLongClicked(url) == true) { tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0)) true } else { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt index 08d92760b2..f9b0b98f29 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt @@ -31,9 +31,14 @@ import android.webkit.WebView import android.webkit.WebViewClient import androidx.appcompat.app.AlertDialog import com.airbnb.mvrx.activityViewModel +import im.vector.matrix.android.api.auth.LOGIN_FALLBACK_PATH +import im.vector.matrix.android.api.auth.REGISTER_FALLBACK_PATH +import im.vector.matrix.android.api.auth.SSO_FALLBACK_PATH +import im.vector.matrix.android.api.auth.SSO_REDIRECT_URL_PARAM import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.riotx.R +import im.vector.riotx.core.extensions.appendParamToUrl import im.vector.riotx.core.utils.AssetReader import im.vector.riotx.features.signout.soft.SoftLogoutAction import im.vector.riotx.features.signout.soft.SoftLogoutViewModel @@ -123,14 +128,24 @@ class LoginWebFragment @Inject constructor( val url = buildString { append(state.homeServerUrl?.trim { it == '/' }) if (state.signMode == SignMode.SignIn) { - append("/_matrix/static/client/login/") + if (state.loginMode == LoginMode.Sso) { + append(SSO_FALLBACK_PATH) + // We do not want to deal with the result, so let the fallback login page to handle it for us + appendParamToUrl(SSO_REDIRECT_URL_PARAM, + buildString { + append(state.homeServerUrl?.trim { it == '/' }) + append(LOGIN_FALLBACK_PATH) + }) + } else { + append(LOGIN_FALLBACK_PATH) + } state.deviceId?.takeIf { it.isNotBlank() }?.let { // But https://github.com/matrix-org/synapse/issues/5755 - append("?device_id=$it") + appendParamToUrl("device_id", it) } } else { // MODE_REGISTER - append("/_matrix/static/client/register/") + append(REGISTER_FALLBACK_PATH) } } 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 cb2adfc6b3..ac725eb850 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 @@ -97,7 +97,7 @@ class DefaultNavigator @Inject constructor( roomId = null, otherUserId = session.myUserId, transactionId = pr.transactionId - ).show(context.supportFragmentManager, "REQPOP") + ).show(context.supportFragmentManager, VerificationBottomSheet.WAITING_SELF_VERIF_TAG) } } diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt index 1d7338e2a4..4acb8ed79e 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt @@ -32,6 +32,23 @@ class EmojiDataSource @Inject constructor( .adapter(EmojiData::class.java) .fromJson(input.bufferedReader().use { it.readText() }) } + ?.let { parsedRawData -> + // Add key as a keyword, it will solve the issue that ":tada" is not available in completion + parsedRawData.copy( + emojis = mutableMapOf().apply { + parsedRawData.emojis.keys.forEach { key -> + val origin = parsedRawData.emojis[key] ?: return@forEach + + // Do not add keys containing '_' + if (origin.keywords.contains(key) || key.contains("_")) { + put(key, origin) + } else { + put(key, origin.copy(keywords = origin.keywords + key)) + } + } + } + ) + } ?: EmojiData(emptyList(), emptyMap(), emptyMap()) private val quickReactions = mutableListOf() diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt index 907c019f39..a9af166c1d 100644 --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt @@ -101,7 +101,7 @@ class DeviceTrustInfoEpoxyController @Inject constructor(private val stringProvi bottomSheetVerificationActionItem { id("verify") - title(stringProvider.getString(R.string.verification_verify_device_manually)) + title(stringProvider.getString(R.string.cross_signing_verify_by_emoji)) titleColor(colorProvider.getColor(R.color.riotx_accent)) iconRes(R.drawable.ic_arrow_right) iconColor(colorProvider.getColor(R.color.riotx_accent)) diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt index e765f961dd..c995c4d986 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt @@ -24,6 +24,7 @@ import android.provider.MediaStore import androidx.core.content.edit import androidx.preference.PreferenceManager import com.squareup.seismic.ShakeDetector +import im.vector.matrix.android.api.extensions.tryThis import im.vector.riotx.BuildConfig import im.vector.riotx.R import im.vector.riotx.features.homeserver.ServerUrlsRepository @@ -166,6 +167,8 @@ class VectorPreferences @Inject constructor(private val context: Context) { private const val MEDIA_SAVING_1_MONTH = 2 private const val MEDIA_SAVING_FOREVER = 3 + private const val SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST = "SETTINGS_UNKNWON_DEVICE_DISMISSED_LIST" + // some preferences keys must be kept after a logout private val mKeysToKeepAfterLogout = listOf( SETTINGS_DEFAULT_MEDIA_COMPRESSION_KEY, @@ -201,6 +204,11 @@ class VectorPreferences @Inject constructor(private val context: Context) { SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY, SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY, + SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY, + SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY, + SETTINGS_LABS_ALLOW_EXTENDED_LOGS, + SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY, + SETTINGS_USE_RAGE_SHAKE_KEY, SETTINGS_SECURITY_USE_FLAG_SECURE ) @@ -364,6 +372,18 @@ class VectorPreferences @Inject constructor(private val context: Context) { return defaultPrefs.getBoolean(SETTINGS_PLAY_SHUTTER_SOUND_KEY, true) } + fun storeUnknownDeviceDismissedList(deviceIds: List) { + defaultPrefs.edit(true) { + putStringSet(SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST, deviceIds.toSet()) + } + } + + fun getUnknownDeviceDismissedList(): List { + return tryThis { + defaultPrefs.getStringSet(SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST, null)?.toList() + } ?: emptyList() + } + /** * Update the notification ringtone * diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt index 6d00f02c97..0c73c0f5d3 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt @@ -26,6 +26,7 @@ import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.replaceFragment import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment import kotlinx.android.synthetic.main.activity_vector_settings.* import timber.log.Timber import javax.inject.Inject @@ -58,11 +59,16 @@ class VectorSettingsActivity : VectorBaseActivity(), if (isFirstCreation()) { // display the fragment when (intent.getIntExtra(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOT)) { - EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS -> + EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS -> replaceFragment(R.id.vector_settings_page, VectorSettingsAdvancedSettingsFragment::class.java, null, FRAGMENT_TAG) - EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY -> + EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY -> replaceFragment(R.id.vector_settings_page, VectorSettingsSecurityPrivacyFragment::class.java, null, FRAGMENT_TAG) - else -> + EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS -> + replaceFragment(R.id.vector_settings_page, + VectorSettingsDevicesFragment::class.java, + null, + FRAGMENT_TAG) + else -> replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG) } } @@ -130,6 +136,7 @@ class VectorSettingsActivity : VectorBaseActivity(), const val EXTRA_DIRECT_ACCESS_ROOT = 0 const val EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS = 1 const val EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY = 2 + const val EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS = 3 private const val FRAGMENT_TAG = "VectorSettingsPreferencesFragment" } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt index bb83658ae7..394587ea5d 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt @@ -412,7 +412,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( refreshCryptographyPreference(it) } // TODO Move to a ViewModel... - session.cryptoService().getDevicesList(object : MatrixCallback { + session.cryptoService().fetchDevicesList(object : MatrixCallback { override fun onSuccess(data: DevicesListResponse) { if (isAdded) { refreshCryptographyPreference(data.devices ?: emptyList()) diff --git a/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningEpoxyController.kt index dd050c29ac..5b7875d0ce 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningEpoxyController.kt @@ -37,7 +37,6 @@ class CrossSigningEpoxyController @Inject constructor( interface InteractionListener { fun onInitializeCrossSigningKeys() - fun onResetCrossSigningKeys() fun verifySession() } @@ -51,18 +50,6 @@ class CrossSigningEpoxyController @Inject constructor( titleIconResourceId(R.drawable.ic_shield_trusted) title(stringProvider.getString(R.string.encryption_information_dg_xsigning_complete)) } - if (vectorPreferences.developerMode() && !data.isUploadingKeys) { - bottomSheetVerificationActionItem { - id("resetkeys") - title("Reset keys") - titleColor(colorProvider.getColor(R.color.riotx_destructive_accent)) - iconRes(R.drawable.ic_arrow_right) - iconColor(colorProvider.getColor(R.color.riotx_destructive_accent)) - listener { - interactionListener?.onResetCrossSigningKeys() - } - } - } } else if (data.xSigningKeysAreTrusted) { genericItem { id("trusted") @@ -70,22 +57,9 @@ class CrossSigningEpoxyController @Inject constructor( title(stringProvider.getString(R.string.encryption_information_dg_xsigning_trusted)) } if (!data.isUploadingKeys) { - if (vectorPreferences.developerMode()) { - bottomSheetVerificationActionItem { - id("resetkeys") - title("Reset keys") - titleColor(colorProvider.getColor(R.color.riotx_destructive_accent)) - iconRes(R.drawable.ic_arrow_right) - iconColor(colorProvider.getColor(R.color.riotx_destructive_accent)) - listener { - interactionListener?.onResetCrossSigningKeys() - } - } - } - bottomSheetVerificationActionItem { id("verify") - title(stringProvider.getString(R.string.complete_security)) + title(stringProvider.getString(R.string.crosssigning_verify_this_session)) titleColor(colorProvider.getColor(R.color.riotx_positive_accent)) iconRes(R.drawable.ic_arrow_right) iconColor(colorProvider.getColor(R.color.riotx_positive_accent)) @@ -102,7 +76,7 @@ class CrossSigningEpoxyController @Inject constructor( } bottomSheetVerificationActionItem { id("verify") - title(stringProvider.getString(R.string.complete_security)) + title(stringProvider.getString(R.string.crosssigning_verify_this_session)) titleColor(colorProvider.getColor(R.color.riotx_positive_accent)) iconRes(R.drawable.ic_arrow_right) iconColor(colorProvider.getColor(R.color.riotx_positive_accent)) @@ -110,18 +84,6 @@ class CrossSigningEpoxyController @Inject constructor( interactionListener?.verifySession() } } - if (vectorPreferences.developerMode()) { - bottomSheetVerificationActionItem { - id("resetkeys") - title("Reset keys") - titleColor(colorProvider.getColor(R.color.riotx_destructive_accent)) - iconRes(R.drawable.ic_arrow_right) - iconColor(colorProvider.getColor(R.color.riotx_destructive_accent)) - listener { - interactionListener?.onResetCrossSigningKeys() - } - } - } } else { genericItem { id("not") @@ -130,7 +92,7 @@ class CrossSigningEpoxyController @Inject constructor( if (vectorPreferences.developerMode() && !data.isUploadingKeys) { bottomSheetVerificationActionItem { id("initKeys") - title("Initialize keys") + title(stringProvider.getString(R.string.initialize_cross_signing)) titleColor(colorProvider.getColor(R.color.riotx_positive_accent)) iconRes(R.drawable.ic_arrow_right) iconColor(colorProvider.getColor(R.color.riotx_positive_accent)) diff --git a/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsFragment.kt index 76835211cb..1f81fd7c7b 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsFragment.kt @@ -101,14 +101,4 @@ class CrossSigningSettingsFragment @Inject constructor( override fun verifySession() { viewModel.handle(CrossSigningAction.VerifySession) } - - override fun onResetCrossSigningKeys() { - AlertDialog.Builder(requireContext()) - .setTitle(R.string.dialog_title_confirmation) - .setMessage(R.string.are_you_sure) - .setPositiveButton(R.string.ok) { _, _ -> - viewModel.handle(CrossSigningAction.InitializeCrossSigning) - } - .show() - } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsViewModel.kt index f18e0b3cc7..24fc1bfdf8 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsViewModel.kt @@ -22,14 +22,12 @@ import com.airbnb.mvrx.ViewModelContext 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.failure.Failure +import im.vector.matrix.android.api.failure.toRegistrationFlowResponse import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo 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.crosssigning.isVerified import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth -import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.rx.rx import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.platform.VectorViewModel @@ -113,35 +111,26 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted privat override fun onFailure(failure: Throwable) { _pendingSession = null - if (failure is Failure.OtherServerError && failure.httpCode == 401) { - try { - MoshiProvider.providesMoshi() - .adapter(RegistrationFlowResponse::class.java) - .fromJson(failure.errorBody) - } catch (e: Exception) { - null - }?.let { flowResponse -> - // Retry with authentication - if (flowResponse.flows?.any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true } == true) { - _pendingSession = flowResponse.session ?: "" - _viewEvents.post(CrossSigningSettingsViewEvents.RequestPassword) - return - } else { - // can't do this from here - _viewEvents.post(CrossSigningSettingsViewEvents.Failure(Throwable("You cannot do that from mobile"))) + val registrationFlowResponse = failure.toRegistrationFlowResponse() + if (registrationFlowResponse != null) { + // Retry with authentication + if (registrationFlowResponse.flows?.any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true } == true) { + _pendingSession = registrationFlowResponse.session ?: "" + _viewEvents.post(CrossSigningSettingsViewEvents.RequestPassword) + } else { + // can't do this from here + _viewEvents.post(CrossSigningSettingsViewEvents.Failure(Throwable("You cannot do that from mobile"))) - setState { - copy(isUploadingKeys = false) - } - return + setState { + copy(isUploadingKeys = false) } } - } + } else { + _viewEvents.post(CrossSigningSettingsViewEvents.Failure(failure)) - _viewEvents.post(CrossSigningSettingsViewEvents.Failure(failure)) - - setState { - copy(isUploadingKeys = false) + setState { + copy(isUploadingKeys = false) + } } } }) diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt index b792afe666..5802bebf39 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt @@ -20,15 +20,17 @@ import android.graphics.Typeface import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView -import androidx.core.content.ContextCompat -import androidx.core.view.isInvisible import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass +import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel +import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.utils.DimensionConverter +import me.gujun.android.span.span import java.text.DateFormat import java.text.SimpleDateFormat import java.util.Date @@ -53,21 +55,37 @@ abstract class DeviceItem : VectorEpoxyModel() { var detailedMode = false @EpoxyAttribute - var trusted : Boolean? = null + var trusted: DeviceTrustLevel? = null + + @EpoxyAttribute + var e2eCapable: Boolean = true + + @EpoxyAttribute + var legacyMode: Boolean = false + + @EpoxyAttribute + var trustedSession: Boolean = false + + @EpoxyAttribute + var colorProvider: ColorProvider? = null + + @EpoxyAttribute + var dimensionConverter: DimensionConverter? = null override fun bind(holder: Holder) { holder.root.setOnClickListener { itemClickAction?.invoke() } - if (trusted != null) { - holder.trustIcon.setImageDrawable( - ContextCompat.getDrawable( - holder.view.context, - if (trusted!!) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning - ) - ) - holder.trustIcon.isInvisible = false + val shield = TrustUtils.shieldForTrust( + currentDevice, + trustedSession, + legacyMode, + trusted + ) + + if (e2eCapable) { + holder.trustIcon.setImageResource(shield) } else { - holder.trustIcon.isInvisible = true + holder.trustIcon.setImageDrawable(null) } val detailedModeLabels = listOf( @@ -103,7 +121,28 @@ abstract class DeviceItem : VectorEpoxyModel() { it.setTypeface(null, if (currentDevice) Typeface.BOLD else Typeface.NORMAL) } } else { - holder.summaryLabelText.text = deviceInfo.displayName ?: deviceInfo.deviceId ?: "" + holder.summaryLabelText.text = + span { + +(deviceInfo.displayName ?: deviceInfo.deviceId ?: "") + apply { + // Add additional info if current session is not trusted + if (!trustedSession) { + +"\n" + span { + text = "${deviceInfo.deviceId}" + apply { + colorProvider?.getColorFromAttribute(R.attr.riotx_text_secondary)?.let { + textColor = it + } + dimensionConverter?.spToPx(12)?.let { + textSize = it + } + } + } + } + } + } + holder.summaryLabelText.isVisible = true detailedModeLabels.map { it.isVisible = false diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt index 7ee79a279f..ac4c371448 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt @@ -16,17 +16,14 @@ package im.vector.riotx.features.settings.devices 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.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.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo @@ -37,7 +34,10 @@ import im.vector.riotx.core.platform.VectorViewModel data class DeviceVerificationInfoBottomSheetViewState( val cryptoDeviceInfo: Async = Uninitialized, - val deviceInfo: Async = Uninitialized + val deviceInfo: Async = Uninitialized, + val hasAccountCrossSigning: Boolean = false, + val accountCrossSigningIsTrusted: Boolean = false, + val isMine: Boolean = false ) : MvRxState class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: DeviceVerificationInfoBottomSheetViewState, @@ -51,31 +51,43 @@ class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@As } init { + + setState { + copy( + hasAccountCrossSigning = session.cryptoService().crossSigningService().getMyCrossSigningKeys() != null, + accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified() + ) + } + session.rx().liveCrossSigningInfo(session.myUserId) + .execute { + copy( + hasAccountCrossSigning = it.invoke()?.getOrNull() != null, + accountCrossSigningIsTrusted = it.invoke()?.getOrNull()?.isTrusted() == true + ) + } + session.rx().liveUserCryptoDevices(session.myUserId) .map { list -> list.firstOrNull { it.deviceId == deviceId } } .execute { copy( - cryptoDeviceInfo = it + cryptoDeviceInfo = it, + isMine = it.invoke()?.deviceId == session.sessionParams.credentials.deviceId ) } + setState { copy(deviceInfo = Loading()) } - session.cryptoService().getDeviceInfo(deviceId, object : MatrixCallback { - override fun onSuccess(data: DeviceInfo) { - setState { - copy(deviceInfo = Success(data)) - } - } - override fun onFailure(failure: Throwable) { - setState { - copy(deviceInfo = Fail(failure)) + session.rx().liveMyDeviceInfo() + .map { devices -> + devices.firstOrNull { it.deviceId == deviceId } ?: DeviceInfo(deviceId = deviceId) + } + .execute { + copy(deviceInfo = it) } - } - }) } companion object : MvRxViewModelFactory { diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoEpoxyController.kt index 90724166a0..4123e260e2 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoEpoxyController.kt @@ -16,7 +16,9 @@ package im.vector.riotx.features.settings.devices import com.airbnb.epoxy.TypedEpoxyController +import im.vector.matrix.android.api.extensions.orFalse import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.riotx.R import im.vector.riotx.core.epoxy.dividerItem import im.vector.riotx.core.epoxy.loadingItem @@ -26,6 +28,7 @@ import im.vector.riotx.core.ui.list.GenericItem import im.vector.riotx.core.ui.list.genericFooterItem import im.vector.riotx.core.ui.list.genericItem import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem +import timber.log.Timber import javax.inject.Inject class DeviceVerificationInfoEpoxyController @Inject constructor(private val stringProvider: StringProvider, @@ -37,111 +40,251 @@ class DeviceVerificationInfoEpoxyController @Inject constructor(private val stri override fun buildModels(data: DeviceVerificationInfoBottomSheetViewState?) { val cryptoDeviceInfo = data?.cryptoDeviceInfo?.invoke() - if (cryptoDeviceInfo != null) { - if (cryptoDeviceInfo.isVerified) { + when { + cryptoDeviceInfo != null -> { + // It's a E2E capable device + handleE2ECapableDevice(data, cryptoDeviceInfo) + } + data?.deviceInfo?.invoke() != null -> { + // It's a non E2E capable device + handleNonE2EDevice(data) + } + else -> { + loadingItem { + id("loading") + } + } + } + } + + private fun handleE2ECapableDevice(data: DeviceVerificationInfoBottomSheetViewState, cryptoDeviceInfo: CryptoDeviceInfo) { + val shield = TrustUtils.shieldForTrust( + currentDevice = data.isMine, + trustMSK = data.accountCrossSigningIsTrusted, + legacyMode = !data.hasAccountCrossSigning, + deviceTrustLevel = cryptoDeviceInfo.trustLevel + ) + + if (data.hasAccountCrossSigning) { + // Cross Signing is enabled + handleE2EWithCrossSigning(data.isMine, data.accountCrossSigningIsTrusted, cryptoDeviceInfo, shield) + } else { + handleE2EInLegacy(data.isMine, cryptoDeviceInfo, shield) + } + + // COMMON ACTIONS (Rename / signout) + addGenericDeviceManageActions(data, cryptoDeviceInfo.deviceId) + } + + private fun handleE2EWithCrossSigning(isMine: Boolean, currentSessionIsTrusted: Boolean, cryptoDeviceInfo: CryptoDeviceInfo, shield: Int) { + Timber.v("handleE2EWithCrossSigning $isMine, $cryptoDeviceInfo, $shield") + + if (isMine) { + if (currentSessionIsTrusted) { genericItem { id("trust${cryptoDeviceInfo.deviceId}") style(GenericItem.STYLE.BIG_TEXT) - titleIconResourceId(R.drawable.ic_shield_trusted) + titleIconResourceId(shield) title(stringProvider.getString(R.string.encryption_information_verified)) description(stringProvider.getString(R.string.settings_active_sessions_verified_device_desc)) } } else { + // You need tomcomplete security genericItem { id("trust${cryptoDeviceInfo.deviceId}") - titleIconResourceId(R.drawable.ic_shield_warning) style(GenericItem.STYLE.BIG_TEXT) - title(stringProvider.getString(R.string.encryption_information_not_verified)) - description(stringProvider.getString(R.string.settings_active_sessions_unverified_device_desc)) - } - } - - genericItem { - id("info${cryptoDeviceInfo.deviceId}") - title(cryptoDeviceInfo.displayName() ?: "") - description("(${cryptoDeviceInfo.deviceId})") - } - - if (!cryptoDeviceInfo.isVerified) { - dividerItem { - id("d1") - } - bottomSheetVerificationActionItem { - id("verify") - title(stringProvider.getString(R.string.verification_verify_device)) - titleColor(colorProvider.getColor(R.color.riotx_accent)) - iconRes(R.drawable.ic_arrow_right) - iconColor(colorProvider.getColor(R.color.riotx_accent)) - listener { - callback?.onAction(DevicesAction.VerifyMyDevice(cryptoDeviceInfo.deviceId)) - } - } - } - - if (cryptoDeviceInfo.deviceId != session.sessionParams.credentials.deviceId) { - // Add the delete option - dividerItem { - id("d2") - } - bottomSheetVerificationActionItem { - id("delete") - title(stringProvider.getString(R.string.settings_active_sessions_signout_device)) - titleColor(colorProvider.getColor(R.color.riotx_destructive_accent)) - iconRes(R.drawable.ic_arrow_right) - iconColor(colorProvider.getColor(R.color.riotx_destructive_accent)) - listener { - callback?.onAction(DevicesAction.Delete(cryptoDeviceInfo.deviceId)) - } - } - } - - dividerItem { - id("d3") - } - bottomSheetVerificationActionItem { - id("rename") - title(stringProvider.getString(R.string.rename)) - titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) - iconRes(R.drawable.ic_arrow_right) - iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) - listener { - callback?.onAction(DevicesAction.PromptRename(cryptoDeviceInfo.deviceId)) - } - } - } else if (data?.deviceInfo?.invoke() != null) { - val info = data.deviceInfo.invoke() - genericItem { - id("info${info?.deviceId}") - title(info?.displayName ?: "") - description("(${info?.deviceId})") - } - - genericFooterItem { - id("infoCrypto${info?.deviceId}") - text(stringProvider.getString(R.string.settings_failed_to_get_crypto_device_info)) - } - - if (info?.deviceId != session.sessionParams.credentials.deviceId) { - // Add the delete option - dividerItem { - id("d2") - } - bottomSheetVerificationActionItem { - id("delete") - title(stringProvider.getString(R.string.settings_active_sessions_signout_device)) - titleColor(colorProvider.getColor(R.color.riotx_destructive_accent)) - iconRes(R.drawable.ic_arrow_right) - iconColor(colorProvider.getColor(R.color.riotx_destructive_accent)) - listener { - callback?.onAction(DevicesAction.Delete(info?.deviceId ?: "")) - } + titleIconResourceId(shield) + title(stringProvider.getString(R.string.crosssigning_verify_this_session)) + description(stringProvider.getString(R.string.confirm_your_identity)) } } } else { - loadingItem { - id("loading") + if (!currentSessionIsTrusted) { + // we don't know if this session is trusted... + // for now we show nothing? + } else { + // we rely on cross signing status + val trust = cryptoDeviceInfo.trustLevel?.isCrossSigningVerified() == true + if (trust) { + genericItem { + id("trust${cryptoDeviceInfo.deviceId}") + style(GenericItem.STYLE.BIG_TEXT) + titleIconResourceId(shield) + title(stringProvider.getString(R.string.encryption_information_verified)) + description(stringProvider.getString(R.string.settings_active_sessions_verified_device_desc)) + } + } else { + genericItem { + id("trust${cryptoDeviceInfo.deviceId}") + titleIconResourceId(shield) + style(GenericItem.STYLE.BIG_TEXT) + title(stringProvider.getString(R.string.encryption_information_not_verified)) + description(stringProvider.getString(R.string.settings_active_sessions_unverified_device_desc)) + } + } } } + + // DEVICE INFO SECTION + genericItem { + id("info${cryptoDeviceInfo.deviceId}") + title(cryptoDeviceInfo.displayName() ?: "") + description("(${cryptoDeviceInfo.deviceId})") + } + + if (isMine && !currentSessionIsTrusted) { + // Add complete security + dividerItem { + id("completeSecurityDiv") + } + bottomSheetVerificationActionItem { + id("completeSecurity") + title(stringProvider.getString(R.string.crosssigning_verify_this_session)) + titleColor(colorProvider.getColor(R.color.riotx_accent)) + iconRes(R.drawable.ic_arrow_right) + iconColor(colorProvider.getColor(R.color.riotx_accent)) + listener { + callback?.onAction(DevicesAction.CompleteSecurity) + } + } + } else if (!isMine) { + if (currentSessionIsTrusted) { + // we can propose to verify it + val isVerified = cryptoDeviceInfo.trustLevel?.crossSigningVerified.orFalse() + if (!isVerified) { + addVerifyActions(cryptoDeviceInfo) + } + } + } + } + + private fun handleE2EInLegacy(isMine: Boolean, cryptoDeviceInfo: CryptoDeviceInfo, shield: Int) { + // ==== Legacy + + // TRUST INFO SECTION + if (cryptoDeviceInfo.trustLevel?.isLocallyVerified() == true) { + genericItem { + id("trust${cryptoDeviceInfo.deviceId}") + style(GenericItem.STYLE.BIG_TEXT) + titleIconResourceId(shield) + title(stringProvider.getString(R.string.encryption_information_verified)) + description(stringProvider.getString(R.string.settings_active_sessions_verified_device_desc)) + } + } else { + genericItem { + id("trust${cryptoDeviceInfo.deviceId}") + titleIconResourceId(shield) + style(GenericItem.STYLE.BIG_TEXT) + title(stringProvider.getString(R.string.encryption_information_not_verified)) + description(stringProvider.getString(R.string.settings_active_sessions_unverified_device_desc)) + } + } + + // DEVICE INFO SECTION + genericItem { + id("info${cryptoDeviceInfo.deviceId}") + title(cryptoDeviceInfo.displayName() ?: "") + description("(${cryptoDeviceInfo.deviceId})") + } + + // ACTIONS + + if (!isMine) { + // if it's not the current device you can trigger a verification + dividerItem { + id("d1") + } + bottomSheetVerificationActionItem { + id("verify${cryptoDeviceInfo.deviceId}") + title(stringProvider.getString(R.string.verification_verify_device)) + titleColor(colorProvider.getColor(R.color.riotx_accent)) + iconRes(R.drawable.ic_arrow_right) + iconColor(colorProvider.getColor(R.color.riotx_accent)) + listener { + callback?.onAction(DevicesAction.VerifyMyDevice(cryptoDeviceInfo.deviceId)) + } + } + } + } + + private fun addVerifyActions(cryptoDeviceInfo: CryptoDeviceInfo) { + dividerItem { + id("verifyDiv") + } + bottomSheetVerificationActionItem { + id("verify_text") + title(stringProvider.getString(R.string.cross_signing_verify_by_text)) + titleColor(colorProvider.getColor(R.color.riotx_accent)) + iconRes(R.drawable.ic_arrow_right) + iconColor(colorProvider.getColor(R.color.riotx_accent)) + listener { + callback?.onAction(DevicesAction.VerifyMyDeviceManually(cryptoDeviceInfo.deviceId)) + } + } + dividerItem { + id("verifyDiv2") + } + bottomSheetVerificationActionItem { + id("verify_emoji") + title(stringProvider.getString(R.string.cross_signing_verify_by_emoji)) + titleColor(colorProvider.getColor(R.color.riotx_accent)) + iconRes(R.drawable.ic_arrow_right) + iconColor(colorProvider.getColor(R.color.riotx_accent)) + listener { + callback?.onAction(DevicesAction.VerifyMyDevice(cryptoDeviceInfo.deviceId)) + } + } + } + + private fun addGenericDeviceManageActions(data: DeviceVerificationInfoBottomSheetViewState, deviceId: String) { + // Offer delete session if not me + if (!data.isMine) { + // Add the delete option + dividerItem { + id("manageD1") + } + bottomSheetVerificationActionItem { + id("delete") + title(stringProvider.getString(R.string.settings_active_sessions_signout_device)) + titleColor(colorProvider.getColor(R.color.riotx_destructive_accent)) + iconRes(R.drawable.ic_arrow_right) + iconColor(colorProvider.getColor(R.color.riotx_destructive_accent)) + listener { + callback?.onAction(DevicesAction.Delete(deviceId)) + } + } + } + + // Always offer rename + dividerItem { + id("manageD2") + } + bottomSheetVerificationActionItem { + id("rename") + title(stringProvider.getString(R.string.rename)) + titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) + iconRes(R.drawable.ic_arrow_right) + iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) + listener { + callback?.onAction(DevicesAction.PromptRename(deviceId)) + } + } + } + + private fun handleNonE2EDevice(data: DeviceVerificationInfoBottomSheetViewState) { + val info = data.deviceInfo.invoke() ?: return + genericItem { + id("info${info.deviceId}") + title(info.displayName ?: "") + description("(${info.deviceId})") + } + + genericFooterItem { + id("infoCrypto${info.deviceId}") + text(stringProvider.getString(R.string.settings_failed_to_get_crypto_device_info)) + } + + info.deviceId?.let { addGenericDeviceManageActions(data, it) } } interface Callback { diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesAction.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesAction.kt index e4b1b98cc8..854f5ea895 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesAction.kt @@ -16,14 +16,18 @@ package im.vector.riotx.features.settings.devices +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.riotx.core.platform.VectorViewModelAction sealed class DevicesAction : VectorViewModelAction { - object Retry : DevicesAction() + object Refresh : DevicesAction() data class Delete(val deviceId: String) : DevicesAction() data class Password(val password: String) : DevicesAction() data class Rename(val deviceId: String, val newName: String) : DevicesAction() data class PromptRename(val deviceId: String) : DevicesAction() data class VerifyMyDevice(val deviceId: String) : DevicesAction() + data class VerifyMyDeviceManually(val deviceId: String) : DevicesAction() + object CompleteSecurity : DevicesAction() + data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt index 1d275c7da2..1b08f23996 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt @@ -21,20 +21,23 @@ import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized -import im.vector.matrix.android.api.extensions.sortByLastSeen -import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo +import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.riotx.R import im.vector.riotx.core.epoxy.errorWithRetryItem import im.vector.riotx.core.epoxy.loadingItem import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.ui.list.genericItemHeader +import im.vector.riotx.core.utils.DimensionConverter import im.vector.riotx.features.settings.VectorPreferences import javax.inject.Inject class DevicesController @Inject constructor(private val errorFormatter: ErrorFormatter, private val stringProvider: StringProvider, + private val colorProvider: ColorProvider, + private val dimensionConverter: DimensionConverter, private val vectorPreferences: VectorPreferences) : EpoxyController() { var callback: Callback? = null @@ -68,30 +71,51 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor listener { callback?.retry() } } is Success -> - buildDevicesList(devices(), state.cryptoDevices(), state.myDeviceId) + buildDevicesList(devices(), state.myDeviceId, !state.hasAccountCrossSigning, state.accountCrossSigningIsTrusted) } } - private fun buildDevicesList(devices: List, cryptoDevices: List?, myDeviceId: String) { - // Current device - genericItemHeader { - id("current") - text(stringProvider.getString(R.string.devices_current_device)) - } - + private fun buildDevicesList(devices: List, + myDeviceId: String, + legacyMode: Boolean, + currentSessionCrossTrusted: Boolean) { devices - .filter { - it.deviceId == myDeviceId - } - .forEachIndexed { idx, deviceInfo -> + .firstOrNull { + it.deviceInfo.deviceId == myDeviceId + }?.let { fullInfo -> + val deviceInfo = fullInfo.deviceInfo + // Current device + genericItemHeader { + id("current") + text(stringProvider.getString(R.string.devices_current_device)) + } + deviceItem { - id("myDevice$idx") + id("myDevice${deviceInfo.deviceId}") + legacyMode(legacyMode) + trustedSession(currentSessionCrossTrusted) + dimensionConverter(dimensionConverter) + colorProvider(colorProvider) detailedMode(vectorPreferences.developerMode()) deviceInfo(deviceInfo) currentDevice(true) + e2eCapable(true) itemClickAction { callback?.onDeviceClicked(deviceInfo) } - trusted(true) + trusted(DeviceTrustLevel(currentSessionCrossTrusted, true)) } + +// // If cross signing enabled and this session not trusted, add short cut to complete security + // NEED DESIGN +// if (!legacyMode && !currentSessionCrossTrusted) { +// genericButtonItem { +// id("complete_security") +// iconRes(R.drawable.ic_shield_warning) +// text(stringProvider.getString(R.string.complete_security)) +// itemClickAction(DebouncedClickListener(View.OnClickListener { _ -> +// callback?.completeSecurity() +// })) +// } +// } } // Other devices @@ -103,19 +127,23 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor devices .filter { - it.deviceId != myDeviceId + it.deviceInfo.deviceId != myDeviceId } - // sort before display: most recent first - .sortByLastSeen() - .forEachIndexed { idx, deviceInfo -> - val isCurrentDevice = deviceInfo.deviceId == myDeviceId + .forEachIndexed { idx, deviceInfoPair -> + val deviceInfo = deviceInfoPair.deviceInfo + val cryptoInfo = deviceInfoPair.cryptoDeviceInfo deviceItem { id("device$idx") + legacyMode(legacyMode) + trustedSession(currentSessionCrossTrusted) + dimensionConverter(dimensionConverter) + colorProvider(colorProvider) detailedMode(vectorPreferences.developerMode()) deviceInfo(deviceInfo) - currentDevice(isCurrentDevice) + currentDevice(false) itemClickAction { callback?.onDeviceClicked(deviceInfo) } - trusted(cryptoDevices?.firstOrNull { it.deviceId == deviceInfo.deviceId }?.isVerified) + e2eCapable(cryptoInfo != null) + trusted(cryptoInfo?.trustLevel) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewEvents.kt index 075eb2050e..2cbdbe9485 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewEvents.kt @@ -17,6 +17,8 @@ package im.vector.riotx.features.settings.devices +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.riotx.core.platform.VectorViewEvents @@ -35,4 +37,10 @@ sealed class DevicesViewEvents : VectorViewEvents { val userId: String, val transactionId: String? ) : DevicesViewEvents() + + data class SelfVerification( + val session: Session + ) : DevicesViewEvents() + + data class ShowManuallyVerify(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesViewEvents() } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt index 79a5fe84aa..cd5e53b7c3 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt @@ -16,6 +16,7 @@ package im.vector.riotx.features.settings.devices +import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Async import com.airbnb.mvrx.Fail import com.airbnb.mvrx.FragmentViewModelContext @@ -28,33 +29,46 @@ import com.airbnb.mvrx.ViewModelContext 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.NoOpMatrixCallback +import im.vector.matrix.android.api.extensions.tryThis import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.Session +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.internal.auth.data.LoginFlowTypes +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.matrix.android.internal.util.awaitCallback import im.vector.matrix.rx.rx import im.vector.riotx.core.platform.VectorViewModel -import im.vector.riotx.features.crypto.verification.SupportedVerificationMethodsProvider +import io.reactivex.Observable +import io.reactivex.functions.BiFunction +import io.reactivex.subjects.PublishSubject +import kotlinx.coroutines.launch +import java.util.concurrent.TimeUnit data class DevicesViewState( val myDeviceId: String = "", - val devices: Async> = Uninitialized, - val cryptoDevices: Async> = Uninitialized, +// val devices: Async> = Uninitialized, +// val cryptoDevices: Async> = Uninitialized, + val devices: Async> = Uninitialized, // TODO Replace by isLoading boolean - val request: Async = Uninitialized + val request: Async = Uninitialized, + val hasAccountCrossSigning: Boolean = false, + val accountCrossSigningIsTrusted: Boolean = false ) : MvRxState +data class DeviceFullInfo( + val deviceInfo: DeviceInfo, + val cryptoDeviceInfo: CryptoDeviceInfo? +) class DevicesViewModel @AssistedInject constructor( @Assisted initialState: DevicesViewState, - private val session: Session, - private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider) - : VectorViewModel(initialState), VerificationService.Listener { + private val session: Session +) : VectorViewModel(initialState), VerificationService.Listener { @AssistedInject.Factory interface Factory { @@ -74,16 +88,76 @@ class DevicesViewModel @AssistedInject constructor( private var _currentDeviceId: String? = null private var _currentSession: String? = null - init { - refreshDevicesList() - session.cryptoService().verificationService().addListener(this) + private val refreshPublisher: PublishSubject = PublishSubject.create() - session.rx().liveUserCryptoDevices(session.myUserId) - .execute { + init { + + setState { + copy( + hasAccountCrossSigning = session.cryptoService().crossSigningService().getMyCrossSigningKeys() != null, + accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified(), + myDeviceId = session.sessionParams.credentials.deviceId ?: "" + ) + } + + Observable.combineLatest, List, List>( + session.rx().liveUserCryptoDevices(session.myUserId), + session.rx().liveMyDeviceInfo(), + BiFunction { cryptoList, infoList -> + infoList + .sortedByDescending { it.lastSeenTs } + .map { deviceInfo -> + val cryptoDeviceInfo = cryptoList.firstOrNull { it.deviceId == deviceInfo.deviceId } + DeviceFullInfo(deviceInfo, cryptoDeviceInfo) + } + } + ) + .distinct() + .execute { async -> copy( - cryptoDevices = it + devices = async ) } + + session.rx().liveCrossSigningInfo(session.myUserId) + .execute { + copy( + hasAccountCrossSigning = it.invoke()?.getOrNull() != null, + accountCrossSigningIsTrusted = it.invoke()?.getOrNull()?.isTrusted() == true + ) + } + session.cryptoService().verificationService().addListener(this) + +// session.rx().liveMyDeviceInfo() +// .execute { +// copy( +// devices = it +// ) +// } + + session.rx().liveUserCryptoDevices(session.myUserId) + .distinct() + .throttleLast(5_000, TimeUnit.MILLISECONDS) + .subscribe { + // If we have a new crypto device change, we might want to trigger refresh of device info + session.cryptoService().fetchDevicesList(NoOpMatrixCallback()) + }.disposeOnClear() + +// session.rx().liveUserCryptoDevices(session.myUserId) +// .execute { +// copy( +// cryptoDevices = it +// ) +// } + + refreshPublisher.throttleFirst(4_000, TimeUnit.MILLISECONDS) + .subscribe { + session.cryptoService().fetchDevicesList(NoOpMatrixCallback()) + session.cryptoService().downloadKeys(listOf(session.myUserId), true, NoOpMatrixCallback()) + } + .disposeOnClear() + // then force download + queryRefreshDevicesList() } override fun onCleared() { @@ -93,7 +167,7 @@ class DevicesViewModel @AssistedInject constructor( override fun transactionUpdated(tx: VerificationTransaction) { if (tx.state == VerificationTxState.Verified) { - refreshDevicesList() + queryRefreshDevicesList() } } @@ -102,91 +176,66 @@ class DevicesViewModel @AssistedInject constructor( * The devices list is the list of the devices where the user is logged in. * It can be any mobile devices, and any browsers. */ - private fun refreshDevicesList() { - if (!session.sessionParams.credentials.deviceId.isNullOrEmpty()) { - // display something asap - val localKnown = session.cryptoService().getUserDevices(session.myUserId).map { - DeviceInfo( - user_id = session.myUserId, - deviceId = it.deviceId, - displayName = it.displayName() - ) - } - - setState { - copy( - // Keep known list if we have it, and let refresh go in backgroung - devices = this.devices.takeIf { it is Success } ?: Success(localKnown) - ) - } - - session.cryptoService().getDevicesList(object : MatrixCallback { - override fun onSuccess(data: DevicesListResponse) { - setState { - copy( - myDeviceId = session.sessionParams.credentials.deviceId ?: "", - devices = Success(data.devices.orEmpty()) - ) - } - } - - override fun onFailure(failure: Throwable) { - setState { - copy( - devices = Fail(failure) - ) - } - } - }) - - // Put cached state - setState { - copy( - myDeviceId = session.sessionParams.credentials.deviceId ?: "", - cryptoDevices = Success(session.cryptoService().getUserDevices(session.myUserId)) - ) - } - - // then force download - session.cryptoService().downloadKeys(listOf(session.myUserId), true, object : MatrixCallback> { - override fun onSuccess(data: MXUsersDevicesMap) { - setState { - copy( - cryptoDevices = Success(session.cryptoService().getUserDevices(session.myUserId)) - ) - } - } - }) - } else { - // Should not happen - } + private fun queryRefreshDevicesList() { + refreshPublisher.onNext(Unit) } override fun handle(action: DevicesAction) { return when (action) { - is DevicesAction.Retry -> refreshDevicesList() - is DevicesAction.Delete -> handleDelete(action) - is DevicesAction.Password -> handlePassword(action) - is DevicesAction.Rename -> handleRename(action) - is DevicesAction.PromptRename -> handlePromptRename(action) - is DevicesAction.VerifyMyDevice -> handleVerify(action) + is DevicesAction.Refresh -> queryRefreshDevicesList() + is DevicesAction.Delete -> handleDelete(action) + is DevicesAction.Password -> handlePassword(action) + is DevicesAction.Rename -> handleRename(action) + is DevicesAction.PromptRename -> handlePromptRename(action) + is DevicesAction.VerifyMyDevice -> handleInteractiveVerification(action) + is DevicesAction.CompleteSecurity -> handleCompleteSecurity() + is DevicesAction.MarkAsManuallyVerified -> handleVerifyManually(action) + is DevicesAction.VerifyMyDeviceManually -> handleShowDeviceCryptoInfo(action) } } - private fun handleVerify(action: DevicesAction.VerifyMyDevice) { + private fun handleInteractiveVerification(action: DevicesAction.VerifyMyDevice) { val txID = session.cryptoService() .verificationService() - .requestKeyVerification(supportedVerificationMethodsProvider.provide(), session.myUserId, listOf(action.deviceId)) + .beginKeyVerification(VerificationMethod.SAS, session.myUserId, action.deviceId, null) _viewEvents.post(DevicesViewEvents.ShowVerifyDevice( session.myUserId, - txID.transactionId + txID )) } + private fun handleShowDeviceCryptoInfo(action: DevicesAction.VerifyMyDeviceManually) = withState { state -> + state.devices.invoke() + ?.firstOrNull { it.cryptoDeviceInfo?.deviceId == action.deviceId } + ?.let { + _viewEvents.post(DevicesViewEvents.ShowManuallyVerify(it.cryptoDeviceInfo!!)) + } + } + + private fun handleVerifyManually(action: DevicesAction.MarkAsManuallyVerified) = withState { state -> + viewModelScope.launch { + if (state.hasAccountCrossSigning) { + awaitCallback { + tryThis { session.cryptoService().crossSigningService().trustDevice(action.cryptoDeviceInfo.deviceId, it) } + } + } else { + // legacy + session.cryptoService().setDeviceVerification( + DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), + action.cryptoDeviceInfo.userId, + action.cryptoDeviceInfo.deviceId) + } + } + } + + private fun handleCompleteSecurity() { + _viewEvents.post(DevicesViewEvents.SelfVerification(session)) + } + private fun handlePromptRename(action: DevicesAction.PromptRename) = withState { state -> - val info = state.devices.invoke()?.firstOrNull { it.deviceId == action.deviceId } + val info = state.devices.invoke()?.firstOrNull { it.deviceInfo.deviceId == action.deviceId } if (info != null) { - _viewEvents.post(DevicesViewEvents.PromptRenameDevice(info)) + _viewEvents.post(DevicesViewEvents.PromptRenameDevice(info.deviceInfo)) } } @@ -199,7 +248,7 @@ class DevicesViewModel @AssistedInject constructor( ) } // force settings update - refreshDevicesList() + queryRefreshDevicesList() } override fun onFailure(failure: Throwable) { @@ -270,7 +319,7 @@ class DevicesViewModel @AssistedInject constructor( ) } // force settings update - refreshDevicesList() + queryRefreshDevicesList() } }) } @@ -299,7 +348,7 @@ class DevicesViewModel @AssistedInject constructor( ) } // force settings update - refreshDevicesList() + queryRefreshDevicesList() } override fun onFailure(failure: Throwable) { diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/TrustUtils.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/TrustUtils.kt new file mode 100644 index 0000000000..7f987b327b --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/TrustUtils.kt @@ -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. + */ + +package im.vector.riotx.features.settings.devices + +import androidx.annotation.DrawableRes +import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel +import im.vector.riotx.R + +object TrustUtils { + + @DrawableRes + fun shieldForTrust(currentDevice: Boolean, trustMSK: Boolean, legacyMode: Boolean, deviceTrustLevel: DeviceTrustLevel?): Int { + return when { + currentDevice -> { + if (legacyMode) { + // In legacy, current session is always trusted + R.drawable.ic_shield_trusted + } else { + // If current session doesn't trust MSK, show red shield for current device + R.drawable.ic_shield_trusted.takeIf { trustMSK } ?: R.drawable.ic_shield_warning + } + } + else -> { + if (legacyMode) { + // use local trust + R.drawable.ic_shield_trusted.takeIf { deviceTrustLevel?.locallyVerified == true } ?: R.drawable.ic_shield_warning + } else { + if (trustMSK) { + // use cross sign trust, put locally trusted in black + R.drawable.ic_shield_trusted.takeIf { deviceTrustLevel?.crossSigningVerified == true } + ?: R.drawable.ic_shield_black.takeIf { deviceTrustLevel?.locallyVerified == true } + ?: R.drawable.ic_shield_warning + } else { + // The current session is untrusted, so displays others in black + // as we can't know the cross-signing state + R.drawable.ic_shield_black + } + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt index 603b0b6b78..fa8ee16931 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt @@ -27,6 +27,7 @@ import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.riotx.R +import im.vector.riotx.core.dialogs.ManuallyVerifyDialog import im.vector.riotx.core.dialogs.PromptPasswordDialog import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.configureWith @@ -73,6 +74,15 @@ class VectorSettingsDevicesFragment @Inject constructor( transactionId = it.transactionId ).show(childFragmentManager, "REQPOP") } + is DevicesViewEvents.SelfVerification -> { + VerificationBottomSheet.forSelfVerification(it.session) + .show(childFragmentManager, "REQPOP") + } + is DevicesViewEvents.ShowManuallyVerify -> { + ManuallyVerifyDialog.show(requireActivity(), it.cryptoDeviceInfo) { + viewModel.handle(DevicesAction.MarkAsManuallyVerified(it.cryptoDeviceInfo)) + } + } }.exhaustive } } @@ -92,8 +102,8 @@ class VectorSettingsDevicesFragment @Inject constructor( override fun onResume() { super.onResume() - (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_active_sessions_manage) + viewModel.handle(DevicesAction.Refresh) } override fun onDeviceClicked(deviceInfo: DeviceInfo) { @@ -112,7 +122,7 @@ class VectorSettingsDevicesFragment @Inject constructor( // } override fun retry() { - viewModel.handle(DevicesAction.Retry) + viewModel.handle(DevicesAction.Refresh) } /** diff --git a/vector/src/main/java/im/vector/riotx/features/ui/SharedPreferencesUiStateRepository.kt b/vector/src/main/java/im/vector/riotx/features/ui/SharedPreferencesUiStateRepository.kt index f850d12422..43761ee214 100644 --- a/vector/src/main/java/im/vector/riotx/features/ui/SharedPreferencesUiStateRepository.kt +++ b/vector/src/main/java/im/vector/riotx/features/ui/SharedPreferencesUiStateRepository.kt @@ -26,6 +26,12 @@ import javax.inject.Inject */ class SharedPreferencesUiStateRepository @Inject constructor(private val sharedPreferences: SharedPreferences) : UiStateRepository { + override fun reset() { + sharedPreferences.edit { + remove(KEY_DISPLAY_MODE) + } + } + override fun getDisplayMode(): RoomListDisplayMode { return when (sharedPreferences.getInt(KEY_DISPLAY_MODE, VALUE_DISPLAY_MODE_CATCHUP)) { VALUE_DISPLAY_MODE_PEOPLE -> RoomListDisplayMode.PEOPLE diff --git a/vector/src/main/java/im/vector/riotx/features/ui/UiStateRepository.kt b/vector/src/main/java/im/vector/riotx/features/ui/UiStateRepository.kt index feac6a64ed..c3ecd456e5 100644 --- a/vector/src/main/java/im/vector/riotx/features/ui/UiStateRepository.kt +++ b/vector/src/main/java/im/vector/riotx/features/ui/UiStateRepository.kt @@ -23,6 +23,11 @@ import im.vector.riotx.features.home.RoomListDisplayMode */ interface UiStateRepository { + /** + * Reset all the saved data + */ + fun reset() + fun getDisplayMode(): RoomListDisplayMode fun storeDisplayMode(displayMode: RoomListDisplayMode) diff --git a/vector/src/main/res/drawable/ic_shield_warning_small.xml b/vector/src/main/res/drawable/ic_shield_warning_small.xml new file mode 100644 index 0000000000..d42add32ea --- /dev/null +++ b/vector/src/main/res/drawable/ic_shield_warning_small.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/vector/src/main/res/layout/alerter_verification_layout.xml b/vector/src/main/res/layout/alerter_verification_layout.xml index b06883b056..7098c3152d 100644 --- a/vector/src/main/res/layout/alerter_verification_layout.xml +++ b/vector/src/main/res/layout/alerter_verification_layout.xml @@ -33,7 +33,7 @@ diff --git a/vector/src/main/res/layout/bottom_sheet_bootstrap.xml b/vector/src/main/res/layout/bottom_sheet_bootstrap.xml index 5f2ce368ff..fda590517c 100644 --- a/vector/src/main/res/layout/bottom_sheet_bootstrap.xml +++ b/vector/src/main/res/layout/bottom_sheet_bootstrap.xml @@ -1,5 +1,4 @@ - + android:paddingBottom="16dp"> - + app:layout_constraintTop_toTopOf="parent" + tools:text="@string/enter_account_password" /> - @@ -34,11 +32,11 @@ android:id="@+id/ssss_passphrase_enter_edittext" android:layout_width="match_parent" android:layout_height="wrap_content" - tools:hint="@string/passphrase_enter_passphrase" android:imeOptions="actionDone" android:maxLines="3" android:singleLine="false" android:textColor="?android:textColorPrimary" + tools:hint="@string/passphrase_enter_passphrase" tools:inputType="textPassword" /> @@ -46,9 +44,9 @@ + android:layout_marginBottom="2dp" /> + android:drawableTint="@color/riotx_destructive_accent" + android:gravity="center_vertical" + android:text="@string/bootstrap_dont_reuse_pwd" + android:textSize="12sp" /> @@ -78,6 +76,13 @@ app:layout_constraintStart_toEndOf="@+id/ssss_passphrase_enter_til" app:layout_constraintTop_toTopOf="@+id/ssss_passphrase_enter_til" /> + diff --git a/vector/src/main/res/layout/fragment_bootstrap_migrate_backup.xml b/vector/src/main/res/layout/fragment_bootstrap_migrate_backup.xml index 68417044f9..ac1b70d2c9 100644 --- a/vector/src/main/res/layout/fragment_bootstrap_migrate_backup.xml +++ b/vector/src/main/res/layout/fragment_bootstrap_migrate_backup.xml @@ -79,7 +79,6 @@ + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index 77eaeae05c..3ae80424cc 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -62,6 +62,24 @@ android:layout_height="0dp" tools:layout_marginStart="52dp" /> + + + + + + + + + + Nastavit důležitost oznámení na základě události, nastavení zvuku, LED, vibrací Důležitost oznámení na základě události - Inicializace služby + Spouštím služby Ověřte relaci Odpojit @@ -1634,4 +1634,711 @@ Vaši e-mailovou adresu můžete přidat k profilu v nastavení. RiotX - Klient Matrixu pro příští generaci Rychlejši a lehčí klient Matrixu s nejnovějšími konstrukcemi Androidu + "RiotX je nový klient protokolu Matrix (Matrix.org): otevřená síť pro bezpečnou, decentralizovanou komunikaci. RiotX je úplný přepis klienta Riot Android založený na úplném přepisu Matrix Android SDK. +\n +\nProhlášení: Toto je beta verze. RiotX nyní prochází aktivním vývojem, obsahuje omezení a (doufáme, že ne moc) chyby. Veškerý feedback je vítán. +\n +\nRiotX podporuje: • Přihlášní do existujícího účtu • Založení místnosti a vstup do veřejných místností • Přijetí a odmítnutí pozvánek • Seznam místností uživatelů • Náhled podrobností místnosti • Odesílání textových zpráv • Odesílání příloh • Čtení a psaní zpráv v zašifrovaných místnostech • Krypto: záloha klíčů E2E, pokročilé ověření zařízení, požadavek na sdílení klíče a jeho uspokojení • Push oznámení • Světlý, tmavý a černý motiv +\n +\nJeště nejsou všechny funkce Riotu v RiotX implementovány. Hlavní chybějící funkce (přijdou brzy!): • Nastavení místnosti (seznam členů místnosti, atd.) • Hovory • Widgety • …" + + Přímé zprávy + + Čekám… + Šifruji miniaturu… + Odesílám miniaturu (%1$s / %2$s) + Šifruji soubor… + Odesílám soubor (%1$s / %2$s) + + Stahuji soubor %1$s… + Soubor %1$s byl stažen! + + (upraveno) + + %1$s pro založení účtu. + Použít zastaralou aplikaci + + + Úpravy zpráv + Úpravy nenalezeny + + Vytřídit konverzace… + Nemůžete najít, co hledáte\? + Založit novou místnost + Poslat novou přímou zprávu + Ukázat adresář místností + + Jméno nebo ID (#example:matrix.org) + + Zapnout přetažení pro odpověď v časovém sledu + + Odkaz zkopírován do schránky + + Přidat pomocí matrix ID + Zakládám místnost… + Žádný výsledek nenalezen, použijte Přidat pomocí matrix ID k hledání na serveru. + K obdržení výsledků začněte zadávat + Vytřídit uživatelským jménem nebo ID… + + Vstupuji do místnosti… + + Ukázat historii úprav + + Všeobecné podmínky + Pročíst všeobecné podmínky + Nechte se nalézt druhými + Použijte boty, můstky, widgety a nálepky + + Čtěte na + + + Server pro identity + Odpojit server pro identity + Nastavit server pro identity + Změnit server pro identity + Nyní používáte %1$s, abyste nalezli a byli nalezeni existujícími kontakty, které znáte. + Nyní nepoužíváte server pro identity. Abyste existující známé kontakty nalezli a nechali se jimi nalézt, nastavte nějaký níže. + Emailová adresa k nalezení + Volby pro nalezení se ukážou, jakmile doplníte email. + Volby pro nalezení se ukážou, jakmile doplníte telefonní číslo. + Odpojení od serveru identit bude znamenat, že Vás jiní uživatelé nebudou moci nalézt a Vy nebudete moci pozvat druhé pomocí emailu nebo telefonního čísla. + Telefonní čísla pro nalezení + Poslali jsme Vám potvrzovací email na %s, podívejte se do emailu a klikněte na protvrzovací odkaz + Nevyřízený + + Zadejte nový server pro identity + Nemohl jsem se spojit se serverem pro identity + Prosím, zadejte url serveru pro identity + Server pro identity nemá žádné všeobecné podmínky + Server pro identity, pro který jste se rozhodli, nemá žádné všeobecné podmínky. Pokračujte pouze tehdy, důvěřujete-li vlastníku služby + Textová zpráva byla odeslána na %s. Prosím, zadejte ověřovací kód v ní obsažený. + + Nyní sdílíte emailovou adresu nebo telefonní číslo na serveru identit %1$s. Budete se muset přepojit na %2$s, abyste je dále nesdíleli. + Souhlaste se všeobecnými podmínkami serveru identit (%s), abyste byli k nalezení podle emailové adresy nebo telefonního čísla. + + Zapnout podrobné záznamy. + Podrobné záznamy pomohou vývojářům mnoha podrobnostmi, odešlete-li RageShake. I když jsou zapnuty, aplikace nezaznamenává obsah zpráv nebo jakákoli soukromá data. + + + Prosím, opakujte, jakmile jste přijali všeobecné podmínky svého homeserveru. + + Vypadá to, že serveru dlouho trvá odpovědět, to může být způsobeno buď slabým spojením nebo chybou na serveru. Prosím, opakujte za chvíli. + + Poslat přílohu + + Otevřít navigační zásuvku + Otevřít menu založení místnosti + Zavřít menu založení místnosti… + Založit novou přímou konverzaci + Založit novou místnost + Zavřít titulek zálohy klíčů + Ukázat heslo + Skrýt heslo + Přeskočit až dolů + + %1$s, %2$s a %3$d dalších přečetli + %1$s, %2$s a %3$s přečetli + %1$s a %2$s přečetli + %s přečetl(a) + + 1 uživatel přečetl + %d uživatelé přečetli + %d uživatelů přečetlo + + + Soubor \'%1$s\' (%2$s) je příliš velký k nahrání. Limit je %3$s. + + Během načítání přílohy došlo k chybě. + Soubor + Kontakt + Fotoaparát + Audio + Galerie + Nálepka + Nemohl jsem zpracovat sdílená data + + Je to spam + Je to nepatřičné + Vlastní hlášení… + Nahlásit tento obsah + Důvod k nahlášení tohoto obsahu + HLÁŠENÍ + ZABLOKOVAT UŽIVATELE + + Obsah ohlášen + Tento obsah byl ohlášen. +\n +\nPokud si dále nepřejete vidět více obsahu tohoto uživatele, můžete jej zablokovat a tím skrýt jejich zprávy + Nahlášeno jako spam + Tento obsah byl nahlášen jako spam. +\n +\nPokud si dále nepřejete vidět více obsahu tohoto uživatele, můžete jej zablokovat a tím skrýt jejich zprávy + Nahlášeno jako nepatřičné + Obsah byl nahlášen jako nepatřičný. +\n +\nPokud si dále nepřejete vidět obsah tohoto uživatele, můžete jej zablokovat a tím skrýt jejich zprávy + + Riot potřebuje práva k uložení Vašich E2E klíčů na disk. +\n +\nProsím, povolte přístup v příštím dialogu, abyste mohli exportovat své klíče manuálně. + + Právě nyní není k dispozici žádné síťové spojení + + Zablokovat uživatele + + Všechny zprávy (hlučné) + Všechny zprávy + Pouze zmínky + Utišit + Nastavení + Opustit místnost + %1$s neučinil žádné změny + Odeslat danou zprávu jako spoiler + Spoiler + Pro nalezení reakce zadejte klíčové slovo. + + Neignorujete žádné uživatele + + Více možností po dlouhém stisku na místnost + + + %1$s učinil místnost veřejnou pro kohokoli znalého odkazu. + %1$s nastavil místnost jen pro pozvané. + Nepřečtené zprávy + + Osvoboďte svou komunikaci + Chatujte s lidmi přímo nebo ve skupinách + Udržujte konverzace soukromé pomocí šifrování + Rozšiřte & upravte si svůj zážitek + Můžeme začít + + Vybrat server + Jako email, účty mají jeden domov, ačkoli můžete mluvit s kýmkoli + Přidejte se k miliónům svobodným na největším veřejném serveru + Prémiový hosting pro organizace + Dozvědět se víc + Další + Vlastní & pokročilá nastavení + + Pokračovat + Připojit k %1$s + Připojit k Modular + Upravit připojení k serveru + Přihlásit se na %1$s + Založit účet + Přihlásit se + Pokračovat s SSO + + Adresa Modular + Adresa + Prémiový hosting pro organizace + Zadejte adresu Modular RIot nebo serveru, který chcete použít + Zadejte adresu serveru nebo Riotu, k němuž se chcete připojit + + Při načítání stránky došlo k chybě: %1$s (%2$d) + Aplikace se nemůže přihlásit k tomuto homeserveru. Homeserver podporuje následující typy přihlášení: %1$s. +\n +\nChcete se přihlásit webovým klientem\? + Omlouváme se, tento server již nepřijímá nové účty. + Aplikace nemůže založit účet na tomto homeserveru. +\n +\nChcete se přihlásit webovým klientem\? + + Tato emailová adresa se nevztahuje k žádnému účtu. + + Resetovat heslo na %1$s + Ověřovací email bude odeslán do Vašeho mailboxu za účelem potvrzení nastavení nového heslo. + Dále + Email + Nové heslo + + Varování! + Změna hesla přenastaví všechny end-to-end šifrovací klíče pro všechny Vaše relace, a tak učiní zašifrovanou historii chatů nečitelnou. Nastavte zálohu klíčů nebo exportujte své klíče místností z jiné relace, než se rozhodnete pokračovat. + Pokračovat + + Tato emailová adresa se nevztahuje k žádnému účtu + + Nahlédněte do inboxu + Ověřovací email byl odeslán na %1$s. + Pro potvrzení svého nového hesla klepněte na odkaz. Jakmile otevřete v něm uvedený odkaz, klikněte níže. + Ověřil(a) jsem svou emailovou adresu + + Hotovo! + Vaše heslo bylo přenastaveno. + Byli jste odhlášeni ze všech svých relací a dále již neobdržíte žádná push oznámení. Abyste je opět zapnuli, přihlašte se na každém zařízení. + Zpět na přihlášení + + Varování + Vaše heslo nebylo dosud změněno. +\n +\nZastavit proces změny hesla\? + + Nastavit emailovou adresu + Nastavte emailovou adresu pro obnově svého účtu. Později můžete volitelně dovolit lidem, které znáte, aby Vás podle emailu nalezli. + Email + Email (volitelné) + Dále + + Nastavit telefonní číslo + Nastavte telefonní číslo, abyste volitelně dovolili lidem, které znáte, aby Vás nalezli. + Prosím, použijte mezinárodní formát. + Telefonní číslo + Telefonní číslo (volitelné) + Dále + + Potvrdit telefonní číslo + Právě jsme poslali kód na %1$s. Zadejte jej níže pro ověření, že jste to Vy. + Zadejte kód + Poslat znovu + Dále + + Mezinárodní telefonní čísla musí začít s \'+\' + Telefonní číslo se zdá neplatné. Prosím, zkontrolujte + + Přihlásit se k %1$s + Uživatelské jméno nebo email + Uživatelské jméno + Toto uživatelské jméno je již obsazeno + Varování + Váš účet nebyl ještě založen. +\n +\nZastavit registrační proces\? + + Vybrat matrix.org + Vybrat modular + Vybrat upravený homeserver + Prosím, proveďte vybídnutí captcha + Přijmout všeobecné podmínky a pokračovat + + Prosím, nahlédněte do svého emailu + Právě jsme odeslali email na %1$s. +\nNež budete pokračovat se založením účtu, prosím, klikněte na odkaz v něm obsažený. + Zadaný kód není správný. Prosím, zkontrolujte. + Zastaralý homeserver + Tento homeserver používá za účelem spojení příliš starou verzi. Požádejte správce, aby provedl aktualizaci. + + + Bylo odesláno příliš mnoho požadavků. Můžete opakovat za %1$d vteřinu… + Bylo odesláno příliš mnoho požadavků. Můžete opakovat za %1$d vteřiny… + Bylo odesláno příliš mnoho požadavků. Můžete opakovat za %1$d vteřin… + + + Viděno + + Právě jste se odhlásili + Může to být způsobeno rozmanitými příčinami: +\n +\n• Změnili jste své heslo v jiné relaci. +\n +\n• Smazali jste tuto relaci z jiné relace. +\n +\n• Správce Vašeho serveru zneplatnil Váš přístup z bezpečnostních důvodů. + Znovu se přihlásit + + Právě jste se odhlásili + Přihlásit se + Správce Vašeho homeserveru (%1$s) Vás odhlásil z Vašeho účtu %2$s (%3$s). + Přihlašte se, abyste získali přístup k šifrovacím klíčům uloženým výlučně v tomto zařízení. Potřebujete je ke čtení všech svých zpráv na jakémkoli zařízení. + Přihlásit + Vyčistit osobní údaje + Varování: Vaše osobní údaje (včetně šifrovacích klíčů) jsou dosud uložena v tomto zařízení. +\n +\nVyčistěte je, pokud toto zařízení nebudete dále používat nebo se chcete přihlásit k jinému účtu. + Vyčistit všechna data + + Vyčistit data + Vyčistit všechna data uložená v tomto zařízení\? +\nPro přístup k účtu a zprávám se znovu se přihlaste. + Ztratíte přístup k šifrovaným zprávám, pokud se nepřihlásíte za účelem obnovy šifrovacích klíčů. + Vyčistit data + Tato relace je pro uživatele %1$s a Vy jste zadali údaje pro uživatele %2$s. RiotX toto nepodporuje. +\nProsím, nejdříve vyčistěte data a pak se přihlaste k jinému účtu. + + Váš odkaz matrix.to byl chybný + Popis je příliš krátký + + Prvotní sync… + + Ukázat všechny mé relace + Pokročilá nastavení + Vývojářský režim + Vývojářský režim aktivuje skryté funkce a může tak učinit aplikaci méně stabilní. Jen pro vývojáře! + Rageshake + Práh detekce + Pro test prahu detekce zatřeste svým telefonem + Zatřesení detekováno! + Nastavení + Nynější relace + Jiné relace + + Ukazuji jen první výsledky, zadejte více znaků… + + Fail-fast + RiotX se může zbořit častěji, když se objeví neočekávané chyby + + Požadavek ověření daného uživatelského ID + Předsune ¯\\_(ツ)_/¯ do textové zprávy + + Zapnout šifrování + Jakmile zapnuto, šifrování nelze vypnout. + + Vaše emailová doména není autorizována registrovat se na tomto serveru + + Nedůvěryhodné přihlášení + Shodují se + Neshodují se + Ověřte tohoto uživatele potvrzením, že se následující ojedinělá emoji ukážou na jejich obrazovce ve stejném pořadí. + Pro nejvyšší zabezpečení použijte další důvěryhodný způsob komunikace nebo proveďte osobně. + Abyste se ujistili o důvěryhodnosti uživatele, dívejte se po zeleném štítu. Pro zabezpečení místnosti důvěřujte všem uživatelům v místnosti. + + Nezabezpečené + Něco z následujích je patrně narušeno: +\n +\n- Váš homeserver +\n- Homeserver uživatele, jejž právě ověřujete +\n- Spojení do internetu Vaše či dalších uživatelů +\n- Zařízení Vaše či dalších uživatelů + + Video. + Obrázek. + Audio + Soubor + + Čekám… + %s zrušeno + Zrušili jste + %s přijal + Přijali jste + Ověření odesláno + Požadavek na ověření + + + Ověřit tuto relaci + Ověřit manuálně + + Vy + + Pro bezpečné vzájemné ověření oskenujte kód zařízením druhého uživatele + Skenovat kód + Nelze skenovat + V případě neosobního ověření porovnejte emoji + + Ověřit porovnáním emoji + + Ověřit pomocí emoji + Nemůžete-li skenovat kód nahoře, ověřte porovnáním krátkého, ojedinělého výběru emoji. + + Obrázek QR kódu + + Ověřit %s + Ověřeno %s + Čekám na %s… + Pro vyšší zabezpečení ověřte %s kontrolou jednorázového kódu na obou zařízeních. +\n +\nPro nejvyšší zabezpečení proveďte osobně. + Zprávy v této místnosti nejsou šifrovány end-to-end. + Zprávy v této místnosti jsou šifrovány end-to-end. +\n +\nVaše zprávy jsou zabezpečeny zámky a pouze Vy a příjemce máte jedinečné klíče k jejich odemknutí. + Zabezpečení + Dozvědět se víc + Více + Nastavení místnosti + Oznámení + + Jedna osoba + %1$d osoby + %1$d osob + + Nahrání + Opustit místnost + Opouštím místnost… + + Správci + Moderátoři + Vlastní + Pozvánky + Uživatelé + + Správce v %1$s + Moderátor v %1$s + Vlastní (%1$d) in %2$s + + Přeskočit k potvrzení přečtení + + RiotX neobstarává události typu \'%1$s\' + RiotX neobstarává zprávy typu \'%1$s\' + RiotX narazil na chybu při převádění obsahu události s id \'%1$s\' + + Odignorovat + + Tato relace nemůže sdílet toto ověření s jinými z Vašich relací. +\nToto ověření bude uloženo místně a sdíleno v budoucí verzi aplikace. + + Poslední místnosti + Další místnosti + + Odešle danou zprávu zabarvenou jako duha + Odešle daný emote zabarvený jako duha + + Časová osa + + Editor zpráv + + Zapnout šifrování end-to-end + Jakmile zapnuto, šifrování nelze vypnout. + + Zapnout šifrování\? + Jakmile zapnuto, šifrování místnosti nelze vypnout. Zprávy odeslané v zašifrované místnosti nemohou být čteny serverem, ale pouze účastníky místnosti. Zapnutím šifrování mohou boty a můstky přestat správně pracovat. + Zapnout šifrování + + Za účelem bezpečnosti ověřte %s kontrolou jednorázového kódu. + Za účelem bezpečnosti to proveďte osobně nebo použijte jiný způsob komunikace. + + Porovnejte jedinečná emoji a ujistěte se, že se ukážou ve stejném pořadí. + Porovnejte kód s tím na obrazovce druhého uživatele. + Zprávy s tímto uživatelem jsou zašifrovány end-to-end a nemohou být čteny třetími stranami. + Vaše nová relace je nyní ověřena. Má přístup k Vašim zašifrovaným zprávám a ostatní uživatelé ji uvidi jako důvěryhodnou. + + + Křížový podpis + Křížový podpis je zapnut. +\nPrivátní klíče v zařízení. + Křížový podpis je zapnut +\nKlíče jsou důvěryhodné. +\nPrivátní klíče nejsou známy + Křížový podpis je zapnut. +\nKlíče nejsou důvěryhodné + Křížový podpis není zapnut + + + Aktivní relace + Ukázat všechny relace + Správa relací + Odhlásit se z této relace + + Žádná kryptografická informace není k dispozici + + Tato relace je důvěryhodná pro bezpečnou komunikaci, protože jste ji ověřili: + Ověřte tuto relaci a tím ji označíte za důvěryhodnou & dovolíte jí přístup k zašifrovaným zprávám. Pokud jste se do této relace nepřihlásili, může být Váš účet ohrožen: + + + %d aktivní relace + %d aktivní relace + %d aktivních relací + + + Ověřit tuto relaci + Ostatní uživatelé ji nemusí důvěřovat + Dokončit zabezpečení + + Pro ověření této relace použijte existující relaci, a tím ji udělíte přístup k zašifrovaným zprávám. + + + Ověřit + Ověřeno + Varování + + Načtení relací selhalo + Relace + Důvěryhodné + Nedůvěryhodné + + Tato relace je důvěryhodná pro bezpečnou komunikaci, protože %1$s (%2$s) ji ověřili: + %1$s (%2$s) se příhlásili skrze novou relaci: + Dokud tento uživatel nezačne důvěřovat této relaci, zprávy z ní odeslané a v ní přijaté budou označeny varováním. Volitelně ji můžete manuálně ověřit. + + + Spustit křížové podepsání + Resetovat klíče + + QR kód + + Skoro u konce! Ukazuje %s totožný štít\? + Ano + Ne + + Spojení k serveru bylo ztraceno + + Vývojářské nástroje + Údaje účtu + + %d hlas + %d hlasy + %d hlasů + + + %d hlas - Konečné výsledky + %d hlasy - Konečné výsledky + %d hlasů - Konečné výsledky + + Zvolená možnost + Založí jednoduchou anketu + Použijte metodu obnovy + Pokud se nemůžete dostat do existující relace + + Nové přihlášení + + Nemohu najít přihlašovací data v úložišti + Zadejte heslo pro úložište údajů + Varování: + Měli byste otevřít úložiště údajů z důvěryhodného zařízení + + Odstranit… + Chcete %1$s poslat tuto přílohu\? + + Odeslat obrázek v původní velikosti + Odeslat obrázky v původní velikosti + Odeslat obrázky v původní velikosti + + + Potvrďte odstranění + Jste si jist, že chcete odstranit (smazat) tuto událost\? Pamatujte, že pokud odstraníte jméno místnosti nebo téma, mohlo by to změnu zvrátit. + Udejte důvod + Důvod pro úpravu + + Událost smazána uživatelem, důvod: %1$s + Událost moderována správcem místnosti, důvod: %1$s + + Klíče jsou již aktuální! + + RiotX Android + + Požadavky na klíče + + Odemknout zašifrovanou historii zpráv + + Obnovit + + Neověřené přihlášení. Byli jste to Vy\? + Klepněte pro přehled & ověření + Použijte tuto relaci k ověření relace nové, a tím ji udělíte přístup k zašifrovaným zprávám. + To jsem nebyl(a) já + Váš účet může být ohrožen + + Pokud přerušíte, nebudete moci číst zašifrované zprávy na tomto zařízení a ostatní uživatelé mu nebudou důvěřovat + Pokud přerušíte, nebudete moci číst zašifrované zprávy na svém novém zařízení a ostatní uživatelé mu nebudou důvěřovat + Nebudete moci ověřit %1$s (%2$s), pokud nyní přerušíte. Začněte znovu v jejich uživatelském profilu. + + Jedno z následujících může být ohroženo: +\n +\n- Vaše heslo +\n- Váš homeserver +\n- Toto zařízení nebo to druhé +\n- Spojení do internetu obou zařízení +\n +\nDoporučujeme, abyste okamžitě změnili heslo & klíč obnovy v nastavení. + + Ověřit svá zařízení v nastavení. + Ověření zrušeno + + Heslo obnovy + Klíč zpráv + Heslo účtu + + Nastavte %s + Generovat klíč zpráv + + Potvrďte %s + + Zadejte své %s a pokračujte. + + Zabezpečit & odemknout zašifrované zprávy a důvěru s %s. + Zadejte opět své %s a potvrďte. + Nepoužívejte heslo účtu opakovaně. + + + To může několik vteřin trvat, prosím, buďte trpěliví. + Nastavuji obnovení. + Váš klíč obnovení + Hotovo! + Udržujte v bezpečí + Dokončit + + Použijte tento %1$s jako záchrannou síť v případě, že zapomenete své %2$s. + + Zvěřejňuji založené klíče identity + Generuji zabezpečené klíče z hesla + Určuji výchozí klíč SSSS + Synchronizuji hlavní klíč + Synchronizuji uživatelský klíč + Synchronizuji sebepodpisový klíč + Nastavuji zálohu klíčů + + + Vaše %2$s & %1$s jsou nyní nastaveny. +\n +\nUdržujte je v bezpečí! Budete je potřebovat k odemknutí zašifrovaných zpráv a zabezpečených informací, pokud přijdete o všechny své aktivní relace. + + Vytiskněte a uložte na bezpečném místě + Uložte je na USB nebo zálohový disk + Nahrajte do svého osobního úložiště v cloudu + + To nelze provést z mobilního zařízení + + Nastavení hesla pro obnovení Vám umožní zabezpečit & odemknout zašifrované zprávy a důvěryhodnost. +\n +\nPokud nechcete nastavit heslo pro zprávy, založte klíč pro zprávy. + Nastavení hesla pro obnovení Vám umožní zabezpečit & odemknout zašifrované zprávy a důvěryhodnost. + + + Šifrování zapnuto + Zprávy v této místnosti jsou zašifrovány end-to-end. Poučte se více & ověřte uživatele v jejich profilech. + Šifrování není zapnuto + Šifrování použité v této místnosti není podporováno + + %s založil a nastavil tuto místnost. + + Skoro u konce! Ukazuje druhé zařízení stejný štít\? + Skoro u konce! Čekám na potvrzení… + Čekám na %s… + + Import klíčů selhal + + Konfigurace oznámení + Zprávy obsahující @room + Zašifrované zprávy v chatech one-to-one + Zašifrované zprávy ve skupinových chatech + Když dojde k upgradu místností + Řešit problémy + Nastavit důležitost oznámení podle události + + Odešle zprávu jako prostý text bez interpretace markdown + + Nesprávné uživatelské jméno a/nebo heslo. Zadané heslo začíná nebo končí mezerami, prosím, zkontrolovat. + + Zpráva… + + Upgrade šifrování je k dispozici + Ověřit sebe & ostatní za účelem bezpečí chatů + + Zadejte svůj %s a pokračujte + Použít soubor + + Zadejte %s + Heslo obnovení + To není platný klíč obnovení + Prosím, zadejte klíč obnovení + + Kontroluji klíč zálohy + Kontroluji klíč zálohy (%s) + Generuji klíč curve + Generuji klíč SSSS z hesla + Generuji klíč SSSS z hesla (%s) + Generuji klíč SSSS z klíče obnovení + Ukládám heslo pro zálohu klíče v SSSS + %1$s (%2$s) + + Zadejte své heslo pro zálohu klíče a pokračujte. + použít svůj klíč obnovy zálohy klíčů + Neznáte-li své heslo zálohy klíčů, můžete %s. + Klíč pro obnovu zálohy klíčů + + Zamezte screenshotům aplikace + Zapnutí toho nastavení doplní značku FLAG_SECURE ke všem aktivitám. Pro aktivaci změny restartujte aplikaci. + + Mediální soubor byl připojen do galerie + Mediální soubor nemohl být připojen do galerie + Nastavit nové heslo účtu… + diff --git a/vector/src/main/res/values-eu/strings.xml b/vector/src/main/res/values-eu/strings.xml index 7c42f6a71a..14827878a4 100644 --- a/vector/src/main/res/values-eu/strings.xml +++ b/vector/src/main/res/values-eu/strings.xml @@ -881,7 +881,7 @@ Matrix-eko mezuen ikusgaitasuna e-mail sistemaren antekoa da. Guk zure mezuak ah Deskargatu Hitz egin Garbitu - Berriro eskatu zifratze-gakoak zure beste saioetatik. + Eskatu berriro zifratze-gakoak zure beste saioetatik. Gako eskaria bidalita. @@ -1344,7 +1344,7 @@ Abisua: Fitxategi hau ezabatu daiteke aplikazioa desinstalatzen bada. Ez galdu inoiz zifratutako mezuak Erabili gakoen babes-kopia - Zifratutako mezuen gako berriak + Mezu seguruen gako berriak Kudeatu gakoen babes-kopian Gakoen babes-kopia egiten… @@ -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. + Erabili aurreko saio bat saio hau egiaztatzeko, mezu zifratuetara sarbidea emanez. Egiaztatu @@ -2166,6 +2166,7 @@ Abisua: Fitxategi hau ezabatu daiteke aplikazioa desinstalatzen bada. QR kodea + Ia bukatu duzu! %s-ek ezkutu bera erakusten du\? Bai Ez @@ -2183,8 +2184,8 @@ Abisua: Fitxategi hau ezabatu daiteke aplikazioa desinstalatzen bada. Hautatutako aukera Inkesta sinplea sortzen du - Ezin zara badagoen saio batera sartu\? - Erabili berreskuratze gakoa edo pasa-esaldia + Erabili berreskuratze metodo bat + Ezin baduzu badagoen saio bat erabili Saio berria @@ -2210,4 +2211,143 @@ Abisua: Fitxategi hau ezabatu daiteke aplikazioa desinstalatzen bada. Gakoak egunean daude jada! + RiotX Android + + Gako eskaerak + + Desblokeatu zifratutako mezuen historiala + + Freskatu + + Egiaztatu gabeko saioa. Zu izan zara\? + Sakatu eta berrikusi eta egiaztatu + Erabili saio hau berria egiaztatzeko, honela mezu zifratuetara sarbidea emanez. + Ez naiz ni izan + Zure kontua konprometituta egon daiteke + + Ezeztatzen baduzu, ezin izango dituzu zifratutako mezuak irakurri gailu honetan, eta beste erabiltzaileek ez dute fidagarritzat joko + Ezeztatzen baduzu, ezin izango dituzu zifratutako mezuak irakurri gailu berrian, eta beste erabiltzaileek ez dute fidagarritzat joko + Ez duzu %1$s (%2$s) egiaztatuko orain ezeztatzen baduzu. Hasi berriro bere erabiltzaile profilean. + + Hauetakoren bat konprometituta egon daiteke: +\n +\n- Zure pasahitza +\n- Zure hasiera-zerbitzaria +\n- Gailu hau, edo beste gailua +\n- Gailuren batek darabilen internet konexioa +\n +\nPasahitza eta berreskuratze gajoa ezarpenetan berehala aldatzea aholkatzen dugu. + + Egiaztatu gailuak ezarpenetatik. + Egiaztaketa ezeztatuta + + Berreskuratze pasa-esaldia + Mezu-gakoa + Kontuaren pasahitza + + Ezarri %s bat + Sortu mezu-gakoa + + Berretsi %s + + Sartu zure %s jarraitzeko. + + Babestu eta desblokeatu zifratutako mezuak eta jo fidagarritzat %s erabiliz. + Sartu zure %s berriro hau berresteko. + Ez berrerabili zure kontuaren pasahitza. + + + Honek segundo batzuk behar litzake, itxaron. + Berrekuratzea ezartzen. + Zure berreskuratze-gakoa + Bukatu duzu! + Gorde toki seguruan + Amaitu + + Erabili %1$s hau babes gehigarri gisa, %2$s ahaztekotan. + + Sortutako identitate-gakoak argitaratzen + Gako segurua pasa-esalditik sortzen + Lehenetsitako SSSS gakoa definitzen + Gako nagusia sinkronizatzen + Erabiltzaile-gakoa sinkronizatzen + Norberak sinatzeko gakoa sinkronizatzen + Gakoen babeskopia ezartzen + + + "Zure %2$s eta %1$s ezarrita daude. +\n +\nGorde toki seguruan! Zifratutako mezuak eta informazio segurua desblokeatzeko beharko dituzu zure saio aktibo guztiak galduz gero." + + Inprimatu eta gorde toki seguruan + Gorde USB memorian edo babes-kopien diskoan + Kopiatu zure hodei pertsonalean + + Ezin duzu hori mugikorretik egin + + Berreskuratze pasa-esaldia ezartzeak zifratutako mezuak babestea zein desblokeatzea eta fidagarritasuna ezartzea ahalbidetzen dizu. +\n +\nEz baduzu mezuen pasahitz bat ezarri, sortu mezuen gakoa. + Berreskuratze pasa-esaldia ezartzeak zifratutako mezuak babestea zein desblokeatzea eta fidagarritasuna ezartzea ahalbidetzen dizu. + + + Zifratzea gaituta + Gela honetako mezuak muturretik muturrera zifratuta daude. Ikasi gehiago eta egiaztatu erabiltzaileak bere erabiltzaile-profilean. + Zifratzea gaitu gabe + Gela honetan erabilitako zifratzea ez da onartzen + + %s erabiltzaileak gela sortu eta konfiguratu du. + + Ia amaitu duzu! Zure beste gailuak ezkutu bera erakusten du\? + Ia bukatu duzu! Baieztapenaren zain… + %s itxaroten… + + Ezin izan dira gakoak inportatu + + Jakinarazpenen ezarpena + \@room duten mezuak + Zifratutako mezuak bi pertsonen arteko txatetan + Zifratutako mezuak talde-txatetan + Gelak eguneratzean + Arazo-ehiza + Ezarri jakinarazpenen garrantzia gertaerako + + Mezua test arrunt gisa bidaltzen du, markdown den aztertu gabe + + Erabiltzaile-izen edo pasahitz okerra. Sartutako pasahitzak zuriuneak ditu hasiera edo amaieran, egiztatu. + + Mezua… + + Zifratze eguneratzea eskuragarri + Egiaztatu zure burua eta besteak txatak seguru mantentzeko + + Sartu zure %s jarraitzeko + Erabili fitxategia + + Sartu %s + Berreskuratze pasa-esaldia + Ez da baliozko berreskuratze-gakoa + Sartu berreskuratze-gakoa + + Babes-kopiaren gakoa egiaztatzen + Babes-kopiaren gakoa egiaztatzen (%s) + Curve gakoa jasotzen + SSSS gakoa pasa-esalditik sortzen + SSSS gakoa pasa-esalditik sortzen (%s) + SSSS gakoa berreskuratze pasa-esalditik sortzen + Gakoen babes-kopia sekretua SSSS-n gordetzen + %1$s (%2$s) + + Sartu zure gakoen babes-kopiaren pasa-esaldia jarraitzeko. + erabili zure gakoen babes-kopiaren berrekuratze gakoa + Ez badakizu zure gakoen babes-koparen pasa-esaldia, %s. + Gakoen babes-kopiaren berrekuratze gakoa + + Eragotzi aplikazioaren pantaila-argazkiak + Ezarpen hau gaitzeak FLAG_SECURE gehitzen die jarduera guztiei. Berrabiarazi aplikazioa aldaketa aplikatzeko. + + Multimedia fitxategia galeriara gehituta + Ezin izan da multimedia fitxategia galeriara gehitu + Ezarri kontuaren pasahitz berria… + diff --git a/vector/src/main/res/values-fi/strings.xml b/vector/src/main/res/values-fi/strings.xml index 41d8ba1057..8215a9966f 100644 --- a/vector/src/main/res/values-fi/strings.xml +++ b/vector/src/main/res/values-fi/strings.xml @@ -890,7 +890,7 @@ Haluatko lisätä paketteja? Avainten varmuuskopio ei ole valmis, odotathan hetken… Menetät salatut viestisi, jos kirjaudut ulos nyt Avainten varmuuskopio on meneillään. Jos kirjaudut ulos, menetät pääsyn salattuihin viesteihisi. - Turvallinen avainten varmuuskopio pitäisi olla käytössä kaikilla laitteillasi, jotta et menettäisi pääsyä salattuihin viesteihisi. + Turvallinen avainten varmuuskopio pitäisi olla käytössä kaikissa istunnoissasi, jotta et menettäisi pääsyä salattuihin viesteihisi. En halua salattuja viestejäni Varmuuskopioidaan avaimia… Käytä avainten varmuuskopiointia @@ -1577,7 +1577,7 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös Näytä muokkaushistoria - Vahvista laite + Vahvista istunto Vahvista Avaimen jakopyyntö @@ -2051,4 +2051,5 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös Poistu huoneesta Poistutaan huoneesta… + Käyttäjätunnus diff --git a/vector/src/main/res/values-fr/strings.xml b/vector/src/main/res/values-fr/strings.xml index 81ce77cf15..79429ad261 100644 --- a/vector/src/main/res/values-fr/strings.xml +++ b/vector/src/main/res/values-fr/strings.xml @@ -875,11 +875,11 @@ Voulez-vous en ajouter ? Voir maintenant Désactiver le compte - Votre compte sera inutilisable de façon permanente. Vous ne pourrez plus vous connecter et personne ne pourra se réenregistrer avec le même identifiant d’utilisateur. Le compte quittera tous les salons auxquels il participe et tous ses détails seront supprimés du serveur d’identité. Cette action est irréversible. - -Désactiver votre compte ne nous fait pas oublier les messages que vous avez envoyés par défaut. Si vous souhaitez que nous oubliions vos messages, cochez la case ci-dessous. - -La visibilité des messages dans Matrix est identique à celle des e-mails. Si nous oublions vos messages, cela signifie que les messages que vous avez envoyés ne seront plus partagés avec les nouveaux utilisateurs ou les utilisateurs non enregistrés, mais les utilisateurs enregistrés qui ont déjà accès à ces messages en conserveront leur copie. + \"Votre compte sera inutilisable de façon permanente. Vous ne pourrez plus vous connecter et personne ne pourra se réenregistrer avec le même identifiant d’utilisateur. Le compte quittera tous les salons auxquels il participe et tous ses détails seront supprimés du serveur d’identité. Cette action est irréversible. +\n +\nDésactiver votre compte ne nous fait pas oublier les messages que vous avez envoyés par défaut. Si vous souhaitez que nous oubliions vos messages, cochez la case ci-dessous. +\n +\nLa visibilité des messages dans Matrix est identique à celle des e-mails. Si nous oublions vos messages, cela signifie que les messages que vous avez envoyés ne seront plus partagés avec les nouveaux utilisateurs ou les utilisateurs non enregistrés, mais les utilisateurs enregistrés qui ont déjà accès à ces messages en conserveront leur copie.\" Veuillez oublier tous les messages que j’ai envoyé quand mon compte sera désactivé (Avertissement : les futurs utilisateurs verront une version incomplète des conversations) Pour continuer, veuillez renseigner votre mot de passe : Désactiver le compte @@ -2227,7 +2227,7 @@ Si vous n’avez pas configuré de nouvelle méthode de récupération, un attaq Actualiser - Nouvelle session + Connexion non vérifiée. Était-ce vous \? Appuyer pour examiner et vérifier Utilisez cette session pour vérifiez la nouvelle, ce qui lui permettra d’avoir accès aux messages chiffrés. Ce n’était pas moi @@ -2249,7 +2249,7 @@ Si vous n’avez pas configuré de nouvelle méthode de récupération, un attaq Vérifiez vos appareils depuis les paramètres. Vérification annulée - mot de passe des messages + Phrase de récupération clé des messages Mot de passe du compte @@ -2296,7 +2296,7 @@ Si vous n’avez pas configuré de nouvelle méthode de récupération, un attaq La configuration d’un mot de passe de messages vous permet des sécuriser et déverrouiller les messages chiffrés et les vérifications. \n \nSi vous ne voulez pas définir un mot de passe de messages, générez plutôt une clé de messages. - La configuration d’un mot de passe de messages vous permet des sécuriser et déverrouiller les messages chiffrés et les vérifications. + La configuration d’une Phrase de récupération vous permet des sécuriser et déverrouiller les messages chiffrés et les vérifications. Chiffrement activé @@ -2324,4 +2324,38 @@ Si vous n’avez pas configuré de nouvelle méthode de récupération, un attaq Nom et/ou mot de passe incorrect(s). Le mot de passe saisi commence ou se termine par des espaces, veuillez le vérifier. + Message… + + Mise à niveau du chiffrement disponible + Entrez votre %s pour continuer + Utiliser un fichier + + Entrez %s + Phrase de récupération + Ce n\'est pas une clé de récupération valide + Veuillez entrer une clé de récupération + + Vérification de la clé de sauvegarde + Vérification de la clé de sauvegarde (%s) + Génération de la clé SSSS à partir de la phrase secrète + Génération de la clé SSSS à partir de la phrase secrète (%s) + Génération de la clé SSSS à partir de la clé de récupération + Stockage du secret de sauvegarde dans le SSSS + %1$s (%2$s) + + Vérifiez vos sessions et les autres pour garantir la sûreté de vos discussions + + Récupération de la clé de courbe + Saisissez votre phrase secrète de sauvegarde de clés pour continuer. + utiliser votre clé de récupération de sauvegarde de clés + Vous ne connaissez pas votre phrase secrète de sauvegarde de clés, vous pouvez %s. + Clé de récupération de sauvegarde de clés + + Empêcher les captures d’écran de l’application + L’activation de ce paramètre ajoute FLAG_SECURE à toutes les activités. Redémarrez l’application pour que la modification soit prise en compte. + + Fichier multimédia ajouté à la galerie + Impossible d’ajouter le fichier multimédia à la galerie + Définir un nouveau mot de passe de compte… + diff --git a/vector/src/main/res/values-hu/strings.xml b/vector/src/main/res/values-hu/strings.xml index e83e396883..929d916c92 100644 --- a/vector/src/main/res/values-hu/strings.xml +++ b/vector/src/main/res/values-hu/strings.xml @@ -555,7 +555,7 @@ Vedd figyelembe, hogy az alkalmazás újraindul ami sok időt vehet igénybe."< A küldő munkamenet információi Nyilvános név Nyilvános név - Azon. + Azonosító Munkamenet kulcs Hitelesítés Ed25519 ujjlenyomat @@ -1631,7 +1631,7 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró Nincs Visszavonás Bontás - Azonosítási szerver nincs beállítva. + Nincs beállítva azonosítási kiszolgáló. A hívás sikertelen a hibásan beállított szerver miatt Kérd meg a matrix szervered (%1$s) adminisztrátorát, hogy állítson be egy TURN szervert, hogy a hívások megbízhatóan működjenek. @@ -2222,7 +2222,7 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró Frissítés - Új munkamenet + Ismeretlen bejelentkezés. Ez te vagy\? A megtekintéshez és ellenőrzéshez koppints Az új munkamenet ellenőrzéséhez használd ezt, amivel hozzáférést adsz a titkosított üzenetekhez. Nem én voltam @@ -2244,7 +2244,7 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró Ellenőrizd az eszközödet a Beállításokból. Ellenőrzés megszakítva - Üzenet Jelszó + Visszaállítási jelmondat Üzenet Kulcs Fiók Jelszó @@ -2291,7 +2291,7 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró Az Üzenet Jelszó beállításával biztonságba helyezheted és hozzáférhetsz a titkosított üzeneteidhez valamint a bizalomhoz. \n \nHa nem akarsz Üzenet Jelszót beállítani, hozz létre inkább Üzenet Kulcsot. - Az Üzenet Jelszó beállításával biztonságba helyezheted és hozzáférhetsz a titkosított üzeneteidhez valamint a bizalomhoz. + Az Visszaállítási Jelmondat beállításával biztonságba helyezheted és hozzáférhetsz a titkosított üzeneteidhez valamint a bizalomhoz. Titkosítás bekapcsolva @@ -2319,4 +2319,38 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró Felhasználói név és/vagy jelszó hibás. A beírt jelszó szóközzel kezdődik vagy ér véget, kérlek ellenőrizd. + Üzenet… + + A titkosítás fejlesztése elérhető + Ellenőrizd magad és másokat, hogy a csevegéseid biztonságban legyenek + + A továbblépéshez add meg: %s + Fájl használata + + Bevitel: %s + Visszaállítási Jelmondat + Ez nem egy érvényes visszaállítási kulcs + Kérlek add meg a visszaállítási kulcsot + + Mentés Kulcs ellenőrzése + Mentés Kulcs (%s) ellenőrzése + Curve kulcs megszerzése + SSSS kulcs készítése a jelmondatból + SSSS kulcs készítése a jelmondatból (%s) + SSSS kulcs készítése a visszaállítási kulcsból + Kulcsmentés titok tárolása az SSSS-ben + %1$s (%2$s) + + A továbblépéshez add meg a kulcs mentés jelmondatát. + használd a Kulcs Mentés visszaállítási kulcsot + Ha nem tudod a Kulcs Mentés Jelmondatodat, akkor %s. + Kulcs Mentés visszaállítási kulcs + + Az alkalmazásról képernyőkép készítésének megakadályozása + A beállítással minden \"Activiti\" megkapja a \"FLAG_SECURE\" flaget. Indítsd újra az alkalmazást, hogy a változás életbe léphessen. + + A média fájl a Galériához hozzáadva + A média fájlt nem sikerült hozzáadni a Galériához + Új fiók jelszó beállítása… + diff --git a/vector/src/main/res/values-it/strings.xml b/vector/src/main/res/values-it/strings.xml index ecacb45994..337966551d 100644 --- a/vector/src/main/res/values-it/strings.xml +++ b/vector/src/main/res/values-it/strings.xml @@ -2272,7 +2272,7 @@ Ricarica - Nuova sessione + Accesso non verificato. Eri tu\? Tocca per controllare e verificare Usa questa sessione per verificare quella nuova, dandole l\'accesso ai messaggi cifrati. Non ero io @@ -2294,7 +2294,7 @@ Verifica i tuoi dispositivi dalle impostazioni. Verifica annullata - password dei messaggi + Password di ripristino chiave dei messaggi password dell\'account @@ -2341,7 +2341,7 @@ Impostare una password dei messaggi ti consente di proteggere e sbloccare i messaggi cifrati e di fidarti. \n \nSe non vuoi impostare una password dei messaggi, genera una chiave dei messaggi. - Impostare una password dei messaggi ti consente di proteggere e sbloccare i messaggi cifrati e di fidarti. + Impostare una password di ripristino ti consente di proteggere e sbloccare i messaggi cifrati e di fidarti. Cifratura attiva @@ -2369,4 +2369,38 @@ Nome utente e/o password errati. La password inserita inizia o termina con spazi, controllala. + Messaggio… + + Aggiornamento cifratura disponibile + Verifica te stesso e gli altri per tenere al sicuro le chat + + Inserisci la tua %s per continuare + Usa file + + Inserisci la %s + Password di ripristino + Non è una chiave di ripristino valida + Inserisci una chiave di ripristino + + Controllo della chiave di backup + Controllo della chiave di backup (%s) + Rilevazione chiave di curva ellittica + Generazione chiave SSSS dalla password + Generazione chiave SSSS dalla password (%s) + Generazione chiave SSSS dalla chiave di ripristino + Memorizzazione segreto della chiave in SSSS + %1$s (%2$s) + + Inserisci la password del backup chiavi per continuare. + usare la chiave di ripristino del backup chiavi + Non conosci la password del backup chiavi, puoi %s. + Chiave di ripristino del backup chiavi + + Impedisci la cattura di schermate dell\'app + Attivandolo verrà aggiunto FLAG_SECURE a tutte le Activity. Riavvia l\'applicazione per applicare le modifiche. + + File multimediale aggiunto alla galleria + Impossibile aggiungere il file multimediale alla galleria + Imposta una nuova password dell\'account… + diff --git a/vector/src/main/res/values-ru/strings.xml b/vector/src/main/res/values-ru/strings.xml index cd453c83b1..418af4d98e 100644 --- a/vector/src/main/res/values-ru/strings.xml +++ b/vector/src/main/res/values-ru/strings.xml @@ -128,7 +128,7 @@ Начать голосовой вызов Начать видеовызов - Отправка файлов + Отправить файлы Камера @@ -957,7 +957,7 @@ Скачать Говорить Очистить - Повторно запросить ключи шифрования с других ваших сессий. + Перезапросить ключи шифрования у других ваших сессий. Отправлен запрос ключа. @@ -1007,21 +1007,19 @@ Из-за отсутствия разрешений это действие невозможно. - %d сек. - %d сек. - %d сек. + %d секунда + %d секунды + %d секунд - %dm - %dm - %dm - + %d минута + %d минуты + %d минут - %dh - %dh - %dh - + %d час + %d часа + %d часов %d день @@ -1136,7 +1134,7 @@ Результаты диагностики Запустить тесты Выполняется… (%1$d из %2$d) - В основном всё в порядке. Если вы по-прежнему не получаете уведомления, пожалуйста, отправьте отчет об ошибке, чтобы помочь нам исследовать проблему. + В основном, всё в порядке. Если вы по-прежнему не получаете уведомления, пожалуйста, отправьте отчет об ошибке, чтобы помочь нам разобраться. Один или несколько тестов не пройдены, попробуйте предлагаемые решения. Один или несколько тестов не пройдены, пожалуйста, отправьте отчет об ошибке, чтобы помочь нам исследовать проблему. @@ -1502,18 +1500,18 @@ Для обеспечения максимальной безопасности мы рекомендуем делать это лично или использовать другие надежные средства связи. Начать проверку Входящий запрос о проверке - Проверьте это устройство, чтобы отметить его как доверенное. Доверенные устройства партнеров дают вам дополнительное спокойствие при использовании сквозного шифрования сообщений. - Убедитесь, что данное устройство является доверенным, а также проверьте, что оно доверено вашему партнеру. + Проверьте эту сессию, чтобы отметить её как доверенную. Доверие сессиям партнеров позволяет быть уверенным в надёжности сквозного шифрования сообщений. + Потдверждение этой сессии пометит её доверенной для вас и вашего партнёра. - Проверьте это устройство, убедившись, что следующие смайлики появляются на экране партнера - Проверьте это устройство, убедившись, что следующие цифры появляются на экране партнера + Подтвердите эту сессию, убедившись, что следующие смайлики появляются на экране партнера + Подтвердите эту сессию, убедившись, что следующие цифры появляются на экране партнера Вы получили входящий запрос на подтверждение. Посмотреть запрос В ожидании партнера, чтобы подтвердить … Проверено! - Вы успешно проверили это устройство. + Вы успешно подтвердили эту сессию. Защищенные сообщения от этого пользователя шифруются end-to-end и не могут быть прочитаны третьими лицами. Понял @@ -1527,17 +1525,17 @@ Проверка отменена. \nПричина: %s - Интерактивная проверка устройства + Интерактивное подтверждение сессии Запрос на подтверждение - %s хочет проверить ваше устройство + %s желает подтвердить ваше устройство Пользователь отменил проверку Время проверки истекло - Устройство не знает об этой транзакции - Устройство не может договориться о соглашении по ключе, хэше, MAC или методе SAS + Сессия не знает об этой транзакции + Сессия не может договориться о выработке общего ключе, хэше, MAC или методе SAS Полученный хеш не соответствует SAS не соответствует - Устройство получило нежданное сообщение + Сессия получила неожиданное сообщение Было получено недопустимое сообщение Несоответствие ключей Несоответствие пользователя @@ -1564,7 +1562,7 @@ Добро пожаловать домой! Узнайте больше о непрочитанных сообщениях здесь Беседа - Ваше прямое сообщение будет отображаться здесь + Здесь будут отображаться ваши диалоги Комнаты Ваши комнаты будут отображаться здесь @@ -1628,7 +1626,7 @@ app_id: push_key: app_display_name: - device_name: + session_name: Url: Формат: @@ -1787,7 +1785,7 @@ В настоящее время вы делитесь адресами электронной почты или телефонными номерами на сервере идентификации %1$s. Вам нужно повторно подключиться к %2$s, чтобы прекратить делиться ими. Примите Условия обслуживания сервера идентификации (%s), чтобы разрешить обнаружение по адресу электронной почты или номеру телефона. - Включить подробное журнал. + Включить подробные логи. Отправить вложенное Откройте навигационный ящик @@ -1846,4 +1844,7 @@ Покинуть комнату %1$s сделал комнату доступной для всех, у кого есть ссылка. %1$s сделал комнату доступной только по приглашению. + Подробные логи помогут разработчикам, предоставив больше информации, когда вы отправляете RageShake. Даже когда они разрешены, приложение не логирует ваши сообщения и другие приватные данные. + + diff --git a/vector/src/main/res/values-zh-rTW/strings.xml b/vector/src/main/res/values-zh-rTW/strings.xml index edefeec974..86dc7333e4 100644 --- a/vector/src/main/res/values-zh-rTW/strings.xml +++ b/vector/src/main/res/values-zh-rTW/strings.xml @@ -2169,7 +2169,7 @@ Matrix 中的消息可見度類似于電子郵件。我們忘記您的郵件意 重新整理 - 新工作階段 + 未驗證的登入。是您嗎? 輕觸即可以審閱並驗證 使用此工作階段來驗證新的,讓它可以存取已加密的訊息。 這不是我 @@ -2191,7 +2191,7 @@ Matrix 中的消息可見度類似于電子郵件。我們忘記您的郵件意 透過設定驗證您的裝置。 驗證已取消 - 訊息密碼 + 復原通關密語 訊息金鑰 帳號密碼 @@ -2238,7 +2238,7 @@ Matrix 中的消息可見度類似于電子郵件。我們忘記您的郵件意 設定訊息密碼可讓您保護並解鎖已加密的訊息並信任。 \n \n如果您不想要設定訊息密碼,請生成訊息金鑰來代替。 - 設定訊息密碼可讓您保護並解鎖已加密的訊息並信任。 + 設定復原通關密語可讓您保護並解鎖已加密的訊息並信任。 加密已啟用 @@ -2266,4 +2266,38 @@ Matrix 中的消息可見度類似于電子郵件。我們忘記您的郵件意 不正確的使用者名稱及/或密碼。輸入的密碼以空格開頭或結尾,請檢查。 + 訊息…… + + 提供加密升級 + 驗證您自己與其他人以保證聊天安全 + + 輸入您的 %s 以繼續 + 使用檔案 + + 輸入 %s + 復源通關密語 + 這不是有效的復原金鑰 + 請輸入復原金鑰 + + 正在檢查備份金鑰 + 正在檢查備份金鑰 (%s) + 正在取得曲線金鑰 + 正在從通關密語生成 SSSS 金鑰 + 正在從通關密語生成 SSSS 金鑰 (%s) + 正在從復原金鑰生成 SSSS 金鑰 + 正在 SSSS 中儲存金鑰備份秘密 + %1$s (%2$s) + + 輸入您的金鑰備份通關密語以繼續。 + 使用您的金鑰備份復原金鑰 + 不知道您的金鑰備份通關密語,您可以 %s。 + 金鑰備份復原金鑰 + + 避免對應用程式進行螢幕截圖 + 啟用此設定會新增 FLAG_SECURE 到所有活動。重新啟動應用程式以讓變動生效。 + + 媒體檔案已新增至媒體庫中 + 無法新增媒體檔案到媒體庫中 + 設定新的帳號密碼…… + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 8248e7e857..65f96e5f04 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1024,7 +1024,7 @@ Never send encrypted messages to unverified sessions from this session. %1$d/%2$d key(s) imported with success. - NOT Verified + Not Verified Verified Blacklisted @@ -1038,8 +1038,8 @@ Unblacklist Verify session - To verify that this session can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this session matches the key below: - If it matches, press the verify button below. If it doesn’t, then someone else is intercepting this session and you should probably blacklist it. In the future this verification process will be more sophisticated. + Confirm by comparing the following with the User Settings in your other session: + "If they don't match, the security of your communication may be compromised." I verify that the keys match Riot now supports end-to-end encryption but you need to log in again to enable it.\n\nYou can do it now or later from the application settings. @@ -2110,7 +2110,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming Active Sessions Show All Sessions Manage Sessions - Sign out this session + Sign out of this session No cryptographic information available @@ -2122,7 +2122,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming %d active sessions - Verify this session + Verify this login Other users may not trust it Complete Security @@ -2201,7 +2201,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming Refresh - Unverified login. Was this you? + New login. Was this you? Tap to review & verify Use this session to verify your new one, granting it access to encrypted messages. This wasn’t me @@ -2336,8 +2336,38 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming Could not add media file to the Gallery Set a new account password… + Use the latest Riot on your other devices, Riot Web, Riot Desktop, Riot iOS, RiotX for Android, or another cross-signing capable Matrix client + Riot Web\nRiot Desktop + Riot iOS\nRiot X for Android + or another cross-signing capable Matrix client + Use the latest Riot on your other devices: + Forces the current outbound group session in an encrypted room to be discarded + Only supported in encrypted rooms + + Use your %1$s or use your %2$s to continue. + Use Recovery Key + Select your Recovery Key, or input it manually by typing it or pasting from your clipboard + Backup could not be decrypted with this Recovery Key: please verify that you entered the correct Recovery Key. + Failed to access secure storage + + Unencrypted + Encrypted by an unverified device + Review where you’re logged in + Verify all your sessions to ensure your account & messages are safe + + Manually Verify by Text + Verify login + Interactively Verify by Emoji + Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages. + Mark as Trusted + + Please choose a username. + Please choose a password. Double-check this link The link %1$s is taking you to another site: %2$s.\n\nAre you sure you want to continue? + + "We couldn't create your DM. Please check the users you want to invite and try again." + Add members INVITE Inviting users… diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 56456aaf5d..585102d46a 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -1,30 +1,45 @@ + + - Use the latest Riot on your other devices, Riot Web, Riot Desktop, Riot iOS, RiotX for Android, or another cross-signing capable Matrix client - Riot Web\nRiot Desktop - Riot iOS\nRiot X for Android - or another cross-signing capable Matrix client - Use the latest Riot on your other devices: - Forces the current outbound group session in an encrypted room to be discarded - Only supported in encrypted rooms - - Use your %1$s or use your %2$s to continue. - Use Recovery Key - Select your Recovery Key, or input it manually by typing it or pasting from your clipboard - Backup could not be decrypted with this Recovery Key: please verify that you entered the correct Recovery Key. - Failed to access secure storage + - Please choose a username. - Please choose a password. + @@ -42,5 +57,4 @@ - "We couldn't create your DM. Please check the users you want to invite and try again." diff --git a/vector/src/sharedTest/java/im/vector/riotx/test/shared/TestRules.kt b/vector/src/sharedTest/java/im/vector/riotx/test/shared/TestRules.kt new file mode 100644 index 0000000000..d5790ba497 --- /dev/null +++ b/vector/src/sharedTest/java/im/vector/riotx/test/shared/TestRules.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.test.shared + +import net.lachlanmckee.timberjunit.TimberTestRule + +fun createTimberTestRule(): TimberTestRule { + return TimberTestRule.builder() + .showThread(false) + .showTimestamp(false) + .onlyLogWhenTestFails(false) + .build() +}