Merge branch 'develop' into feature/e2e_timeline_decoration

This commit is contained in:
Valere 2020-04-30 12:01:44 +02:00 committed by GitHub
commit 7b20db64a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
114 changed files with 3459 additions and 1018 deletions

View File

@ -29,6 +29,8 @@ Improvements 🙌:
- Cross-Signing | Hide Use recovery key when 4S is not setup (#1007) - 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 - Cross-Signing | Trust account xSigning keys by entering Recovery Key (select file or copy) #1199
- E2E timeline decoration (#1279) - E2E timeline decoration (#1279)
- Manage Session Settings / Cross Signing update (#1295)
- Cross-Signing | Review sessions toast update old vs new (#1293, #1306)
Bugfix 🐛: Bugfix 🐛:
- Fix summary notification staying after "mark as read" - Fix summary notification staying after "mark as read"
@ -44,6 +46,9 @@ Bugfix 🐛:
- RiotX now uses as many threads as it needs to do work and send messages (#1221) - RiotX now uses as many threads as it needs to do work and send messages (#1221)
- Fix issue with media path (#1227) - Fix issue with media path (#1227)
- Add user to direct chat by user id (#1065) - 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 🗣: Translations 🗣:
- -

View File

@ -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.Optional
import im.vector.matrix.android.api.util.toOptional 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.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 im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Single import io.reactivex.Single
@ -58,6 +60,13 @@ class RxSession(private val session: Session) {
} }
} }
fun liveMyDeviceInfo(): Observable<List<DeviceInfo>> {
return session.cryptoService().getLiveMyDevicesInfo().asObservable()
.startWithCallable {
session.cryptoService().getMyDevicesInfo()
}
}
fun liveSyncState(): Observable<SyncState> { fun liveSyncState(): Observable<SyncState> {
return session.getSyncStateLive().asObservable() return session.getSyncStateLive().asObservable()
} }
@ -123,6 +132,13 @@ class RxSession(private val session: Session) {
} }
} }
fun liveCrossSigningPrivateKeys(): Observable<Optional<PrivateKeysInfo>> {
return session.cryptoService().crossSigningService().getLiveCrossSigningPrivateKeys().asObservable()
.startWithCallable {
session.cryptoService().crossSigningService().getCrossSigningPrivateKeys().toOptional()
}
}
fun liveAccountData(types: Set<String>): Observable<List<UserAccountDataEvent>> { fun liveAccountData(types: Set<String>): Observable<List<UserAccountDataEvent>> {
return session.getLiveAccountDataEvents(types).asObservable() return session.getLiveAccountDataEvents(types).asObservable()
.startWithCallable { .startWithCallable {

View File

@ -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.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStore 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.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 io.realm.RealmConfiguration
import kotlin.random.Random import kotlin.random.Random
@ -31,6 +33,7 @@ internal class CryptoStoreHelper {
.name("test.realm") .name("test.realm")
.modules(RealmCryptoStoreModule()) .modules(RealmCryptoStoreModule())
.build(), .build(),
crossSigningKeysMapper = CrossSigningKeysMapper(MoshiProvider.providesMoshi()),
credentials = createCredential()) credentials = createCredential())
} }

View File

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

View File

@ -16,6 +16,10 @@
package im.vector.matrix.android.api.failure 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 import javax.net.ssl.HttpsURLConnection
fun Throwable.is401() = fun Throwable.is401() =
@ -29,6 +33,7 @@ fun Throwable.isTokenError() =
fun Throwable.shouldBeRetried(): Boolean { fun Throwable.shouldBeRetried(): Boolean {
return this is Failure.NetworkConnection return this is Failure.NetworkConnection
|| this is IOException
|| (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED) || (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED)
} }
@ -37,3 +42,18 @@ fun Throwable.isInvalidPassword(): Boolean {
&& error.code == MatrixError.M_FORBIDDEN && error.code == MatrixError.M_FORBIDDEN
&& error.message == "Invalid password" && 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
}
}

View File

@ -98,7 +98,9 @@ interface CryptoService {
fun removeRoomKeysRequestListener(listener: GossipingRequestListener) fun removeRoomKeysRequestListener(listener: GossipingRequestListener)
fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) fun fetchDevicesList(callback: MatrixCallback<DevicesListResponse>)
fun getMyDevicesInfo() : List<DeviceInfo>
fun getLiveMyDevicesInfo() : LiveData<List<DeviceInfo>>
fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>) fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>)

View File

@ -55,6 +55,8 @@ interface CrossSigningService {
fun getCrossSigningPrivateKeys(): PrivateKeysInfo? fun getCrossSigningPrivateKeys(): PrivateKeysInfo?
fun getLiveCrossSigningPrivateKeys(): LiveData<Optional<PrivateKeysInfo>>
fun canCrossSign(): Boolean fun canCrossSign(): Boolean
fun trustUser(otherUserId: String, fun trustUser(otherUserId: String,

View File

@ -50,7 +50,6 @@ data class RoomSummary constructor(
val inviterId: String? = null, val inviterId: String? = null,
val typingRoomMemberIds: List<String> = emptyList(), val typingRoomMemberIds: List<String> = emptyList(),
val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS, val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS,
// TODO Plug it
val roomEncryptionTrustLevel: RoomEncryptionTrustLevel? = null val roomEncryptionTrustLevel: RoomEncryptionTrustLevel? = null
) { ) {

View File

@ -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.auth.data.Credentials
import im.vector.matrix.android.api.failure.Failure 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.auth.AuthAPI
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
@ -39,25 +39,9 @@ internal class DefaultRegisterTask(
apiCall = authAPI.register(params.registrationParams) apiCall = authAPI.register(params.registrationParams)
} }
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
if (throwable is Failure.OtherServerError && throwable.httpCode == 401) { throw throwable.toRegistrationFlowResponse()
// Parse to get a RegistrationFlowResponse ?.let { Failure.RegistrationFlowError(it) }
val registrationFlowResponse = try { ?: throwable
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
}
} }
} }
} }

View File

@ -112,6 +112,7 @@ internal abstract class CryptoModule {
@SessionScope @SessionScope
fun providesRealmConfiguration(@SessionFilesDirectory directory: File, fun providesRealmConfiguration(@SessionFilesDirectory directory: File,
@UserMd5 userMd5: String, @UserMd5 userMd5: String,
realmCryptoStoreMigration: RealmCryptoStoreMigration,
realmKeysUtils: RealmKeysUtils): RealmConfiguration { realmKeysUtils: RealmKeysUtils): RealmConfiguration {
return RealmConfiguration.Builder() return RealmConfiguration.Builder()
.directory(directory) .directory(directory)
@ -121,7 +122,7 @@ internal abstract class CryptoModule {
.name("crypto_store.realm") .name("crypto_store.realm")
.modules(RealmCryptoStoreModule()) .modules(RealmCryptoStoreModule())
.schemaVersion(RealmCryptoStoreMigration.CRYPTO_STORE_SCHEMA_VERSION) .schemaVersion(RealmCryptoStoreMigration.CRYPTO_STORE_SCHEMA_VERSION)
.migration(RealmCryptoStoreMigration) .migration(realmCryptoStoreMigration)
.build() .build()
} }

View File

@ -251,15 +251,33 @@ internal class DefaultCryptoService @Inject constructor(
return myDeviceInfoHolder.get().myDevice return myDeviceInfoHolder.get().myDevice
} }
override fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) { override fun fetchDevicesList(callback: MatrixCallback<DevicesListResponse>) {
getDevicesTask getDevicesTask
.configureWith { .configureWith {
// this.executionThread = TaskThread.CRYPTO // this.executionThread = TaskThread.CRYPTO
this.callback = callback this.callback = object : MatrixCallback<DevicesListResponse> {
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) .executeBy(taskExecutor)
} }
override fun getLiveMyDevicesInfo(): LiveData<List<DeviceInfo>> {
return cryptoStore.getLiveMyDevicesInfo()
}
override fun getMyDevicesInfo(): List<DeviceInfo> {
return cryptoStore.getMyDevicesInfo()
}
override fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>) { override fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>) {
getDeviceInfoTask getDeviceInfoTask
.configureWith(GetDeviceInfoTask.Params(deviceId)) { .configureWith(GetDeviceInfoTask.Params(deviceId)) {
@ -318,6 +336,8 @@ internal class DefaultCryptoService @Inject constructor(
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
internalStart(isInitialSync) internalStart(isInitialSync)
} }
// Just update
fetchDevicesList(NoOpMatrixCallback())
} }
private suspend fun internalStart(isInitialSync: Boolean) { private suspend fun internalStart(isInitialSync: Boolean) {

View File

@ -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.extensions.orFalse
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo 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.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.task.Task
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -26,17 +27,28 @@ import javax.inject.Inject
internal interface ComputeTrustTask : Task<ComputeTrustTask.Params, RoomEncryptionTrustLevel> { internal interface ComputeTrustTask : Task<ComputeTrustTask.Params, RoomEncryptionTrustLevel> {
data class Params( data class Params(
val userIds: List<String> val activeMemberUserIds: List<String>,
val isDirectRoom: Boolean
) )
} }
internal class DefaultComputeTrustTask @Inject constructor( internal class DefaultComputeTrustTask @Inject constructor(
private val cryptoStore: IMXCryptoStore, private val cryptoStore: IMXCryptoStore,
@UserId private val userId: String,
private val coroutineDispatchers: MatrixCoroutineDispatchers private val coroutineDispatchers: MatrixCoroutineDispatchers
) : ComputeTrustTask { ) : ComputeTrustTask {
override suspend fun execute(params: ComputeTrustTask.Params): RoomEncryptionTrustLevel = withContext(coroutineDispatchers.crypto) { 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 } .filter { userId -> getUserCrossSigningKeys(userId)?.isTrusted() == true }
if (allTrustedUserIds.isEmpty()) { if (allTrustedUserIds.isEmpty()) {
@ -60,7 +72,7 @@ internal class DefaultComputeTrustTask @Inject constructor(
if (hasWarning) { if (hasWarning) {
RoomEncryptionTrustLevel.Warning RoomEncryptionTrustLevel.Warning
} else { } else {
if (params.userIds.size == allTrustedUserIds.size) { if (listToCheck.size == allTrustedUserIds.size) {
// all users are trusted and all devices are verified // all users are trusted and all devices are verified
RoomEncryptionTrustLevel.Trusted RoomEncryptionTrustLevel.Trusted
} else { } else {

View File

@ -470,6 +470,10 @@ internal class DefaultCrossSigningService @Inject constructor(
return cryptoStore.getCrossSigningPrivateKeys() return cryptoStore.getCrossSigningPrivateKeys()
} }
override fun getLiveCrossSigningPrivateKeys(): LiveData<Optional<PrivateKeysInfo>> {
return cryptoStore.getLiveCrossSigningPrivateKeys()
}
override fun canCrossSign(): Boolean { override fun canCrossSign(): Boolean {
return checkSelfTrust().isVerified() && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null return checkSelfTrust().isVerified() && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null
&& cryptoStore.getCrossSigningPrivateKeys()?.user != null && cryptoStore.getCrossSigningPrivateKeys()?.user != null

View File

@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.crypto.crosssigning
data class SessionToCryptoRoomMembersUpdate( data class SessionToCryptoRoomMembersUpdate(
val roomId: String, val roomId: String,
val isDirect: Boolean,
val userIds: List<String> val userIds: List<String>
) )

View File

@ -15,18 +15,20 @@
*/ */
package im.vector.matrix.android.internal.crypto.crosssigning 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.RoomMemberSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields 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.database.query.where
import im.vector.matrix.android.internal.di.SessionDatabase 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.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.task.TaskExecutor
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.createBackgroundHandler import im.vector.matrix.android.internal.util.createBackgroundHandler
import io.realm.Realm import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import timber.log.Timber import timber.log.Timber
@ -38,13 +40,13 @@ internal class ShieldTrustUpdater @Inject constructor(
private val eventBus: EventBus, private val eventBus: EventBus,
private val computeTrustTask: ComputeTrustTask, private val computeTrustTask: ComputeTrustTask,
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
@SessionDatabase private val sessionRealmConfiguration: RealmConfiguration, @SessionDatabase private val sessionRealmConfiguration: RealmConfiguration,
private val roomSummaryUpdater: RoomSummaryUpdater private val roomSummaryUpdater: RoomSummaryUpdater
) { ) {
companion object { companion object {
private val BACKGROUND_HANDLER = createBackgroundHandler("SHIELD_CRYPTO_DB_THREAD") private val BACKGROUND_HANDLER = createBackgroundHandler("SHIELD_CRYPTO_DB_THREAD")
private val BACKGROUND_HANDLER_DISPATCHER = BACKGROUND_HANDLER.asCoroutineDispatcher()
} }
private val backgroundSessionRealm = AtomicReference<Realm>() private val backgroundSessionRealm = AtomicReference<Realm>()
@ -76,58 +78,42 @@ internal class ShieldTrustUpdater @Inject constructor(
if (!isStarted.get()) { if (!isStarted.get()) {
return return
} }
taskExecutor.executorScope.launch(coroutineDispatchers.crypto) { taskExecutor.executorScope.launch(BACKGROUND_HANDLER_DISPATCHER) {
val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(update.userIds)) val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(update.userIds, update.isDirect))
// We need to send that back to session base // We need to send that back to session base
BACKGROUND_HANDLER.post {
backgroundSessionRealm.get()?.executeTransaction { realm -> backgroundSessionRealm.get()?.executeTransaction { realm ->
roomSummaryUpdater.updateShieldTrust(realm, update.roomId, updatedTrust) roomSummaryUpdater.updateShieldTrust(realm, update.roomId, updatedTrust)
} }
} }
} }
}
@Subscribe @Subscribe
fun onTrustUpdate(update: CryptoToSessionUserTrustChange) { fun onTrustUpdate(update: CryptoToSessionUserTrustChange) {
if (!isStarted.get()) { if (!isStarted.get()) {
return return
} }
onCryptoDevicesChange(update.userIds) onCryptoDevicesChange(update.userIds)
} }
private fun onCryptoDevicesChange(users: List<String>) { private fun onCryptoDevicesChange(users: List<String>) {
BACKGROUND_HANDLER.post { taskExecutor.executorScope.launch(BACKGROUND_HANDLER_DISPATCHER) {
val impactedRoomsId = backgroundSessionRealm.get()?.where(RoomMemberSummaryEntity::class.java) val realm = backgroundSessionRealm.get() ?: return@launch
?.`in`(RoomMemberSummaryEntityFields.USER_ID, users.toTypedArray()) val distinctRoomIds = realm.where(RoomMemberSummaryEntity::class.java)
?.findAll() .`in`(RoomMemberSummaryEntityFields.USER_ID, users.toTypedArray())
?.map { it.roomId } .distinct(RoomMemberSummaryEntityFields.ROOM_ID)
?.distinct()
val map = HashMap<String, List<String>>()
impactedRoomsId?.forEach { roomId ->
backgroundSessionRealm.get()?.let { realm ->
RoomMemberSummaryEntity.where(realm, roomId)
.findAll() .findAll()
.let { results -> .map { it.roomId }
map[roomId] = results.map { it.userId }
}
}
}
map.forEach { entry -> distinctRoomIds.forEach { roomId ->
val roomId = entry.key val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
val userList = entry.value if (roomSummary?.isEncrypted.orFalse()) {
taskExecutor.executorScope.launch { val allActiveRoomMembers = RoomMemberHelper(realm, roomId).getActiveRoomMemberIds()
withContext(coroutineDispatchers.crypto) {
try { try {
// Can throw if the crypto database has been closed in between, in this case log and ignore? val updatedTrust = computeTrustTask.execute(
val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(userList)) ComputeTrustTask.Params(allActiveRoomMembers, roomSummary?.isDirect == true)
BACKGROUND_HANDLER.post { )
backgroundSessionRealm.get()?.executeTransaction { realm -> realm.executeTransaction {
roomSummaryUpdater.updateShieldTrust(realm, roomId, updatedTrust) roomSummaryUpdater.updateShieldTrust(it, roomId, updatedTrust)
}
} }
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.e(failure) Timber.e(failure)
@ -137,4 +123,3 @@ internal class ShieldTrustUpdater @Inject constructor(
} }
} }
} }
}

View File

@ -29,7 +29,8 @@ data class CryptoDeviceInfo(
override val signatures: Map<String, Map<String, String>>? = null, override val signatures: Map<String, Map<String, String>>? = null,
val unsigned: JsonDict? = null, val unsigned: JsonDict? = null,
var trustLevel: DeviceTrustLevel? = null, var trustLevel: DeviceTrustLevel? = null,
var isBlocked: Boolean = false var isBlocked: Boolean = false,
val firstTimeSeenLocalTs: Long? = null
) : CryptoInfo { ) : CryptoInfo {
val isVerified: Boolean val isVerified: Boolean

View File

@ -61,20 +61,4 @@ internal object CryptoInfoMapper {
signatures = keyInfo.signatures 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)
}
} }

View File

@ -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.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper 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.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.rest.RoomKeyRequestBody
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
import org.matrix.olm.OlmAccount import org.matrix.olm.OlmAccount
@ -218,6 +219,9 @@ internal interface IMXCryptoStore {
// TODO temp // TODO temp
fun getLiveDeviceList(): LiveData<List<CryptoDeviceInfo>> fun getLiveDeviceList(): LiveData<List<CryptoDeviceInfo>>
fun getMyDevicesInfo() : List<DeviceInfo>
fun getLiveMyDevicesInfo() : LiveData<List<DeviceInfo>>
fun saveMyDevicesInfo(info: List<DeviceInfo>)
/** /**
* Store the crypto algorithm for a room. * Store the crypto algorithm for a room.
* *
@ -405,6 +409,7 @@ internal interface IMXCryptoStore {
fun storeUSKPrivateKey(usk: String?) fun storeUSKPrivateKey(usk: String?)
fun getCrossSigningPrivateKeys(): PrivateKeysInfo? fun getCrossSigningPrivateKeys(): PrivateKeysInfo?
fun getLiveCrossSigningPrivateKeys(): LiveData<Optional<PrivateKeysInfo>>
fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) fun saveBackupRecoveryKey(recoveryKey: String?, version: String?)
fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo? fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo?

View File

@ -62,6 +62,7 @@ fun doRealmTransaction(realmConfiguration: RealmConfiguration, action: (Realm) -
realm.executeTransaction { action.invoke(it) } realm.executeTransaction { action.invoke(it) }
} }
} }
fun doRealmTransactionAsync(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) { fun doRealmTransactionAsync(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) {
Realm.getInstance(realmConfiguration).use { realm -> Realm.getInstance(realmConfiguration).use { realm ->
realm.executeTransactionAsync { action.invoke(it) } realm.executeTransactionAsync { action.invoke(it) }
@ -79,31 +80,26 @@ fun serializeForRealm(o: Any?): String? {
val baos = ByteArrayOutputStream() val baos = ByteArrayOutputStream()
val gzis = CompatUtil.createGzipOutputStream(baos) val gzis = CompatUtil.createGzipOutputStream(baos)
val out = ObjectOutputStream(gzis) val out = ObjectOutputStream(gzis)
out.use {
out.writeObject(o) it.writeObject(o)
out.close() }
return Base64.encodeToString(baos.toByteArray(), Base64.DEFAULT) return Base64.encodeToString(baos.toByteArray(), Base64.DEFAULT)
} }
/** /**
* Do the opposite of serializeForRealm. * Do the opposite of serializeForRealm.
*/ */
@Suppress("UNCHECKED_CAST")
fun <T> deserializeFromRealm(string: String?): T? { fun <T> deserializeFromRealm(string: String?): T? {
if (string == null) { if (string == null) {
return null return null
} }
val decodedB64 = Base64.decode(string.toByteArray(), Base64.DEFAULT) val decodedB64 = Base64.decode(string.toByteArray(), Base64.DEFAULT)
val bais = ByteArrayInputStream(decodedB64) val bais = ByteArrayInputStream(decodedB64)
val gzis = GZIPInputStream(bais) val gzis = GZIPInputStream(bais)
val ois = ObjectInputStream(gzis) val ois = ObjectInputStream(gzis)
return ois.use {
@Suppress("UNCHECKED_CAST") it.readObject() as T
val result = ois.readObject() as T }
ois.close()
return result
} }

View File

@ -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.OutgoingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.OutgoingSecretRequest 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.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.CryptoCrossSigningKey
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo 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.OlmInboundGroupSessionWrapper
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper 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.rest.RoomKeyRequestBody
import im.vector.matrix.android.internal.crypto.model.toEntity 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.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo 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.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.CrossSigningInfoEntity
import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMapper 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.IncomingGossipingRequestEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity 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.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.OlmInboundGroupSessionEntity
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity
@ -91,6 +93,7 @@ import kotlin.collections.set
@SessionScope @SessionScope
internal class RealmCryptoStore @Inject constructor( internal class RealmCryptoStore @Inject constructor(
@CryptoDatabase private val realmConfiguration: RealmConfiguration, @CryptoDatabase private val realmConfiguration: RealmConfiguration,
private val crossSigningKeysMapper: CrossSigningKeysMapper,
private val credentials: Credentials) : IMXCryptoStore { private val credentials: Credentials) : IMXCryptoStore {
/* ========================================================================================== /* ==========================================================================================
@ -200,9 +203,9 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun getDeviceId(): String { override fun getDeviceId(): String {
return doRealmQueryAndCopy(realmConfiguration) { return doWithRealm(realmConfiguration) {
it.where<CryptoMetadataEntity>().findFirst() it.where<CryptoMetadataEntity>().findFirst()?.deviceId
}?.deviceId ?: "" } ?: ""
} }
override fun saveOlmAccount() { override fun saveOlmAccount() {
@ -256,23 +259,24 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun getUserDevice(userId: String, deviceId: String): CryptoDeviceInfo? { override fun getUserDevice(userId: String, deviceId: String): CryptoDeviceInfo? {
return doRealmQueryAndCopy(realmConfiguration) { return doWithRealm(realmConfiguration) {
it.where<DeviceInfoEntity>() it.where<DeviceInfoEntity>()
.equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId)) .equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId))
.findFirst() .findFirst()
}?.let { ?.let { deviceInfo ->
CryptoMapper.mapToModel(it) CryptoMapper.mapToModel(deviceInfo)
}
} }
} }
override fun deviceWithIdentityKey(identityKey: String): CryptoDeviceInfo? { override fun deviceWithIdentityKey(identityKey: String): CryptoDeviceInfo? {
return doRealmQueryAndCopy(realmConfiguration) { return doWithRealm(realmConfiguration) {
it.where<DeviceInfoEntity>() it.where<DeviceInfoEntity>()
.equalTo(DeviceInfoEntityFields.IDENTITY_KEY, identityKey) .equalTo(DeviceInfoEntityFields.IDENTITY_KEY, identityKey)
.findFirst() .findFirst()
?.let { deviceInfo ->
CryptoMapper.mapToModel(deviceInfo)
} }
?.let {
CryptoMapper.mapToModel(it)
} }
} }
@ -285,10 +289,16 @@ internal class RealmCryptoStore @Inject constructor(
UserEntity.getOrCreate(realm, userId) UserEntity.getOrCreate(realm, userId)
.let { u -> .let { u ->
// Add the devices // 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 // Ensure all other devices are deleted
u.devices.deleteAllFromRealm() u.devices.deleteAllFromRealm()
val new = devices.map { entry -> entry.value.toEntity() }
new.forEach { realm.insertOrUpdate(it) }
u.devices.addAll(new) u.devices.addAll(new)
} }
} }
@ -309,36 +319,19 @@ internal class RealmCryptoStore @Inject constructor(
} else { } else {
CrossSigningInfoEntity.getOrCreate(realm, userId).let { signingInfo -> CrossSigningInfoEntity.getOrCreate(realm, userId).let { signingInfo ->
// What should we do if we detect a change of the keys? // What should we do if we detect a change of the keys?
val existingMaster = signingInfo.getMasterKey() val existingMaster = signingInfo.getMasterKey()
if (existingMaster != null && existingMaster.publicKeyBase64 == masterKey.unpaddedBase64PublicKey) { if (existingMaster != null && existingMaster.publicKeyBase64 == masterKey.unpaddedBase64PublicKey) {
// update signatures? crossSigningKeysMapper.update(existingMaster, masterKey)
existingMaster.putSignatures(masterKey.signatures)
existingMaster.usages = masterKey.usages?.toTypedArray()?.let { RealmList(*it) }
?: RealmList()
} else { } else {
val keyEntity = realm.createObject(KeyInfoEntity::class.java).apply { val keyEntity = crossSigningKeysMapper.map(masterKey)
this.publicKeyBase64 = masterKey.unpaddedBase64PublicKey
this.usages = masterKey.usages?.toTypedArray()?.let { RealmList(*it) }
?: RealmList()
this.putSignatures(masterKey.signatures)
}
signingInfo.setMasterKey(keyEntity) signingInfo.setMasterKey(keyEntity)
} }
val existingSelfSigned = signingInfo.getSelfSignedKey() val existingSelfSigned = signingInfo.getSelfSignedKey()
if (existingSelfSigned != null && existingSelfSigned.publicKeyBase64 == selfSigningKey.unpaddedBase64PublicKey) { if (existingSelfSigned != null && existingSelfSigned.publicKeyBase64 == selfSigningKey.unpaddedBase64PublicKey) {
// update signatures? crossSigningKeysMapper.update(existingSelfSigned, selfSigningKey)
existingSelfSigned.putSignatures(selfSigningKey.signatures)
existingSelfSigned.usages = selfSigningKey.usages?.toTypedArray()?.let { RealmList(*it) }
?: RealmList()
} else { } else {
val keyEntity = realm.createObject(KeyInfoEntity::class.java).apply { val keyEntity = crossSigningKeysMapper.map(selfSigningKey)
this.publicKeyBase64 = selfSigningKey.unpaddedBase64PublicKey
this.usages = selfSigningKey.usages?.toTypedArray()?.let { RealmList(*it) }
?: RealmList()
this.putSignatures(selfSigningKey.signatures)
}
signingInfo.setSelfSignedKey(keyEntity) signingInfo.setSelfSignedKey(keyEntity)
} }
@ -346,21 +339,12 @@ internal class RealmCryptoStore @Inject constructor(
if (userSigningKey != null) { if (userSigningKey != null) {
val existingUSK = signingInfo.getUserSigningKey() val existingUSK = signingInfo.getUserSigningKey()
if (existingUSK != null && existingUSK.publicKeyBase64 == userSigningKey.unpaddedBase64PublicKey) { if (existingUSK != null && existingUSK.publicKeyBase64 == userSigningKey.unpaddedBase64PublicKey) {
// update signatures? crossSigningKeysMapper.update(existingUSK, userSigningKey)
existingUSK.putSignatures(userSigningKey.signatures)
existingUSK.usages = userSigningKey.usages?.toTypedArray()?.let { RealmList(*it) }
?: RealmList()
} else { } else {
val keyEntity = realm.createObject(KeyInfoEntity::class.java).apply { val keyEntity = crossSigningKeysMapper.map(userSigningKey)
this.publicKeyBase64 = userSigningKey.unpaddedBase64PublicKey
this.usages = userSigningKey.usages?.toTypedArray()?.let { RealmList(*it) }
?: RealmList()
this.putSignatures(userSigningKey.signatures)
}
signingInfo.setUserSignedKey(keyEntity) signingInfo.setUserSignedKey(keyEntity)
} }
} }
userEntity.crossSigningInfoEntity = signingInfo userEntity.crossSigningInfoEntity = signingInfo
} }
} }
@ -369,9 +353,10 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun getCrossSigningPrivateKeys(): PrivateKeysInfo? { override fun getCrossSigningPrivateKeys(): PrivateKeysInfo? {
return doRealmQueryAndCopy(realmConfiguration) { realm -> return doWithRealm(realmConfiguration) { realm ->
realm.where<CryptoMetadataEntity>().findFirst() realm.where<CryptoMetadataEntity>()
}?.let { .findFirst()
?.let {
PrivateKeysInfo( PrivateKeysInfo(
master = it.xSignMasterPrivateKey, master = it.xSignMasterPrivateKey,
selfSigned = it.xSignSelfSignedPrivateKey, selfSigned = it.xSignSelfSignedPrivateKey,
@ -379,6 +364,26 @@ internal class RealmCryptoStore @Inject constructor(
) )
} }
} }
}
override fun getLiveCrossSigningPrivateKeys(): LiveData<Optional<PrivateKeysInfo>> {
val liveData = monarchy.findAllMappedWithChanges(
{ realm: Realm ->
realm
.where<CryptoMetadataEntity>()
},
{
PrivateKeysInfo(
master = it.xSignMasterPrivateKey,
selfSigned = it.xSignSelfSignedPrivateKey,
user = it.xSignUserPrivateKey
)
}
)
return Transformations.map(liveData) {
it.firstOrNull().toOptional()
}
}
override fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) { override fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) {
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction(realmConfiguration) { realm ->
@ -400,9 +405,10 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? { override fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? {
return doRealmQueryAndCopy(realmConfiguration) { realm -> return doWithRealm(realmConfiguration) { realm ->
realm.where<CryptoMetadataEntity>().findFirst() realm.where<CryptoMetadataEntity>()
}?.let { .findFirst()
?.let {
val key = it.keyBackupRecoveryKey val key = it.keyBackupRecoveryKey
val version = it.keyBackupRecoveryKeyVersion val version = it.keyBackupRecoveryKeyVersion
if (!key.isNullOrBlank() && !version.isNullOrBlank()) { if (!key.isNullOrBlank() && !version.isNullOrBlank()) {
@ -412,6 +418,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
} }
} }
}
override fun storeSSKPrivateKey(ssk: String?) { override fun storeSSKPrivateKey(ssk: String?) {
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction(realmConfiguration) { realm ->
@ -430,24 +437,30 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun getUserDevices(userId: String): Map<String, CryptoDeviceInfo>? { override fun getUserDevices(userId: String): Map<String, CryptoDeviceInfo>? {
return doRealmQueryAndCopy(realmConfiguration) { return doWithRealm(realmConfiguration) {
it.where<UserEntity>() it.where<UserEntity>()
.equalTo(UserEntityFields.USER_ID, userId) .equalTo(UserEntityFields.USER_ID, userId)
.findFirst() .findFirst()
}
?.devices ?.devices
?.map { CryptoMapper.mapToModel(it) } ?.map { deviceInfo ->
?.associateBy { it.deviceId } CryptoMapper.mapToModel(deviceInfo)
}
?.associateBy { cryptoDevice ->
cryptoDevice.deviceId
}
}
} }
override fun getUserDeviceList(userId: String): List<CryptoDeviceInfo>? { override fun getUserDeviceList(userId: String): List<CryptoDeviceInfo>? {
return doRealmQueryAndCopy(realmConfiguration) { return doWithRealm(realmConfiguration) {
it.where<UserEntity>() it.where<UserEntity>()
.equalTo(UserEntityFields.USER_ID, userId) .equalTo(UserEntityFields.USER_ID, userId)
.findFirst() .findFirst()
}
?.devices ?.devices
?.map { CryptoMapper.mapToModel(it) } ?.map { deviceInfo ->
CryptoMapper.mapToModel(deviceInfo)
}
}
} }
override fun getLiveDeviceList(userId: String): LiveData<List<CryptoDeviceInfo>> { override fun getLiveDeviceList(userId: String): LiveData<List<CryptoDeviceInfo>> {
@ -496,6 +509,52 @@ internal class RealmCryptoStore @Inject constructor(
} }
} }
override fun getMyDevicesInfo(): List<DeviceInfo> {
return monarchy.fetchAllCopiedSync {
it.where<MyDeviceLastSeenInfoEntity>()
}.map {
DeviceInfo(
deviceId = it.deviceId,
lastSeenIp = it.lastSeenIp,
lastSeenTs = it.lastSeenTs,
displayName = it.displayName
)
}
}
override fun getLiveMyDevicesInfo(): LiveData<List<DeviceInfo>> {
return monarchy.findAllMappedWithChanges(
{ realm: Realm ->
realm.where<MyDeviceLastSeenInfoEntity>()
},
{ entity ->
DeviceInfo(
deviceId = entity.deviceId,
lastSeenIp = entity.lastSeenIp,
lastSeenTs = entity.lastSeenTs,
displayName = entity.displayName
)
}
)
}
override fun saveMyDevicesInfo(info: List<DeviceInfo>) {
val entities = info.map {
MyDeviceLastSeenInfoEntity(
lastSeenTs = it.lastSeenTs,
lastSeenIp = it.lastSeenIp,
displayName = it.displayName,
deviceId = it.deviceId
)
}
monarchy.writeAsync { realm ->
realm.where<MyDeviceLastSeenInfoEntity>().findAll().deleteAllFromRealm()
entities.forEach {
realm.insertOrUpdate(it)
}
}
}
override fun storeRoomAlgorithm(roomId: String, algorithm: String) { override fun storeRoomAlgorithm(roomId: String, algorithm: String) {
doRealmTransaction(realmConfiguration) { doRealmTransaction(realmConfiguration) {
CryptoRoomEntity.getOrCreate(it, roomId).algorithm = algorithm CryptoRoomEntity.getOrCreate(it, roomId).algorithm = algorithm
@ -503,17 +562,16 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun getRoomAlgorithm(roomId: String): String? { override fun getRoomAlgorithm(roomId: String): String? {
return doRealmQueryAndCopy(realmConfiguration) { return doWithRealm(realmConfiguration) {
CryptoRoomEntity.getById(it, roomId) CryptoRoomEntity.getById(it, roomId)?.algorithm
} }
?.algorithm
} }
override fun shouldEncryptForInvitedMembers(roomId: String): Boolean { override fun shouldEncryptForInvitedMembers(roomId: String): Boolean {
return doRealmQueryAndCopy(realmConfiguration) { return doWithRealm(realmConfiguration) {
CryptoRoomEntity.getById(it, roomId) CryptoRoomEntity.getById(it, roomId)?.shouldEncryptForInvitedMembers
} }
?.shouldEncryptForInvitedMembers ?: false ?: false
} }
override fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) { override fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) {
@ -577,23 +635,23 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun getLastUsedSessionId(deviceKey: String): String? { override fun getLastUsedSessionId(deviceKey: String): String? {
return doRealmQueryAndCopy(realmConfiguration) { return doWithRealm(realmConfiguration) {
it.where<OlmSessionEntity>() it.where<OlmSessionEntity>()
.equalTo(OlmSessionEntityFields.DEVICE_KEY, deviceKey) .equalTo(OlmSessionEntityFields.DEVICE_KEY, deviceKey)
.sort(OlmSessionEntityFields.LAST_RECEIVED_MESSAGE_TS, Sort.DESCENDING) .sort(OlmSessionEntityFields.LAST_RECEIVED_MESSAGE_TS, Sort.DESCENDING)
.findFirst() .findFirst()
}
?.sessionId ?.sessionId
} }
}
override fun getDeviceSessionIds(deviceKey: String): MutableSet<String> { override fun getDeviceSessionIds(deviceKey: String): MutableSet<String> {
return doRealmQueryAndCopyList(realmConfiguration) { return doWithRealm(realmConfiguration) {
it.where<OlmSessionEntity>() it.where<OlmSessionEntity>()
.equalTo(OlmSessionEntityFields.DEVICE_KEY, deviceKey) .equalTo(OlmSessionEntityFields.DEVICE_KEY, deviceKey)
.findAll() .findAll()
.mapNotNull { sessionEntity ->
sessionEntity.sessionId
} }
.mapNotNull {
it.sessionId
} }
.toMutableSet() .toMutableSet()
} }
@ -641,12 +699,12 @@ internal class RealmCryptoStore @Inject constructor(
// If not in cache (or not found), try to read it from realm // If not in cache (or not found), try to read it from realm
if (inboundGroupSessionToRelease[key] == null) { if (inboundGroupSessionToRelease[key] == null) {
doRealmQueryAndCopy(realmConfiguration) { doWithRealm(realmConfiguration) {
it.where<OlmInboundGroupSessionEntity>() it.where<OlmInboundGroupSessionEntity>()
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
.findFirst() .findFirst()
}
?.getInboundGroupSession() ?.getInboundGroupSession()
}
?.let { ?.let {
inboundGroupSessionToRelease[key] = it inboundGroupSessionToRelease[key] = it
} }
@ -660,12 +718,12 @@ internal class RealmCryptoStore @Inject constructor(
* so there is no need to use or update `inboundGroupSessionToRelease` for native memory management * so there is no need to use or update `inboundGroupSessionToRelease` for native memory management
*/ */
override fun getInboundGroupSessions(): MutableList<OlmInboundGroupSessionWrapper> { override fun getInboundGroupSessions(): MutableList<OlmInboundGroupSessionWrapper> {
return doRealmQueryAndCopyList(realmConfiguration) { return doWithRealm(realmConfiguration) {
it.where<OlmInboundGroupSessionEntity>() it.where<OlmInboundGroupSessionEntity>()
.findAll() .findAll()
.mapNotNull { inboundGroupSessionEntity ->
inboundGroupSessionEntity.getInboundGroupSession()
} }
.mapNotNull {
it.getInboundGroupSession()
} }
.toMutableList() .toMutableList()
} }
@ -755,15 +813,16 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun inboundGroupSessionsToBackup(limit: Int): List<OlmInboundGroupSessionWrapper> { override fun inboundGroupSessionsToBackup(limit: Int): List<OlmInboundGroupSessionWrapper> {
return doRealmQueryAndCopyList(realmConfiguration) { return doWithRealm(realmConfiguration) {
it.where<OlmInboundGroupSessionEntity>() it.where<OlmInboundGroupSessionEntity>()
.equalTo(OlmInboundGroupSessionEntityFields.BACKED_UP, false) .equalTo(OlmInboundGroupSessionEntityFields.BACKED_UP, false)
.limit(limit.toLong()) .limit(limit.toLong())
.findAll() .findAll()
}.mapNotNull { inboundGroupSession -> .mapNotNull { inboundGroupSession ->
inboundGroupSession.getInboundGroupSession() inboundGroupSession.getInboundGroupSession()
} }
} }
}
override fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int { override fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int {
return doWithRealm(realmConfiguration) { return doWithRealm(realmConfiguration) {
@ -785,10 +844,9 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun getGlobalBlacklistUnverifiedDevices(): Boolean { override fun getGlobalBlacklistUnverifiedDevices(): Boolean {
return doRealmQueryAndCopy(realmConfiguration) { return doWithRealm(realmConfiguration) {
it.where<CryptoMetadataEntity>().findFirst() it.where<CryptoMetadataEntity>().findFirst()?.globalBlacklistUnverifiedDevices
}?.globalBlacklistUnverifiedDevices } ?: false
?: false
} }
override fun setRoomsListBlacklistUnverifiedDevices(roomIds: List<String>) { override fun setRoomsListBlacklistUnverifiedDevices(roomIds: List<String>) {
@ -811,27 +869,27 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun getRoomsListBlacklistUnverifiedDevices(): MutableList<String> { override fun getRoomsListBlacklistUnverifiedDevices(): MutableList<String> {
return doRealmQueryAndCopyList(realmConfiguration) { return doWithRealm(realmConfiguration) {
it.where<CryptoRoomEntity>() it.where<CryptoRoomEntity>()
.equalTo(CryptoRoomEntityFields.BLACKLIST_UNVERIFIED_DEVICES, true) .equalTo(CryptoRoomEntityFields.BLACKLIST_UNVERIFIED_DEVICES, true)
.findAll() .findAll()
.mapNotNull { cryptoRoom ->
cryptoRoom.roomId
} }
.mapNotNull {
it.roomId
} }
.toMutableList() .toMutableList()
} }
override fun getDeviceTrackingStatuses(): MutableMap<String, Int> { override fun getDeviceTrackingStatuses(): MutableMap<String, Int> {
return doRealmQueryAndCopyList(realmConfiguration) { return doWithRealm(realmConfiguration) {
it.where<UserEntity>() it.where<UserEntity>()
.findAll() .findAll()
.associateBy { user ->
user.userId!!
} }
.associateBy { .mapValues { entry ->
it.userId!! entry.value.deviceTrackingStatus
} }
.mapValues {
it.value.deviceTrackingStatus
} }
.toMutableMap() .toMutableMap()
} }
@ -847,12 +905,12 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun getDeviceTrackingStatus(userId: String, defaultValue: Int): Int { override fun getDeviceTrackingStatus(userId: String, defaultValue: Int): Int {
return doRealmQueryAndCopy(realmConfiguration) { return doWithRealm(realmConfiguration) {
it.where<UserEntity>() it.where<UserEntity>()
.equalTo(UserEntityFields.USER_ID, userId) .equalTo(UserEntityFields.USER_ID, userId)
.findFirst() .findFirst()
}
?.deviceTrackingStatus ?.deviceTrackingStatus
}
?: defaultValue ?: defaultValue
} }
@ -1089,24 +1147,25 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest? { override fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest? {
return doRealmQueryAndCopyList(realmConfiguration) { realm -> return doWithRealm(realmConfiguration) { realm ->
realm.where<IncomingGossipingRequestEntity>() realm.where<IncomingGossipingRequestEntity>()
.equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) .equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
.equalTo(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, deviceId) .equalTo(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, deviceId)
.equalTo(IncomingGossipingRequestEntityFields.OTHER_USER_ID, userId) .equalTo(IncomingGossipingRequestEntityFields.OTHER_USER_ID, userId)
.findAll() .findAll()
}.mapNotNull { entity -> .mapNotNull { entity ->
entity.toIncomingGossipingRequest() as? IncomingRoomKeyRequest entity.toIncomingGossipingRequest() as? IncomingRoomKeyRequest
}.firstOrNull() }
.firstOrNull()
}
} }
override fun getPendingIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> { override fun getPendingIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> {
return doRealmQueryAndCopyList(realmConfiguration) { return doWithRealm(realmConfiguration) {
it.where<IncomingGossipingRequestEntity>() it.where<IncomingGossipingRequestEntity>()
.equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) .equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
.equalTo(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, GossipingRequestState.PENDING.name) .equalTo(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, GossipingRequestState.PENDING.name)
.findAll() .findAll()
}
.map { entity -> .map { entity ->
IncomingRoomKeyRequest( IncomingRoomKeyRequest(
userId = entity.otherUserId, userId = entity.otherUserId,
@ -1117,13 +1176,13 @@ internal class RealmCryptoStore @Inject constructor(
) )
} }
} }
}
override fun getPendingIncomingGossipingRequests(): List<IncomingShareRequestCommon> { override fun getPendingIncomingGossipingRequests(): List<IncomingShareRequestCommon> {
return doRealmQueryAndCopyList(realmConfiguration) { return doWithRealm(realmConfiguration) {
it.where<IncomingGossipingRequestEntity>() it.where<IncomingGossipingRequestEntity>()
.equalTo(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, GossipingRequestState.PENDING.name) .equalTo(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, GossipingRequestState.PENDING.name)
.findAll() .findAll()
}
.mapNotNull { entity -> .mapNotNull { entity ->
when (entity.type) { when (entity.type) {
GossipRequestType.KEY -> { GossipRequestType.KEY -> {
@ -1147,6 +1206,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
} }
} }
}
override fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?) { override fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?) {
doRealmTransactionAsync(realmConfiguration) { realm -> doRealmTransactionAsync(realmConfiguration) { realm ->
@ -1183,9 +1243,9 @@ internal class RealmCryptoStore @Inject constructor(
* Cross Signing * Cross Signing
* ========================================================================================== */ * ========================================================================================== */
override fun getMyCrossSigningInfo(): MXCrossSigningInfo? { override fun getMyCrossSigningInfo(): MXCrossSigningInfo? {
return doRealmQueryAndCopy(realmConfiguration) { return doWithRealm(realmConfiguration) {
it.where<CryptoMetadataEntity>().findFirst() it.where<CryptoMetadataEntity>().findFirst()?.userId
}?.userId?.let { }?.let {
getCrossSigningInfo(it) getCrossSigningInfo(it)
} }
} }
@ -1304,33 +1364,24 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? { override fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? {
return doRealmQueryAndCopy(realmConfiguration) { realm -> return doWithRealm(realmConfiguration) { realm ->
realm.where(CrossSigningInfoEntity::class.java) val crossSigningInfo = realm.where(CrossSigningInfoEntity::class.java)
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId) .equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
.findFirst() .findFirst()
}?.let { xsignInfo -> if (crossSigningInfo == null) {
mapCrossSigningInfoEntity(xsignInfo) null
} else {
mapCrossSigningInfoEntity(crossSigningInfo)
}
} }
} }
private fun mapCrossSigningInfoEntity(xsignInfo: CrossSigningInfoEntity): MXCrossSigningInfo { private fun mapCrossSigningInfoEntity(xsignInfo: CrossSigningInfoEntity): MXCrossSigningInfo {
val userId = xsignInfo.userId ?: ""
return MXCrossSigningInfo( return MXCrossSigningInfo(
userId = xsignInfo.userId ?: "", userId = userId,
crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull { crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull {
val pubKey = it.publicKeyBase64 ?: return@mapNotNull null crossSigningKeysMapper.map(userId, it)
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
)
}
)
} }
) )
} }
@ -1341,26 +1392,7 @@ internal class RealmCryptoStore @Inject constructor(
realm.where<CrossSigningInfoEntity>() realm.where<CrossSigningInfoEntity>()
.equalTo(UserEntityFields.USER_ID, userId) .equalTo(UserEntityFields.USER_ID, userId)
}, },
{ entity -> { mapCrossSigningInfoEntity(it) }
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
)
}
)
}
)
}
) )
return Transformations.map(liveData) { return Transformations.map(liveData) {
it.firstOrNull().toOptional() it.firstOrNull().toOptional()
@ -1402,17 +1434,8 @@ internal class RealmCryptoStore @Inject constructor(
// existing.crossSigningKeys.forEach { it.deleteFromRealm() } // existing.crossSigningKeys.forEach { it.deleteFromRealm() }
val xkeys = RealmList<KeyInfoEntity>() val xkeys = RealmList<KeyInfoEntity>()
info.crossSigningKeys.forEach { cryptoCrossSigningKey -> info.crossSigningKeys.forEach { cryptoCrossSigningKey ->
xkeys.add( val keyEntity = crossSigningKeysMapper.map(cryptoCrossSigningKey)
realm.createObject(KeyInfoEntity::class.java).also { keyInfoEntity -> xkeys.add(keyEntity)
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
}
)
} }
existing.crossSigningKeys = xkeys existing.crossSigningKeys = xkeys
} }

View File

@ -18,14 +18,17 @@ package im.vector.matrix.android.internal.crypto.store.db
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.Types 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.api.util.JsonDict
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo 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.CrossSigningInfoEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMetadataEntityFields 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.DeviceInfoEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.GossipingEventEntityFields 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.IncomingGossipingRequestEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntityFields 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.OutgoingGossipingRequestEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields
@ -33,11 +36,14 @@ import im.vector.matrix.android.internal.di.SerializeNulls
import io.realm.DynamicRealm import io.realm.DynamicRealm
import io.realm.RealmMigration import io.realm.RealmMigration
import timber.log.Timber 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 // 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) { override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.v("Migrating Realm Crypto from $oldVersion to $newVersion") Timber.v("Migrating Realm Crypto from $oldVersion to $newVersion")
@ -45,6 +51,8 @@ internal object RealmCryptoStoreMigration : RealmMigration {
if (oldVersion <= 0) migrateTo1(realm) if (oldVersion <= 0) migrateTo1(realm)
if (oldVersion <= 1) migrateTo2(realm) if (oldVersion <= 1) migrateTo2(realm)
if (oldVersion <= 2) migrateTo3(realm) if (oldVersion <= 2) migrateTo3(realm)
if (oldVersion <= 3) migrateTo4(realm)
if (oldVersion <= 4) migrateTo5(realm)
} }
private fun migrateTo1(realm: DynamicRealm) { 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, String::class.java)
?.addField(CryptoMetadataEntityFields.KEY_BACKUP_RECOVERY_KEY_VERSION, 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<String, Map<String, String>>? = 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)
}
}
}
} }

View File

@ -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.IncomingGossipingRequestEntity
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity 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.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.OlmInboundGroupSessionEntity
import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntity import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntity
@ -48,6 +49,7 @@ import io.realm.annotations.RealmModule
TrustLevelEntity::class, TrustLevelEntity::class,
GossipingEventEntity::class, GossipingEventEntity::class,
IncomingGossipingRequestEntity::class, IncomingGossipingRequestEntity::class,
OutgoingGossipingRequestEntity::class OutgoingGossipingRequestEntity::class,
MyDeviceLastSeenInfoEntity::class
]) ])
internal class RealmCryptoStoreModule internal class RealmCryptoStoreModule

View File

@ -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<Map<String, Map<String, String>>>(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, Map<String, String>>?): String {
return signaturesAdapter.toJson(signatures)
}
fun deserializeSignatures(signatures: String?): Map<String, Map<String, String>>? {
if (signatures == null) {
return null
}
return try {
signaturesAdapter.fromJson(signatures)
} catch (failure: Throwable) {
Timber.e(failure)
null
}
}
}

View File

@ -104,7 +104,8 @@ object CryptoMapper {
Timber.e(failure) Timber.e(failure)
null null
} }
} },
firstTimeSeenLocalTs = deviceInfoEntity.firstTimeSeenLocalTs
) )
} }
} }

View File

@ -34,7 +34,12 @@ internal open class DeviceInfoEntity(@PrimaryKey var primaryKey: String = "",
var keysMapJson: String? = null, var keysMapJson: String? = null,
var signatureMapJson: String? = null, var signatureMapJson: String? = null,
var unsignedMapJson: 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() { ) : RealmObject() {
// // Deserialize data // // Deserialize data

View File

@ -16,8 +16,6 @@
package im.vector.matrix.android.internal.crypto.store.db.model 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.RealmList
import io.realm.RealmObject import io.realm.RealmObject
@ -31,15 +29,4 @@ internal open class KeyInfoEntity(
*/ */
var signatures: String? = null, var signatures: String? = null,
var trustLevelEntity: TrustLevelEntity? = null var trustLevelEntity: TrustLevelEntity? = null
) : RealmObject() { ) : RealmObject()
// Deserialize data
fun getSignatures(): Map<String, Map<String, String>>? {
return deserializeFromRealm(signatures)
}
// Serialize data
fun putSignatures(deviceInfo: Map<String, Map<String, String>>?) {
signatures = serializeForRealm(deviceInfo)
}
}

View File

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

View File

@ -17,10 +17,9 @@
package im.vector.matrix.android.internal.crypto.tasks package im.vector.matrix.android.internal.crypto.tasks
import im.vector.matrix.android.api.failure.Failure 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.api.CryptoApi
import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceParams 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.network.executeRequest
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
@ -43,25 +42,9 @@ internal class DefaultDeleteDeviceTask @Inject constructor(
apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams()) apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams())
} }
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
if (throwable is Failure.OtherServerError && throwable.httpCode == 401) { throw throwable.toRegistrationFlowResponse()
// Parse to get a RegistrationFlowResponse ?.let { Failure.RegistrationFlowError(it) }
val registrationFlowResponse = try { ?: throwable
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
}
} }
} }
} }

View File

@ -52,6 +52,8 @@ internal class DefaultSendToDeviceTask @Inject constructor(
params.transactionId ?: Random.nextInt(Integer.MAX_VALUE).toString(), params.transactionId ?: Random.nextInt(Integer.MAX_VALUE).toString(),
sendToDeviceBody sendToDeviceBody
) )
isRetryable = true
maxRetryCount = 3
} }
} }
} }

View File

@ -17,14 +17,13 @@
package im.vector.matrix.android.internal.crypto.tasks package im.vector.matrix.android.internal.crypto.tasks
import im.vector.matrix.android.api.failure.Failure 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.api.CryptoApi
import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey 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.KeysQueryResponse
import im.vector.matrix.android.internal.crypto.model.rest.UploadSigningKeysBody 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.rest.UserPasswordAuth
import im.vector.matrix.android.internal.crypto.model.toRest 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.network.executeRequest
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
@ -65,37 +64,25 @@ internal class DefaultUploadSigningKeysTask @Inject constructor(
} }
return return
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
if (throwable is Failure.OtherServerError val registrationFlowResponse = throwable.toRegistrationFlowResponse()
&& throwable.httpCode == 401 if (registrationFlowResponse != null
&& params.userPasswordAuth != null && params.userPasswordAuth != null
/* Avoid infinite loop */ /* Avoid infinite loop */
&& params.userPasswordAuth.session.isNullOrEmpty() && params.userPasswordAuth.session.isNullOrEmpty()
) { ) {
try {
MoshiProvider.providesMoshi()
.adapter(RegistrationFlowResponse::class.java)
.fromJson(throwable.errorBody)
} catch (e: Exception) {
null
}?.let {
// Retry with authentication // Retry with authentication
try {
val req = executeRequest<KeysQueryResponse>(eventBus) { val req = executeRequest<KeysQueryResponse>(eventBus) {
apiCall = cryptoApi.uploadSigningKeys( apiCall = cryptoApi.uploadSigningKeys(
uploadQuery.copy(auth = params.userPasswordAuth.copy(session = it.session)) uploadQuery.copy(auth = params.userPasswordAuth.copy(session = registrationFlowResponse.session))
) )
} }
if (req.failures?.isNotEmpty() == true) { if (req.failures?.isNotEmpty() == true) {
throw UploadSigningKeys(req.failures) throw UploadSigningKeys(req.failures)
} }
return } else {
} catch (failure: Throwable) {
throw failure
}
}
}
// Other error // Other error
throw throwable throw throwable
} }
} }
} }
}

View File

@ -138,7 +138,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
override fun onVerificationAccept(accept: ValidVerificationInfoAccept) { override fun onVerificationAccept(accept: ValidVerificationInfoAccept) {
Timber.v("## SAS O: onVerificationAccept id:$transactionId") 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") Timber.e("## SAS O: received accept request from invalid state $state")
cancel(CancelCode.UnexpectedMessage) cancel(CancelCode.UnexpectedMessage)
return return
@ -148,7 +148,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
|| !KNOWN_HASHES.contains(accept.hash) || !KNOWN_HASHES.contains(accept.hash)
|| !KNOWN_MACS.contains(accept.messageAuthenticationCode) || !KNOWN_MACS.contains(accept.messageAuthenticationCode)
|| accept.shortAuthenticationStrings.intersect(KNOWN_SHORT_CODES).isEmpty()) { || 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) cancel(CancelCode.UnknownMethod)
return return
} }

View File

@ -117,6 +117,7 @@ internal class VerificationTransportToDevice(
onDone: (() -> Unit)?) { onDone: (() -> Unit)?) {
Timber.d("## SAS sending msg type $type") Timber.d("## SAS sending msg type $type")
Timber.v("## SAS sending msg info $verificationInfo") Timber.v("## SAS sending msg info $verificationInfo")
val stateBeforeCall = tx?.state
val tx = tx ?: return val tx = tx ?: return
val contentMap = MXUsersDevicesMap<Any>() val contentMap = MXUsersDevicesMap<Any>()
val toSendToDeviceObject = verificationInfo.toSendToDeviceObject() val toSendToDeviceObject = verificationInfo.toSendToDeviceObject()
@ -132,9 +133,13 @@ internal class VerificationTransportToDevice(
if (onDone != null) { if (onDone != null) {
onDone() onDone()
} else { } else {
// 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 tx.state = nextState
} }
} }
}
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
Timber.e("## SAS verification [$tx.transactionId] failed to send toDevice in state : $tx.state") Timber.e("## SAS verification [$tx.transactionId] failed to send toDevice in state : $tx.state")

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.network package im.vector.matrix.android.internal.network
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.shouldBeRetried
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
@ -46,7 +47,7 @@ internal class Request<DATA>(private val eventBus: EventBus?) {
throw response.toFailure(eventBus) throw response.toFailure(eventBus)
} }
} catch (exception: Throwable) { } catch (exception: Throwable) {
if (isRetryable && currentRetryCount++ < maxRetryCount && exception is IOException) { if (isRetryable && currentRetryCount++ < maxRetryCount && exception.shouldBeRetried()) {
delay(currentDelay) delay(currentDelay)
currentDelay = (currentDelay * 2L).coerceAtMost(maxDelay) currentDelay = (currentDelay * 2L).coerceAtMost(maxDelay)
return execute() return execute()

View File

@ -16,9 +16,7 @@
package im.vector.matrix.android.internal.session.account package im.vector.matrix.android.internal.session.account
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.toRegistrationFlowResponse
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
@ -45,31 +43,20 @@ internal class DefaultChangePasswordTask @Inject constructor(
apiCall = accountAPI.changePassword(changePasswordParams) apiCall = accountAPI.changePassword(changePasswordParams)
} }
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
if (throwable is Failure.OtherServerError val registrationFlowResponse = throwable.toRegistrationFlowResponse()
&& throwable.httpCode == 401
if (registrationFlowResponse != null
/* Avoid infinite loop */ /* Avoid infinite loop */
&& changePasswordParams.auth?.session == null) { && changePasswordParams.auth?.session == null) {
try {
MoshiProvider.providesMoshi()
.adapter(RegistrationFlowResponse::class.java)
.fromJson(throwable.errorBody)
} catch (e: Exception) {
null
}?.let {
// Retry with authentication // Retry with authentication
try {
executeRequest<Unit>(eventBus) { executeRequest<Unit>(eventBus) {
apiCall = accountAPI.changePassword( apiCall = accountAPI.changePassword(
changePasswordParams.copy(auth = changePasswordParams.auth?.copy(session = it.session)) changePasswordParams.copy(auth = changePasswordParams.auth?.copy(session = registrationFlowResponse.session))
) )
} }
return } else {
} catch (failure: Throwable) {
throw failure
}
}
}
throw throwable throw throwable
} }
} }
} }
}

View File

@ -153,7 +153,7 @@ internal class RoomSummaryUpdater @Inject constructor(
if (updateMembers) { if (updateMembers) {
val otherRoomMembers = RoomMemberHelper(realm, roomId) val otherRoomMembers = RoomMemberHelper(realm, roomId)
.queryRoomMembersEvent() .queryActiveRoomMembersEvent()
.notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId) .notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId)
.findAll() .findAll()
.asSequence() .asSequence()
@ -162,15 +162,7 @@ internal class RoomSummaryUpdater @Inject constructor(
roomSummaryEntity.otherMemberIds.clear() roomSummaryEntity.otherMemberIds.clear()
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers) roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers)
if (roomSummaryEntity.isEncrypted) { if (roomSummaryEntity.isEncrypted) {
// The set of “all users” depends on the type of room: eventBus.post(SessionToCryptoRoomMembersUpdate(roomId, roomSummaryEntity.isDirect, roomSummaryEntity.otherMemberIds.toList() + userId))
// 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))
} }
} }
} }

View File

@ -53,8 +53,6 @@ internal class DefaultSyncTask @Inject constructor(
private suspend fun doSync(params: SyncTask.Params) { private suspend fun doSync(params: SyncTask.Params) {
Timber.v("Sync task started on Thread: ${Thread.currentThread().name}") 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<String, String>() val requestParams = HashMap<String, String>()
var timeout = 0L var timeout = 0L
@ -73,6 +71,9 @@ internal class DefaultSyncTask @Inject constructor(
initialSyncProgressService.endAll() initialSyncProgressService.endAll()
initialSyncProgressService.startTask(R.string.initial_sync_start_importing_account, 100) 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<SyncResponse>(eventBus) { val syncResponse = executeRequest<SyncResponse>(eventBus) {
apiCall = syncAPI.sync(requestParams) apiCall = syncAPI.sync(requestParams)
} }

View File

@ -4,24 +4,24 @@
<string name="summary_user_sent_image">%1$s küldött egy képet.</string> <string name="summary_user_sent_image">%1$s küldött egy képet.</string>
<string name="notice_room_invite_no_invitee">%s meghívója</string> <string name="notice_room_invite_no_invitee">%s meghívója</string>
<string name="notice_room_invite">%1$s meghívta %2$s -t</string> <string name="notice_room_invite">%1$s meghívta: %2$s</string>
<string name="notice_room_invite_you">%1$s meghívott</string> <string name="notice_room_invite_you">%1$s meghívott</string>
<string name="notice_room_join">%1$s csatlakozott</string> <string name="notice_room_join">%1$s csatlakozott</string>
<string name="notice_room_leave">%1$s kilépett</string> <string name="notice_room_leave">%1$s kilépett</string>
<string name="notice_room_reject">%1$s elutasította a meghívást</string> <string name="notice_room_reject">%1$s elutasította a meghívást</string>
<string name="notice_room_kick">%1$s kidobta %2$s -t</string> <string name="notice_room_kick">%1$s kidobta: %2$s</string>
<string name="notice_room_unban">%1$s feloldotta tiltását %2$s -nak/nek</string> <string name="notice_room_unban">%1$s feloldotta %2$s tiltását</string>
<string name="notice_room_ban">%1$s kitiltotta %2$s -t</string> <string name="notice_room_ban">%1$s kitiltotta: %2$s</string>
<string name="notice_room_withdraw">%1$s visszavonta %2$s\'s meghívását</string> <string name="notice_room_withdraw">%1$s visszavonta %2$s meghívását</string>
<string name="notice_avatar_url_changed">%1$s megváltoztatták a felhasználó képüket</string> <string name="notice_avatar_url_changed">%1$s megváltoztatta a profilképét</string>
<string name="notice_display_name_set">%1$s megváltoztatták a megjelenő nevüket erre: %2$s</string> <string name="notice_display_name_set">%1$s megváltoztatta a megjelenő nevét erre: %2$s</string>
<string name="notice_display_name_changed_from">%1$s megváltoztatták a megjelenő nevüket erről %2$s erre %3$s</string> <string name="notice_display_name_changed_from">%1$s megváltoztatta a megjelenítendő nevét erről: %2$s, erre: %3$s</string>
<string name="notice_display_name_removed">%1$s eltávolították a megjelenő nevüket (%2$s)</string> <string name="notice_display_name_removed">%1$s eltávolította a megjelenítendő nevét (%2$s)</string>
<string name="notice_room_topic_changed">%1$s megváltoztatta a témát erre: %2$s</string> <string name="notice_room_topic_changed">%1$s megváltoztatta a témát erre: %2$s</string>
<string name="notice_room_name_changed">%1$s megváltoztatta a szoba nevét erre: %2$s</string> <string name="notice_room_name_changed">%1$s megváltoztatta a szoba nevét erre: %2$s</string>
<string name="notice_placed_video_call">%s videóhívást kezdeményezett.</string> <string name="notice_placed_video_call">%s videóhívást kezdeményezett.</string>
<string name="notice_placed_voice_call">%s hanghívást kezdeményezett.</string> <string name="notice_placed_voice_call">%s hanghívást kezdeményezett.</string>
<string name="notice_answered_call">%s elfogadta a hívást.</string> <string name="notice_answered_call">%s fogadta a hívást.</string>
<string name="notice_ended_call">%s befejezte a hívást.</string> <string name="notice_ended_call">%s befejezte a hívást.</string>
<string name="notice_made_future_room_visibility">%1$s láthatóvá tette a jövőbeli előzményeket %2$s számára</string> <string name="notice_made_future_room_visibility">%1$s láthatóvá tette a jövőbeli előzményeket %2$s számára</string>
<string name="notice_room_visibility_invited">az összes szobatag, onnantól, hogy meg lettek hívva.</string> <string name="notice_room_visibility_invited">az összes szobatag, onnantól, hogy meg lettek hívva.</string>
@ -29,47 +29,47 @@
<string name="notice_room_visibility_shared">az összes szobatag.</string> <string name="notice_room_visibility_shared">az összes szobatag.</string>
<string name="notice_room_visibility_world_readable">bárki.</string> <string name="notice_room_visibility_world_readable">bárki.</string>
<string name="notice_room_visibility_unknown">ismeretlen (%s).</string> <string name="notice_room_visibility_unknown">ismeretlen (%s).</string>
<string name="notice_end_to_end">%1$s bekapcsolta a végtől végig titkosítást (%2$s)</string> <string name="notice_end_to_end">%1$s bekapcsolta a végpontok közötti titkosítást (%2$s)</string>
<string name="notice_requested_voip_conference">%1$s hanghívás konferenciát kérelmezett</string> <string name="notice_requested_voip_conference">%1$s hanghívás konferenciát kérelmezett</string>
<string name="notice_voip_started">Hanghívás konferencia elindult</string> <string name="notice_voip_started">Hanghívás konferencia elindult</string>
<string name="notice_voip_finished">Hanghívás konferencia befejeződött</string> <string name="notice_voip_finished">Hanghívás konferencia befejeződött</string>
<string name="notice_avatar_changed_too">(profilképp is meg lett változtatva)</string> <string name="notice_avatar_changed_too">(a profilkép is megváltozott)</string>
<string name="notice_room_name_removed">%1$s eltávolította a szoba nevét</string> <string name="notice_room_name_removed">%1$s eltávolította a szoba nevét</string>
<string name="notice_room_topic_removed">%1$s eltávolította a szoba témáját</string> <string name="notice_room_topic_removed">%1$s eltávolította a szoba témáját</string>
<string name="notice_profile_change_redacted">%1$s megváltoztatták a profiljukat %2$s</string> <string name="notice_profile_change_redacted">%1$s megváltoztatta a(z) %2$s profilját</string>
<string name="notice_room_third_party_invite">"%1$s meghívót küldött %2$s -nak/-nek hogy csatlakozzon a szobához"</string> <string name="notice_room_third_party_invite">%1$s meghívót küldött %2$s számára, hogy csatlakozzon a szobához</string>
<string name="notice_room_third_party_registered_invite">%1$s elfogadta a meghívót a %2$s -hoz</string> <string name="notice_room_third_party_registered_invite">%1$s elfogadta a meghívót ebbe: %2$s</string>
<string name="notice_crypto_unable_to_decrypt">** Visszafejtés sikertelen: %s **</string> <string name="notice_crypto_unable_to_decrypt">** Visszafejtés sikertelen: %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">A küldő eszköze nem küldte el a kulcsokat ehhez az üzenethez.</string> <string name="notice_crypto_error_unkwown_inbound_session_id">A küldő eszköze nem küldte el a kulcsokat ehhez az üzenethez.</string>
<string name="could_not_redact">Szerkesztés sikertelen</string> <string name="could_not_redact">Kitakarás sikertelen</string>
<string name="unable_to_send_message">Üzenet küldése sikertelen</string> <string name="unable_to_send_message">Üzenet küldése sikertelen</string>
<string name="message_failed_to_upload">Kép feltöltése sikertelen</string> <string name="message_failed_to_upload">Kép feltöltése sikertelen</string>
<string name="network_error">Hálózat hiba</string> <string name="network_error">Hálózati hiba</string>
<string name="matrix_error">Matrix hiba</string> <string name="matrix_error">Matrix hiba</string>
<string name="room_error_join_failed_empty_room">Jelenleg nem lehetséges újracsatlakozni egy üres szobába.</string> <string name="room_error_join_failed_empty_room">Jelenleg nem lehetséges újracsatlakozni egy üres szobához.</string>
<string name="encrypted_message">Titkosított üzenet</string> <string name="encrypted_message">Titkosított üzenet</string>
<string name="medium_email">Email cím</string> <string name="medium_email">E-mail cím</string>
<string name="medium_phone_number">Telefonszám</string> <string name="medium_phone_number">Telefonszám</string>
<string name="summary_user_sent_sticker">%1$s küldött egy matricát.</string> <string name="summary_user_sent_sticker">%1$s küldött egy matricát.</string>
<string name="message_reply_to_prefix">Válasz erre:</string> <string name="message_reply_to_prefix">Válasz erre:</string>
<string name="reply_to_an_image">kép elküldve.</string> <string name="reply_to_an_image">képet küldött.</string>
<string name="reply_to_a_video">videó elküldve.</string> <string name="reply_to_a_video">videót küldött.</string>
<string name="reply_to_an_audio_file">hangfájl elküldve.</string> <string name="reply_to_an_audio_file">hangfájlt küldött.</string>
<string name="reply_to_a_file">fájl elküldve.</string> <string name="reply_to_a_file">fájlt küldött.</string>
<string name="room_displayname_invite_from">%s meghívott</string> <string name="room_displayname_invite_from">Meghívó tőle: %s</string>
<string name="room_displayname_room_invite">Meghívó egy szobába</string> <string name="room_displayname_room_invite">Meghívó egy szobába</string>
<string name="room_displayname_two_members">%1$s és %2$s</string> <string name="room_displayname_two_members">%1$s és %2$s</string>
<string name="room_displayname_empty_room">Üres szoba</string> <string name="room_displayname_empty_room">Üres szoba</string>
@ -171,7 +171,7 @@
<string name="event_status_sending_message">Üzenet küldése…</string> <string name="event_status_sending_message">Üzenet küldése…</string>
<string name="clear_timeline_send_queue">Küldő sor ürítése</string> <string name="clear_timeline_send_queue">Küldő sor ürítése</string>
<string name="notice_room_third_party_revoked_invite">%1$s visszavonta a meghívót a belépéshez ebbe a szobába: %2$s</string> <string name="notice_room_third_party_revoked_invite">%1$s visszavonta %2$s meghívását, hogy csatlakozzon a szobához</string>
<string name="notice_room_invite_no_invitee_with_reason">%1$s meghívója. Ok: %2$s</string> <string name="notice_room_invite_no_invitee_with_reason">%1$s meghívója. Ok: %2$s</string>
<string name="notice_room_invite_with_reason">%1$s meghívta őt: %2$s. Ok: %3$s</string> <string name="notice_room_invite_with_reason">%1$s meghívta őt: %2$s. Ok: %3$s</string>
<string name="notice_room_invite_you_with_reason">%1$s meghívott. Ok: %2$s</string> <string name="notice_room_invite_you_with_reason">%1$s meghívott. Ok: %2$s</string>

View File

@ -170,4 +170,39 @@
<string name="notice_room_third_party_revoked_invite">%1$s 撤回了对 %2$s 邀请</string> <string name="notice_room_third_party_revoked_invite">%1$s 撤回了对 %2$s 邀请</string>
<string name="verification_emoji_pin">置顶</string> <string name="verification_emoji_pin">置顶</string>
<string name="notice_room_invite_no_invitee_with_reason">%1$s 的邀请。理由:%2$s</string>
<string name="notice_room_invite_with_reason">%1$s 邀请了 %2$s。理由%3$s</string>
<string name="notice_room_invite_you_with_reason">%1$s 邀请了您。理由:%2$s</string>
<string name="notice_room_join_with_reason">%1$s 已加入。理由:%2$s</string>
<string name="notice_room_leave_with_reason">%1$s 已离开。理由:%2$s</string>
<string name="notice_room_reject_with_reason">%1$s 已拒绝邀请。理由:%2$s</string>
<string name="notice_room_kick_with_reason">%1$s 踢走了 %2$s。理由%3$s</string>
<string name="notice_room_unban_with_reason">%1$s 取消封锁了 %2$s。理由%3$s</string>
<string name="notice_room_ban_with_reason">%1$s 封锁了 %2$s。理由%3$s</string>
<string name="notice_room_third_party_invite_with_reason">%1$s 已发送邀请给 %2$s 来加入聊天室。理由:%3$s</string>
<string name="notice_room_third_party_revoked_invite_with_reason">%1$s 撤销了 %2$s 加入聊天室的邀請。理由:%3$s</string>
<string name="notice_room_third_party_registered_invite_with_reason">%1$s 接受 %2$s 的邀請。理由:%3$s</string>
<string name="notice_room_withdraw_with_reason">%1$s 撤回了对 %2$s 的邀请。理由:%3$s</string>
<plurals name="notice_room_aliases_added">
<item quantity="other">%1$s 新增了 %2$s 为此聊天室的地址。</item>
</plurals>
<plurals name="notice_room_aliases_removed">
<item quantity="other">%1$s 移除了此聊天室的 %3$s 地址。</item>
</plurals>
<string name="notice_room_aliases_added_and_removed">%1$s 为此聊天室新增 %2$s 并移除 %3$s 地址。</string>
<string name="notice_room_canonical_alias_set">%1$s 为此聊天室设定了 %2$s 为主地址。</string>
<string name="notice_room_canonical_alias_unset">%1$s 为此聊天室移除了主要地址。</string>
<string name="notice_room_guest_access_can_join">%1$s 已允许访客加入聊天室。</string>
<string name="notice_room_guest_access_forbidden">%1$s 已禁止访客加入聊天室。</string>
<string name="notice_end_to_end_ok">%1$s 已开启端到端加密。</string>
<string name="notice_end_to_end_unknown_algorithm">%1$s 已开启端到端加密(无法识别的演算法 %2$s</string>
<string name="key_verification_request_fallback_message">%s 正在请求验证您的密钥,但您的客户端不支援聊天中密钥验证。 您将必须使用旧版的密钥验证来验证金钥。</string>
</resources> </resources>

View File

@ -18,7 +18,7 @@ package im.vector.matrix.android.test.shared
import net.lachlanmckee.timberjunit.TimberTestRule import net.lachlanmckee.timberjunit.TimberTestRule
fun createTimberTestRule(): TimberTestRule { internal fun createTimberTestRule(): TimberTestRule {
return TimberTestRule.builder() return TimberTestRule.builder()
.showThread(false) .showThread(false)
.showTimestamp(false) .showTimestamp(false)

View File

@ -0,0 +1,6 @@
<?xml version="1.0"?>
<globals>
<#include "root://activities/common/common_globals.xml.ftl" />
<global id="resOut" value="${resDir}" />
<global id="srcOut" value="${srcDir}/${slashedPackageName(packageName)}" />
</globals>

View File

@ -0,0 +1,37 @@
<?xml version="1.0"?>
<#import "root://activities/common/kotlin_macros.ftl" as kt>
<recipe>
<instantiate from="root/res/layout/fragment.xml.ftl"
to="${escapeXmlAttribute(resOut)}/layout/${escapeXmlAttribute(fragmentLayout)}.xml" />
<open file="${escapeXmlAttribute(resOut)}/layout/${fragmentLayout}.xml" />
<#if createActivity>
<instantiate from="root/src/app_package/Activity.kt.ftl"
to="${escapeXmlAttribute(srcOut)}/${activityClass}.kt" />
<open file="${escapeXmlAttribute(srcOut)}/${activityClass}.kt" />
</#if>
<instantiate from="root/src/app_package/Fragment.kt.ftl"
to="${escapeXmlAttribute(srcOut)}/${fragmentClass}.kt" />
<open file="${escapeXmlAttribute(srcOut)}/${fragmentClass}.kt" />
<instantiate from="root/src/app_package/ViewModel.kt.ftl"
to="${escapeXmlAttribute(srcOut)}/${viewModelClass}.kt" />
<open file="${escapeXmlAttribute(srcOut)}/${viewModelClass}.kt" />
<instantiate from="root/src/app_package/ViewState.kt.ftl"
to="${escapeXmlAttribute(srcOut)}/${viewStateClass}.kt" />
<open file="${escapeXmlAttribute(srcOut)}/${viewStateClass}.kt" />
<instantiate from="root/src/app_package/Action.kt.ftl"
to="${escapeXmlAttribute(srcOut)}/${actionClass}.kt" />
<open file="${escapeXmlAttribute(srcOut)}/${actionClass}.kt" />
<#if createViewEvents>
<instantiate from="root/src/app_package/ViewEvents.kt.ftl"
to="${escapeXmlAttribute(srcOut)}/${viewEventsClass}.kt" />
<open file="${escapeXmlAttribute(srcOut)}/${viewEventsClass}.kt" />
</#if>
</recipe>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/rootConstraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="${fragmentClass}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,5 @@
package ${escapeKotlinIdentifiers(packageName)}
import im.vector.riotx.core.platform.VectorViewModelAction
sealed class ${actionClass}: VectorViewModelAction

View File

@ -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)
}
</#if>
}
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)
</#if>
}
}
override fun configure(toolbar: Toolbar) {
configureToolbar(toolbar)
}
}

View File

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

View File

@ -0,0 +1,5 @@
package ${escapeKotlinIdentifiers(packageName)}
import im.vector.riotx.core.platform.VectorViewEvents
sealed class ${viewEventsClass} : VectorViewEvents

View File

@ -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
</#if>
class ${viewModelClass} @AssistedInject constructor(@Assisted initialState: ${viewStateClass})
<#if createViewEvents>
: VectorViewModel<${viewStateClass}, ${actionClass}, ${viewEventsClass}>(initialState) {
<#else>
: VectorViewModel<${viewStateClass}, ${actionClass}, EmptyViewEvents>(initialState) {
</#if>
@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
}
}

View File

@ -0,0 +1,5 @@
package ${escapeKotlinIdentifiers(packageName)}
import com.airbnb.mvrx.MvRxState
data class ${viewStateClass}() : MvRxState

View File

@ -0,0 +1,121 @@
<?xml version="1.0"?>
<template
format="5"
revision="1"
name="RiotX Feature"
minApi="19"
minBuildApi="19"
description="Creates a new activity and a fragment with view model, view state and actions">
<category value="New Vector" />
<formfactor value="Mobile" />
<parameter
id="createActivity"
name="Create host activity"
type="boolean"
default="true"
help="If true, you will have a host activity" />
<parameter
id="activityClass"
name="Activity Name"
type="string"
constraints="class|unique|nonempty"
visibility="createActivity"
default="MainActivity"
help="The name of the activity class to create" />
<parameter
id="fragmentClass"
name="Fragment Name"
type="string"
constraints="class|unique|nonempty"
suggest="${underscoreToCamelCase(classToResource(activityClass))}Fragment"
default="MainFragment"
help="The name of the fragment class to create" />
<parameter
id="createFragmentArgs"
name="Create fragment Args"
type="boolean"
default="false"
help="If true, you will have a fragment args" />
<parameter
id="fragmentArgsClass"
name="Fragment Args"
type="string"
constraints="class|unique|nonempty"
visibility="createFragmentArgs"
suggest="${underscoreToCamelCase(classToResource(fragmentClass))}Args"
default="MainArgs"
help="The name of the fragment args to create" />
<parameter
id="fragmentLayout"
name="Fragment Layout Name"
type="string"
constraints="layout|unique|nonempty"
suggest="fragment_${classToResource(fragmentClass)}"
default="main_fragment"
help="The name of the layout to create for the fragment" />
<parameter
id="viewModelClass"
name="ViewModel Name"
type="string"
constraints="class|unique|nonempty"
suggest="${underscoreToCamelCase(classToResource(fragmentClass))}ViewModel"
default="MainViewModel"
help="The name of the view model class to create" />
<parameter
id="actionClass"
name="Action Name"
type="string"
constraints="class|unique|nonempty"
suggest="${underscoreToCamelCase(classToResource(fragmentClass))}Action"
default="MainAction"
help="The name of the action class to create" />
<parameter
id="viewStateClass"
name="ViewState Name"
type="string"
constraints="class|unique|nonempty"
suggest="${underscoreToCamelCase(classToResource(fragmentClass))}ViewState"
default="MainViewState"
help="The name of the ViewState class to create" />
<parameter
id="createViewEvents"
name="Create ViewEvents"
type="boolean"
default="false"
help="If true, you will have a view events" />
<parameter
id="viewEventsClass"
name="ViewEvents Class"
type="string"
constraints="class|unique|nonempty"
visibility="createViewEvents"
suggest="${underscoreToCamelCase(classToResource(fragmentClass))}ViewEvents"
default="MainViewEvents"
help="The name of the view events to create" />
<parameter
id="packageName"
name="Package name"
type="string"
constraints="package"
default="com.mycompany.myapp" />
<globals file="globals.xml.ftl" />
<execute file="recipe.xml.ftl" />
</template>

24
tools/templates/configure.sh Executable file
View File

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

View File

@ -235,6 +235,15 @@ android {
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = "1.8"
} }
sourceSets {
androidTest {
java.srcDirs += "src/sharedTest/java"
}
test {
java.srcDirs += "src/sharedTest/java"
}
}
} }
dependencies { dependencies {
@ -250,6 +259,7 @@ dependencies {
def daggerVersion = '2.25.4' def daggerVersion = '2.25.4'
def autofill_version = "1.0.0" def autofill_version = "1.0.0"
def work_version = '2.3.3' def work_version = '2.3.3'
def arch_version = '2.1.0'
implementation project(":matrix-sdk-android") implementation project(":matrix-sdk-android")
implementation project(":matrix-sdk-android-rx") implementation project(":matrix-sdk-android-rx")
@ -378,10 +388,18 @@ dependencies {
// TESTS // TESTS
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
testImplementation 'org.amshove.kluent:kluent-android:1.44' 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: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 'androidx.test.espresso:espresso-core:3.2.0'
androidTestImplementation 'org.amshove.kluent:kluent-android:1.44' 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")) { if (getGradle().getStartParameter().getTaskRequests().toString().contains("Gplay")) {

View File

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

View File

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

View File

@ -0,0 +1,52 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.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<TextView>(R.id.encrypted_device_info_device_name)?.let {
it.text = cryptoDeviceInfo.displayName()
}
dialogLayout.findViewById<TextView>(R.id.encrypted_device_info_device_id)?.let {
it.text = cryptoDeviceInfo.deviceId
}
dialogLayout.findViewById<TextView>(R.id.encrypted_device_info_device_key)?.let {
it.text = cryptoDeviceInfo.getFingerprintHumanReadable()
}
builder.show()
}
}

View File

@ -30,6 +30,10 @@ import io.reactivex.Single
abstract class VectorViewModel<S : MvRxState, VA : VectorViewModelAction, VE : VectorViewEvents>(initialState: S) abstract class VectorViewModel<S : MvRxState, VA : VectorViewModelAction, VE : VectorViewEvents>(initialState: S)
: BaseMvRxViewModel<S>(initialState, false) { : BaseMvRxViewModel<S>(initialState, false) {
interface Factory<S: MvRxState> {
fun create(state: S): BaseMvRxViewModel<S>
}
// Used to post transient events to the View // Used to post transient events to the View
protected val _viewEvents = PublishDataSource<VE>() protected val _viewEvents = PublishDataSource<VE>()
val viewEvents: DataSource<VE> = _viewEvents val viewEvents: DataSource<VE> = _viewEvents

View File

@ -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<GenericButtonItem.Holder>() {
@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<MaterialButton>(R.id.itemGenericItemButton)
}
}

View File

@ -34,8 +34,10 @@ import im.vector.riotx.core.utils.deleteAllFiles
import im.vector.riotx.features.home.HomeActivity import im.vector.riotx.features.home.HomeActivity
import im.vector.riotx.features.login.LoginActivity import im.vector.riotx.features.login.LoginActivity
import im.vector.riotx.features.notifications.NotificationDrawerManager 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.hard.SignedOutActivity
import im.vector.riotx.features.signout.soft.SoftLogoutActivity import im.vector.riotx.features.signout.soft.SoftLogoutActivity
import im.vector.riotx.features.ui.UiStateRepository
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@ -78,6 +80,8 @@ class MainActivity : VectorBaseActivity() {
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var notificationDrawerManager: NotificationDrawerManager
@Inject lateinit var sessionHolder: ActiveSessionHolder @Inject lateinit var sessionHolder: ActiveSessionHolder
@Inject lateinit var errorFormatter: ErrorFormatter @Inject lateinit var errorFormatter: ErrorFormatter
@Inject lateinit var vectorPreferences: VectorPreferences
@Inject lateinit var uiStateRepository: UiStateRepository
override fun injectWith(injector: ScreenComponent) { override fun injectWith(injector: ScreenComponent) {
injector.inject(this) injector.inject(this)
@ -127,7 +131,7 @@ class MainActivity : VectorBaseActivity() {
// Just do the local cleanup // Just do the local cleanup
Timber.w("Account deactivated, start app") Timber.w("Account deactivated, start app")
sessionHolder.clearActiveSession() sessionHolder.clearActiveSession()
doLocalCleanup() doLocalCleanup(clearPreferences = true)
startNextActivityAndFinish() startNextActivityAndFinish()
} }
args.clearCredentials -> session.signOut( args.clearCredentials -> session.signOut(
@ -136,7 +140,7 @@ class MainActivity : VectorBaseActivity() {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
Timber.w("SIGN_OUT: success, start app") Timber.w("SIGN_OUT: success, start app")
sessionHolder.clearActiveSession() sessionHolder.clearActiveSession()
doLocalCleanup() doLocalCleanup(clearPreferences = true)
startNextActivityAndFinish() startNextActivityAndFinish()
} }
@ -147,7 +151,7 @@ class MainActivity : VectorBaseActivity() {
args.clearCache -> session.clearCache( args.clearCache -> session.clearCache(
object : MatrixCallback<Unit> { object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
doLocalCleanup() doLocalCleanup(clearPreferences = false)
session.startSyncing(applicationContext) session.startSyncing(applicationContext)
startNextActivityAndFinish() startNextActivityAndFinish()
} }
@ -164,10 +168,15 @@ class MainActivity : VectorBaseActivity() {
Timber.w("Ignoring invalid token global error") Timber.w("Ignoring invalid token global error")
} }
private fun doLocalCleanup() { private fun doLocalCleanup(clearPreferences: Boolean) {
GlobalScope.launch(Dispatchers.Main) { GlobalScope.launch(Dispatchers.Main) {
// On UI Thread // On UI Thread
Glide.get(this@MainActivity).clearMemory() Glide.get(this@MainActivity).clearMemory()
if (clearPreferences) {
vectorPreferences.clearPreferences()
uiStateRepository.reset()
}
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
// On BG thread // On BG thread
Glide.get(this@MainActivity).clearDiskCache() Glide.get(this@MainActivity).clearDiskCache()

View File

@ -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.VerificationService
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.IncomingRequestCancellation 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.IncomingSecretShareRequest
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel 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.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap 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.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.features.popup.DefaultVectorAlert import im.vector.riotx.features.popup.DefaultVectorAlert
import im.vector.riotx.features.popup.PopupAlertManager import im.vector.riotx.features.popup.PopupAlertManager
@ -124,19 +123,11 @@ class KeyRequestHandler @Inject constructor(private val context: Context, privat
deviceInfo.trustLevel = DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false) deviceInfo.trustLevel = DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false)
// can we get more info on this device? // can we get more info on this device?
session?.cryptoService()?.getDevicesList(object : MatrixCallback<DevicesListResponse> { session?.cryptoService()?.getMyDevicesInfo()?.firstOrNull { it.deviceId == deviceId }?.let {
override fun onSuccess(data: DevicesListResponse) {
data.devices?.find { it.deviceId == deviceId }?.let {
postAlert(context, userId, deviceId, true, deviceInfo, it) postAlert(context, userId, deviceId, true, deviceInfo, it)
} ?: run { } ?: kotlin.run {
postAlert(context, userId, deviceId, true, deviceInfo) postAlert(context, userId, deviceId, true, deviceInfo)
} }
}
override fun onFailure(failure: Throwable) {
postAlert(context, userId, deviceId, true, deviceInfo)
}
})
} else { } else {
postAlert(context, userId, deviceId, false, deviceInfo) postAlert(context, userId, deviceId, false, deviceInfo)
} }

View File

@ -95,6 +95,14 @@ class BootstrapConfirmPassphraseFragment @Inject constructor(
sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility)
} }
.disposeOnDestroyView() .disposeOnDestroyView()
bootstrapSubmit.clicks()
.debounce(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
submit()
}
.disposeOnDestroyView()
} }
private fun submit() = withState(sharedViewModel) { state -> private fun submit() = withState(sharedViewModel) { state ->
@ -113,8 +121,6 @@ class BootstrapConfirmPassphraseFragment @Inject constructor(
} }
override fun invalidate() = withState(sharedViewModel) { state -> override fun invalidate() = withState(sharedViewModel) { state ->
super.invalidate()
if (state.step is BootstrapStep.ConfirmPassphrase) { if (state.step is BootstrapStep.ConfirmPassphrase) {
val isPasswordVisible = state.step.isPasswordVisible val isPasswordVisible = state.step.isPasswordVisible
ssss_passphrase_enter_edittext.showPassword(isPasswordVisible, updateCursor = false) ssss_passphrase_enter_edittext.showPassword(isPasswordVisible, updateCursor = false)

View File

@ -18,7 +18,9 @@ package im.vector.riotx.features.crypto.recover
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError 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.Session
import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
@ -27,11 +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.SsssKeyCreationInfo
import im.vector.matrix.android.api.session.securestorage.SsssKeySpec 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.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.MegolmBackupCreationInfo
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion 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.crypto.model.rest.UserPasswordAuth
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.util.awaitCallback import im.vector.matrix.android.internal.util.awaitCallback
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.platform.ViewModelTask import im.vector.riotx.core.platform.ViewModelTask
@ -206,6 +208,16 @@ class BootstrapCrossSigningTask @Inject constructor(
} }
// Save it for gossiping // Save it for gossiping
session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version) session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version)
awaitCallback<Unit> {
extractCurveKeyFromRecoveryKey(creationInfo.recoveryKey)?.toBase64NoPadding()?.let { secret ->
ssssService.storeSecret(
KEYBACKUP_SECRET_SSSS_NAME,
secret,
listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)), it
)
}
}
} }
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.e("## BootstrapCrossSigningTask: Failed to init keybackup") Timber.e("## BootstrapCrossSigningTask: Failed to init keybackup")
@ -217,15 +229,10 @@ class BootstrapCrossSigningTask @Inject constructor(
private fun handleInitializeXSigningError(failure: Throwable): BootstrapResult { private fun handleInitializeXSigningError(failure: Throwable): BootstrapResult {
if (failure is Failure.ServerError && failure.error.code == MatrixError.M_FORBIDDEN) { if (failure is Failure.ServerError && failure.error.code == MatrixError.M_FORBIDDEN) {
return BootstrapResult.InvalidPasswordError(failure.error) return BootstrapResult.InvalidPasswordError(failure.error)
} else if (failure is Failure.OtherServerError && failure.httpCode == 401) { } else {
try { val registrationFlowResponse = failure.toRegistrationFlowResponse()
MoshiProvider.providesMoshi() if (registrationFlowResponse != null) {
.adapter(RegistrationFlowResponse::class.java) if (registrationFlowResponse.flows?.any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true } != true) {
.fromJson(failure.errorBody)
} catch (e: Exception) {
null
}?.let { flowResponse ->
if (flowResponse.flows?.any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true } != true) {
// can't do this from here // can't do this from here
return BootstrapResult.UnsupportedAuthFlow() return BootstrapResult.UnsupportedAuthFlow()
} }

View File

@ -90,6 +90,14 @@ class BootstrapEnterPassphraseFragment @Inject constructor(
sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility)
} }
.disposeOnDestroyView() .disposeOnDestroyView()
bootstrapSubmit.clicks()
.debounce(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
submit()
}
.disposeOnDestroyView()
} }
private fun submit() = withState(sharedViewModel) { state -> private fun submit() = withState(sharedViewModel) { state ->
@ -108,8 +116,6 @@ class BootstrapEnterPassphraseFragment @Inject constructor(
} }
override fun invalidate() = withState(sharedViewModel) { state -> override fun invalidate() = withState(sharedViewModel) { state ->
super.invalidate()
if (state.step is BootstrapStep.SetupPassphrase) { if (state.step is BootstrapStep.SetupPassphrase) {
val isPasswordVisible = state.step.isPasswordVisible val isPasswordVisible = state.step.isPasswordVisible
ssss_passphrase_enter_edittext.showPassword(isPasswordVisible, updateCursor = false) ssss_passphrase_enter_edittext.showPassword(isPasswordVisible, updateCursor = false)

View File

@ -66,7 +66,7 @@ class IncomingVerificationRequestHandler @Inject constructor(
uid, uid,
context.getString(R.string.sas_incoming_request_notif_title), context.getString(R.string.sas_incoming_request_notif_title),
context.getString(R.string.sas_incoming_request_notif_content, name), context.getString(R.string.sas_incoming_request_notif_content, name),
R.drawable.shield, R.drawable.ic_shield_black,
shouldBeDisplayedIn = { activity -> shouldBeDisplayedIn = { activity ->
if (activity is VectorBaseActivity) { if (activity is VectorBaseActivity) {
// TODO a bit too hugly :/ // TODO a bit too hugly :/

View File

@ -165,7 +165,9 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
} else { } else {
otherUserShield.setImageResource(R.drawable.ic_shield_warning) 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 otherUserShield.isVisible = true
} else { } else {
avatarRenderer.render(matrixItem, otherUserAvatarImageView) avatarRenderer.render(matrixItem, otherUserAvatarImageView)

View File

@ -423,7 +423,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
} }
} catch (failure: Throwable) { } catch (failure: Throwable) {
// Just ignore for now // Just ignore for now
Timber.v("## Failed to restore backup after SSSS recovery") Timber.e(failure, "## Failed to restore backup after SSSS recovery")
} }
} }
} }

View File

@ -167,6 +167,8 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
val crossSigningEnabledOnAccount = myCrossSigningKeys != null val crossSigningEnabledOnAccount = myCrossSigningKeys != null
if (!crossSigningEnabledOnAccount && !sharedActionViewModel.isAccountCreation) { if (!crossSigningEnabledOnAccount && !sharedActionViewModel.isAccountCreation) {
// 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 // We need to ask
promptSecurityEvent( promptSecurityEvent(
session, session,
@ -175,12 +177,16 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
) { ) {
it.navigator.upgradeSessionSecurity(it) it.navigator.upgradeSessionSecurity(it)
} }
} else {
// Do not do it again
sharedActionViewModel.hasDisplayedCompleteSecurityPrompt = true
}
} else if (myCrossSigningKeys?.isTrusted() == false) { } else if (myCrossSigningKeys?.isTrusted() == false) {
// We need to ask // We need to ask
promptSecurityEvent( promptSecurityEvent(
session, 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) it.navigator.waitSessionVerification(it)
} }

View File

@ -1,4 +1,3 @@
/* /*
* Copyright 2019 New Vector Ltd * 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.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.util.toMatrixItem 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.R
import im.vector.riotx.core.extensions.commitTransactionNow import im.vector.riotx.core.extensions.commitTransactionNow
import im.vector.riotx.core.glide.GlideApp 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.home.room.list.UnreadCounterBadgeView
import im.vector.riotx.features.popup.PopupAlertManager import im.vector.riotx.features.popup.PopupAlertManager
import im.vector.riotx.features.popup.VerificationVectorAlert 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 im.vector.riotx.features.workers.signout.SignOutViewModel
import kotlinx.android.synthetic.main.fragment_home_detail.* import kotlinx.android.synthetic.main.fragment_home_detail.*
import timber.log.Timber import timber.log.Timber
@ -87,17 +88,27 @@ class HomeDetailFragment @Inject constructor(
switchDisplayMode(displayMode) switchDisplayMode(displayMode)
} }
unknownDeviceDetectorSharedViewModel.subscribe { unknownDeviceDetectorSharedViewModel.subscribe { state ->
it.unknownSessions.invoke()?.let { unknownDevices -> state.unknownSessions.invoke()?.let { unknownDevices ->
Timber.v("## Detector - ${unknownDevices.size} Unknown sessions") // Timber.v("## Detector Triggerred in fragment - ${unknownDevices.firstOrNull()}")
unknownDevices.forEachIndexed { index, deviceInfo -> if (unknownDevices.firstOrNull()?.currentSessionTrust == true) {
Timber.v("## Detector - #$index deviceId:${deviceInfo.second.deviceId} lastSeenTs:${deviceInfo.second.lastSeenTs}") val uid = "review_login"
}
val uid = "Newest_Device"
alertManager.cancelAlert(uid) alertManager.cancelAlert(uid)
if (it.canCrossSign && unknownDevices.isNotEmpty()) { val olderUnverified = unknownDevices.filter { !it.isNew }
val newest = unknownDevices.first().second val newest = unknownDevices.firstOrNull { it.isNew }?.deviceInfo
val user = unknownDevices.first().first 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( alertManager.postVectorAlert(
VerificationVectorAlert( VerificationVectorAlert(
uid = uid, uid = uid,
@ -111,13 +122,46 @@ class HomeDetailFragment @Inject constructor(
(weakCurrentActivity?.get() as? VectorBaseActivity) (weakCurrentActivity?.get() as? VectorBaseActivity)
?.navigator ?.navigator
?.requestSessionVerification(requireContext(), newest.deviceId ?: "") ?.requestSessionVerification(requireContext(), newest.deviceId ?: "")
unknownDeviceDetectorSharedViewModel.handle(
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) } ?: emptyList())
)
} }
dismissedAction = Runnable {} dismissedAction = Runnable {
} unknownDeviceDetectorSharedViewModel.handle(
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) } ?: emptyList())
) )
} }
} }
)
} }
private fun promptToReviewChanges(uid: String, state: UnknownDevicesState, oldUnverified: List<DeviceInfo>) {
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?) { private fun onGroupChange(groupSummary: GroupSummary?) {

View File

@ -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<List<DeviceDetectionInfo>> = 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<UnknownDevicesState, UnknownDeviceDetectorSharedViewModel.Action, EmptyViewEvents>(initialState) {
sealed class Action : VectorViewModelAction {
data class IgnoreDevice(val deviceIds: List<String>) : Action()
}
private val ignoredDeviceList = ArrayList<String>()
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<CryptoDeviceInfo>, List<DeviceInfo>, Optional<PrivateKeysInfo>, List<DeviceDetectionInfo>>(
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<UnknownDeviceDetectorSharedViewModel, UnknownDevicesState> {
override fun create(viewModelContext: ViewModelContext, state: UnknownDevicesState): UnknownDeviceDetectorSharedViewModel? {
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
return UnknownDeviceDetectorSharedViewModel(session, VectorPreferences(viewModelContext.activity()), state)
}
}
}

View File

@ -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<List<Pair<MatrixItem?, DeviceInfo>>> = Uninitialized,
val canCrossSign: Boolean = false
) : MvRxState
class UnknownDeviceDetectorSharedViewModel(session: Session, initialState: UnknownDevicesState)
: VectorViewModel<UnknownDevicesState, EmptyAction, EmptyViewEvents>(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<DevicesListResponse> {
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<UnknownDeviceDetectorSharedViewModel, UnknownDevicesState> {
override fun create(viewModelContext: ViewModelContext, state: UnknownDevicesState): UnknownDeviceDetectorSharedViewModel? {
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
return UnknownDeviceDetectorSharedViewModel(session, state)
}
}
}

View File

@ -957,8 +957,8 @@ class RoomDetailFragment @Inject constructor(
.setMessage( .setMessage(
getString(R.string.external_link_confirmation_message, title, url) getString(R.string.external_link_confirmation_message, title, url)
.toSpannable() .toSpannable()
.colorizeMatchingText(url, colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) .colorizeMatchingText(url, colorProvider.getColorFromAttribute(R.attr.riotx_text_primary_body_contrast))
.colorizeMatchingText(title, colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) .colorizeMatchingText(title, colorProvider.getColorFromAttribute(R.attr.riotx_text_primary_body_contrast))
) )
.setPositiveButton(R.string._continue) { _, _ -> .setPositiveButton(R.string._continue) { _, _ ->
openUrlInExternalBrowser(requireContext(), url) openUrlInExternalBrowser(requireContext(), url)

View File

@ -31,6 +31,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import im.vector.riotx.core.utils.isValidUrl
fun CharSequence.findPillsAndProcess(scope: CoroutineScope, processBlock: (PillImageSpan) -> Unit) { fun CharSequence.findPillsAndProcess(scope: CoroutineScope, processBlock: (PillImageSpan) -> Unit) {
scope.launch(Dispatchers.Main) { scope.launch(Dispatchers.Main) {
@ -59,14 +60,16 @@ fun CharSequence.linkify(callback: TimelineEventController.UrlClickCallback?): C
fun createLinkMovementMethod(urlClickCallback: TimelineEventController.UrlClickCallback?): EvenBetterLinkMovementMethod { fun createLinkMovementMethod(urlClickCallback: TimelineEventController.UrlClickCallback?): EvenBetterLinkMovementMethod {
return EvenBetterLinkMovementMethod(object : EvenBetterLinkMovementMethod.OnLinkClickListener { return EvenBetterLinkMovementMethod(object : EvenBetterLinkMovementMethod.OnLinkClickListener {
override fun onLinkClicked(textView: TextView, span: ClickableSpan, url: String, actualText: String): Boolean { 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 { .apply {
// We need also to fix the case when long click on link will trigger long click on cell // We need also to fix the case when long click on link will trigger long click on cell
setOnLinkLongClickListener { tv, url -> setOnLinkLongClickListener { tv, url ->
// Long clicks are handled by parent, return true to block android to do something with 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)) tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0))
true true
} else { } else {

View File

@ -31,9 +31,14 @@ import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.activityViewModel 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.api.auth.data.Credentials
import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.appendParamToUrl
import im.vector.riotx.core.utils.AssetReader import im.vector.riotx.core.utils.AssetReader
import im.vector.riotx.features.signout.soft.SoftLogoutAction import im.vector.riotx.features.signout.soft.SoftLogoutAction
import im.vector.riotx.features.signout.soft.SoftLogoutViewModel import im.vector.riotx.features.signout.soft.SoftLogoutViewModel
@ -123,14 +128,24 @@ class LoginWebFragment @Inject constructor(
val url = buildString { val url = buildString {
append(state.homeServerUrl?.trim { it == '/' }) append(state.homeServerUrl?.trim { it == '/' })
if (state.signMode == SignMode.SignIn) { 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 { state.deviceId?.takeIf { it.isNotBlank() }?.let {
// But https://github.com/matrix-org/synapse/issues/5755 // But https://github.com/matrix-org/synapse/issues/5755
append("?device_id=$it") appendParamToUrl("device_id", it)
} }
} else { } else {
// MODE_REGISTER // MODE_REGISTER
append("/_matrix/static/client/register/") append(REGISTER_FALLBACK_PATH)
} }
} }

View File

@ -96,7 +96,7 @@ class DefaultNavigator @Inject constructor(
roomId = null, roomId = null,
otherUserId = session.myUserId, otherUserId = session.myUserId,
transactionId = pr.transactionId transactionId = pr.transactionId
).show(context.supportFragmentManager, "REQPOP") ).show(context.supportFragmentManager, VerificationBottomSheet.WAITING_SELF_VERIF_TAG)
} }
} }

View File

@ -32,6 +32,23 @@ class EmojiDataSource @Inject constructor(
.adapter(EmojiData::class.java) .adapter(EmojiData::class.java)
.fromJson(input.bufferedReader().use { it.readText() }) .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<String, EmojiItem>().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()) ?: EmojiData(emptyList(), emptyMap(), emptyMap())
private val quickReactions = mutableListOf<EmojiItem>() private val quickReactions = mutableListOf<EmojiItem>()

View File

@ -101,7 +101,7 @@ class DeviceTrustInfoEpoxyController @Inject constructor(private val stringProvi
bottomSheetVerificationActionItem { bottomSheetVerificationActionItem {
id("verify") 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)) titleColor(colorProvider.getColor(R.color.riotx_accent))
iconRes(R.drawable.ic_arrow_right) iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_accent)) iconColor(colorProvider.getColor(R.color.riotx_accent))

View File

@ -24,6 +24,7 @@ import android.provider.MediaStore
import androidx.core.content.edit import androidx.core.content.edit
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.squareup.seismic.ShakeDetector import com.squareup.seismic.ShakeDetector
import im.vector.matrix.android.api.extensions.tryThis
import im.vector.riotx.BuildConfig import im.vector.riotx.BuildConfig
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.features.homeserver.ServerUrlsRepository 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_1_MONTH = 2
private const val MEDIA_SAVING_FOREVER = 3 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 // some preferences keys must be kept after a logout
private val mKeysToKeepAfterLogout = listOf( private val mKeysToKeepAfterLogout = listOf(
SETTINGS_DEFAULT_MEDIA_COMPRESSION_KEY, 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_TIMEOUT_PREFERENCE_KEY,
SETTINGS_SET_SYNC_DELAY_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_USE_RAGE_SHAKE_KEY,
SETTINGS_SECURITY_USE_FLAG_SECURE 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) return defaultPrefs.getBoolean(SETTINGS_PLAY_SHUTTER_SOUND_KEY, true)
} }
fun storeUnknownDeviceDismissedList(deviceIds: List<String>) {
defaultPrefs.edit(true) {
putStringSet(SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST, deviceIds.toSet())
}
}
fun getUnknownDeviceDismissedList(): List<String> {
return tryThis {
defaultPrefs.getStringSet(SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST, null)?.toList()
} ?: emptyList()
}
/** /**
* Update the notification ringtone * Update the notification ringtone
* *

View File

@ -26,6 +26,7 @@ import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.replaceFragment import im.vector.riotx.core.extensions.replaceFragment
import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment
import kotlinx.android.synthetic.main.activity_vector_settings.* import kotlinx.android.synthetic.main.activity_vector_settings.*
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -62,6 +63,11 @@ class VectorSettingsActivity : VectorBaseActivity(),
replaceFragment(R.id.vector_settings_page, VectorSettingsAdvancedSettingsFragment::class.java, null, FRAGMENT_TAG) 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) replaceFragment(R.id.vector_settings_page, VectorSettingsSecurityPrivacyFragment::class.java, null, FRAGMENT_TAG)
EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS ->
replaceFragment(R.id.vector_settings_page,
VectorSettingsDevicesFragment::class.java,
null,
FRAGMENT_TAG)
else -> else ->
replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG) 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_ROOT = 0
const val EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS = 1 const val EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS = 1
const val EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY = 2 const val EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY = 2
const val EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS = 3
private const val FRAGMENT_TAG = "VectorSettingsPreferencesFragment" private const val FRAGMENT_TAG = "VectorSettingsPreferencesFragment"
} }

View File

@ -412,7 +412,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
refreshCryptographyPreference(it) refreshCryptographyPreference(it)
} }
// TODO Move to a ViewModel... // TODO Move to a ViewModel...
session.cryptoService().getDevicesList(object : MatrixCallback<DevicesListResponse> { session.cryptoService().fetchDevicesList(object : MatrixCallback<DevicesListResponse> {
override fun onSuccess(data: DevicesListResponse) { override fun onSuccess(data: DevicesListResponse) {
if (isAdded) { if (isAdded) {
refreshCryptographyPreference(data.devices ?: emptyList()) refreshCryptographyPreference(data.devices ?: emptyList())

View File

@ -37,7 +37,6 @@ class CrossSigningEpoxyController @Inject constructor(
interface InteractionListener { interface InteractionListener {
fun onInitializeCrossSigningKeys() fun onInitializeCrossSigningKeys()
fun onResetCrossSigningKeys()
fun verifySession() fun verifySession()
} }
@ -51,18 +50,6 @@ class CrossSigningEpoxyController @Inject constructor(
titleIconResourceId(R.drawable.ic_shield_trusted) titleIconResourceId(R.drawable.ic_shield_trusted)
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_complete)) 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) { } else if (data.xSigningKeysAreTrusted) {
genericItem { genericItem {
id("trusted") id("trusted")
@ -70,22 +57,9 @@ class CrossSigningEpoxyController @Inject constructor(
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_trusted)) title(stringProvider.getString(R.string.encryption_information_dg_xsigning_trusted))
} }
if (!data.isUploadingKeys) { 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 { bottomSheetVerificationActionItem {
id("verify") 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)) titleColor(colorProvider.getColor(R.color.riotx_positive_accent))
iconRes(R.drawable.ic_arrow_right) iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_positive_accent)) iconColor(colorProvider.getColor(R.color.riotx_positive_accent))
@ -102,7 +76,7 @@ class CrossSigningEpoxyController @Inject constructor(
} }
bottomSheetVerificationActionItem { bottomSheetVerificationActionItem {
id("verify") 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)) titleColor(colorProvider.getColor(R.color.riotx_positive_accent))
iconRes(R.drawable.ic_arrow_right) iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_positive_accent)) iconColor(colorProvider.getColor(R.color.riotx_positive_accent))
@ -110,18 +84,6 @@ class CrossSigningEpoxyController @Inject constructor(
interactionListener?.verifySession() 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 { } else {
genericItem { genericItem {
id("not") id("not")
@ -130,7 +92,7 @@ class CrossSigningEpoxyController @Inject constructor(
if (vectorPreferences.developerMode() && !data.isUploadingKeys) { if (vectorPreferences.developerMode() && !data.isUploadingKeys) {
bottomSheetVerificationActionItem { bottomSheetVerificationActionItem {
id("initKeys") id("initKeys")
title("Initialize keys") title(stringProvider.getString(R.string.initialize_cross_signing))
titleColor(colorProvider.getColor(R.color.riotx_positive_accent)) titleColor(colorProvider.getColor(R.color.riotx_positive_accent))
iconRes(R.drawable.ic_arrow_right) iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_positive_accent)) iconColor(colorProvider.getColor(R.color.riotx_positive_accent))

View File

@ -101,14 +101,4 @@ class CrossSigningSettingsFragment @Inject constructor(
override fun verifySession() { override fun verifySession() {
viewModel.handle(CrossSigningAction.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()
}
} }

View File

@ -22,14 +22,12 @@ import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback 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.Session
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo 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.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.crosssigning.isVerified
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth 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.matrix.rx.rx
import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
@ -113,19 +111,12 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted privat
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
_pendingSession = null _pendingSession = null
if (failure is Failure.OtherServerError && failure.httpCode == 401) { val registrationFlowResponse = failure.toRegistrationFlowResponse()
try { if (registrationFlowResponse != null) {
MoshiProvider.providesMoshi()
.adapter(RegistrationFlowResponse::class.java)
.fromJson(failure.errorBody)
} catch (e: Exception) {
null
}?.let { flowResponse ->
// Retry with authentication // Retry with authentication
if (flowResponse.flows?.any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true } == true) { if (registrationFlowResponse.flows?.any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true } == true) {
_pendingSession = flowResponse.session ?: "" _pendingSession = registrationFlowResponse.session ?: ""
_viewEvents.post(CrossSigningSettingsViewEvents.RequestPassword) _viewEvents.post(CrossSigningSettingsViewEvents.RequestPassword)
return
} else { } else {
// can't do this from here // can't do this from here
_viewEvents.post(CrossSigningSettingsViewEvents.Failure(Throwable("You cannot do that from mobile"))) _viewEvents.post(CrossSigningSettingsViewEvents.Failure(Throwable("You cannot do that from mobile")))
@ -133,17 +124,15 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted privat
setState { setState {
copy(isUploadingKeys = false) copy(isUploadingKeys = false)
} }
return
} }
} } else {
}
_viewEvents.post(CrossSigningSettingsViewEvents.Failure(failure)) _viewEvents.post(CrossSigningSettingsViewEvents.Failure(failure))
setState { setState {
copy(isUploadingKeys = false) copy(isUploadingKeys = false)
} }
} }
}
}) })
} }

View File

@ -20,15 +20,17 @@ import android.graphics.Typeface
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass 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.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel 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.DateFormat
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
@ -53,21 +55,37 @@ abstract class DeviceItem : VectorEpoxyModel<DeviceItem.Holder>() {
var detailedMode = false var detailedMode = false
@EpoxyAttribute @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) { override fun bind(holder: Holder) {
holder.root.setOnClickListener { itemClickAction?.invoke() } holder.root.setOnClickListener { itemClickAction?.invoke() }
if (trusted != null) { val shield = TrustUtils.shieldForTrust(
holder.trustIcon.setImageDrawable( currentDevice,
ContextCompat.getDrawable( trustedSession,
holder.view.context, legacyMode,
if (trusted!!) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning trusted
) )
)
holder.trustIcon.isInvisible = false if (e2eCapable) {
holder.trustIcon.setImageResource(shield)
} else { } else {
holder.trustIcon.isInvisible = true holder.trustIcon.setImageDrawable(null)
} }
val detailedModeLabels = listOf( val detailedModeLabels = listOf(
@ -103,7 +121,28 @@ abstract class DeviceItem : VectorEpoxyModel<DeviceItem.Holder>() {
it.setTypeface(null, if (currentDevice) Typeface.BOLD else Typeface.NORMAL) it.setTypeface(null, if (currentDevice) Typeface.BOLD else Typeface.NORMAL)
} }
} else { } 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 holder.summaryLabelText.isVisible = true
detailedModeLabels.map { detailedModeLabels.map {
it.isVisible = false it.isVisible = false

View File

@ -16,17 +16,14 @@
package im.vector.riotx.features.settings.devices package im.vector.riotx.features.settings.devices
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo 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.model.rest.DeviceInfo
@ -37,7 +34,10 @@ import im.vector.riotx.core.platform.VectorViewModel
data class DeviceVerificationInfoBottomSheetViewState( data class DeviceVerificationInfoBottomSheetViewState(
val cryptoDeviceInfo: Async<CryptoDeviceInfo?> = Uninitialized, val cryptoDeviceInfo: Async<CryptoDeviceInfo?> = Uninitialized,
val deviceInfo: Async<DeviceInfo> = Uninitialized val deviceInfo: Async<DeviceInfo> = Uninitialized,
val hasAccountCrossSigning: Boolean = false,
val accountCrossSigningIsTrusted: Boolean = false,
val isMine: Boolean = false
) : MvRxState ) : MvRxState
class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: DeviceVerificationInfoBottomSheetViewState, class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: DeviceVerificationInfoBottomSheetViewState,
@ -51,31 +51,43 @@ class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@As
} }
init { 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) session.rx().liveUserCryptoDevices(session.myUserId)
.map { list -> .map { list ->
list.firstOrNull { it.deviceId == deviceId } list.firstOrNull { it.deviceId == deviceId }
} }
.execute { .execute {
copy( copy(
cryptoDeviceInfo = it cryptoDeviceInfo = it,
isMine = it.invoke()?.deviceId == session.sessionParams.credentials.deviceId
) )
} }
setState { setState {
copy(deviceInfo = Loading()) copy(deviceInfo = Loading())
} }
session.cryptoService().getDeviceInfo(deviceId, object : MatrixCallback<DeviceInfo> {
override fun onSuccess(data: DeviceInfo) {
setState {
copy(deviceInfo = Success(data))
}
}
override fun onFailure(failure: Throwable) { session.rx().liveMyDeviceInfo()
setState { .map { devices ->
copy(deviceInfo = Fail(failure)) devices.firstOrNull { it.deviceId == deviceId } ?: DeviceInfo(deviceId = deviceId)
} }
.execute {
copy(deviceInfo = it)
} }
})
} }
companion object : MvRxViewModelFactory<DeviceVerificationInfoBottomSheetViewModel, DeviceVerificationInfoBottomSheetViewState> { companion object : MvRxViewModelFactory<DeviceVerificationInfoBottomSheetViewModel, DeviceVerificationInfoBottomSheetViewState> {

View File

@ -16,7 +16,9 @@
package im.vector.riotx.features.settings.devices package im.vector.riotx.features.settings.devices
import com.airbnb.epoxy.TypedEpoxyController 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.api.session.Session
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.dividerItem import im.vector.riotx.core.epoxy.dividerItem
import im.vector.riotx.core.epoxy.loadingItem 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.genericFooterItem
import im.vector.riotx.core.ui.list.genericItem import im.vector.riotx.core.ui.list.genericItem
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class DeviceVerificationInfoEpoxyController @Inject constructor(private val stringProvider: StringProvider, class DeviceVerificationInfoEpoxyController @Inject constructor(private val stringProvider: StringProvider,
@ -37,37 +40,162 @@ class DeviceVerificationInfoEpoxyController @Inject constructor(private val stri
override fun buildModels(data: DeviceVerificationInfoBottomSheetViewState?) { override fun buildModels(data: DeviceVerificationInfoBottomSheetViewState?) {
val cryptoDeviceInfo = data?.cryptoDeviceInfo?.invoke() val cryptoDeviceInfo = data?.cryptoDeviceInfo?.invoke()
if (cryptoDeviceInfo != null) { when {
if (cryptoDeviceInfo.isVerified) { 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 { genericItem {
id("trust${cryptoDeviceInfo.deviceId}") id("trust${cryptoDeviceInfo.deviceId}")
style(GenericItem.STYLE.BIG_TEXT) 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}")
style(GenericItem.STYLE.BIG_TEXT)
titleIconResourceId(shield)
title(stringProvider.getString(R.string.crosssigning_verify_this_session))
description(stringProvider.getString(R.string.confirm_your_identity))
}
}
} else {
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)) title(stringProvider.getString(R.string.encryption_information_verified))
description(stringProvider.getString(R.string.settings_active_sessions_verified_device_desc)) description(stringProvider.getString(R.string.settings_active_sessions_verified_device_desc))
} }
} else { } else {
genericItem { genericItem {
id("trust${cryptoDeviceInfo.deviceId}") id("trust${cryptoDeviceInfo.deviceId}")
titleIconResourceId(R.drawable.ic_shield_warning) titleIconResourceId(shield)
style(GenericItem.STYLE.BIG_TEXT) style(GenericItem.STYLE.BIG_TEXT)
title(stringProvider.getString(R.string.encryption_information_not_verified)) title(stringProvider.getString(R.string.encryption_information_not_verified))
description(stringProvider.getString(R.string.settings_active_sessions_unverified_device_desc)) description(stringProvider.getString(R.string.settings_active_sessions_unverified_device_desc))
} }
} }
}
}
// DEVICE INFO SECTION
genericItem { genericItem {
id("info${cryptoDeviceInfo.deviceId}") id("info${cryptoDeviceInfo.deviceId}")
title(cryptoDeviceInfo.displayName() ?: "") title(cryptoDeviceInfo.displayName() ?: "")
description("(${cryptoDeviceInfo.deviceId})") description("(${cryptoDeviceInfo.deviceId})")
} }
if (!cryptoDeviceInfo.isVerified) { 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 { dividerItem {
id("d1") id("d1")
} }
bottomSheetVerificationActionItem { bottomSheetVerificationActionItem {
id("verify") id("verify${cryptoDeviceInfo.deviceId}")
title(stringProvider.getString(R.string.verification_verify_device)) title(stringProvider.getString(R.string.verification_verify_device))
titleColor(colorProvider.getColor(R.color.riotx_accent)) titleColor(colorProvider.getColor(R.color.riotx_accent))
iconRes(R.drawable.ic_arrow_right) iconRes(R.drawable.ic_arrow_right)
@ -77,11 +205,43 @@ class DeviceVerificationInfoEpoxyController @Inject constructor(private val stri
} }
} }
} }
}
if (cryptoDeviceInfo.deviceId != session.sessionParams.credentials.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 // Add the delete option
dividerItem { dividerItem {
id("d2") id("manageD1")
} }
bottomSheetVerificationActionItem { bottomSheetVerificationActionItem {
id("delete") id("delete")
@ -90,13 +250,14 @@ class DeviceVerificationInfoEpoxyController @Inject constructor(private val stri
iconRes(R.drawable.ic_arrow_right) iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_destructive_accent)) iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
listener { listener {
callback?.onAction(DevicesAction.Delete(cryptoDeviceInfo.deviceId)) callback?.onAction(DevicesAction.Delete(deviceId))
} }
} }
} }
// Always offer rename
dividerItem { dividerItem {
id("d3") id("manageD2")
} }
bottomSheetVerificationActionItem { bottomSheetVerificationActionItem {
id("rename") id("rename")
@ -105,43 +266,25 @@ class DeviceVerificationInfoEpoxyController @Inject constructor(private val stri
iconRes(R.drawable.ic_arrow_right) iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
listener { listener {
callback?.onAction(DevicesAction.PromptRename(cryptoDeviceInfo.deviceId)) callback?.onAction(DevicesAction.PromptRename(deviceId))
} }
} }
} else if (data?.deviceInfo?.invoke() != null) { }
val info = data.deviceInfo.invoke()
private fun handleNonE2EDevice(data: DeviceVerificationInfoBottomSheetViewState) {
val info = data.deviceInfo.invoke() ?: return
genericItem { genericItem {
id("info${info?.deviceId}") id("info${info.deviceId}")
title(info?.displayName ?: "") title(info.displayName ?: "")
description("(${info?.deviceId})") description("(${info.deviceId})")
} }
genericFooterItem { genericFooterItem {
id("infoCrypto${info?.deviceId}") id("infoCrypto${info.deviceId}")
text(stringProvider.getString(R.string.settings_failed_to_get_crypto_device_info)) text(stringProvider.getString(R.string.settings_failed_to_get_crypto_device_info))
} }
if (info?.deviceId != session.sessionParams.credentials.deviceId) { info.deviceId?.let { addGenericDeviceManageActions(data, it) }
// 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 ?: ""))
}
}
}
} else {
loadingItem {
id("loading")
}
}
} }
interface Callback { interface Callback {

View File

@ -16,14 +16,18 @@
package im.vector.riotx.features.settings.devices package im.vector.riotx.features.settings.devices
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.riotx.core.platform.VectorViewModelAction import im.vector.riotx.core.platform.VectorViewModelAction
sealed class DevicesAction : VectorViewModelAction { sealed class DevicesAction : VectorViewModelAction {
object Retry : DevicesAction() object Refresh : DevicesAction()
data class Delete(val deviceId: String) : DevicesAction() data class Delete(val deviceId: String) : DevicesAction()
data class Password(val password: String) : DevicesAction() data class Password(val password: String) : DevicesAction()
data class Rename(val deviceId: String, val newName: String) : DevicesAction() data class Rename(val deviceId: String, val newName: String) : DevicesAction()
data class PromptRename(val deviceId: String) : DevicesAction() data class PromptRename(val deviceId: String) : DevicesAction()
data class VerifyMyDevice(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()
} }

View File

@ -21,20 +21,23 @@ import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.extensions.sortByLastSeen 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.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.errorWithRetryItem import im.vector.riotx.core.epoxy.errorWithRetryItem
import im.vector.riotx.core.epoxy.loadingItem import im.vector.riotx.core.epoxy.loadingItem
import im.vector.riotx.core.error.ErrorFormatter 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.resources.StringProvider
import im.vector.riotx.core.ui.list.genericItemHeader import im.vector.riotx.core.ui.list.genericItemHeader
import im.vector.riotx.core.utils.DimensionConverter
import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.settings.VectorPreferences
import javax.inject.Inject import javax.inject.Inject
class DevicesController @Inject constructor(private val errorFormatter: ErrorFormatter, class DevicesController @Inject constructor(private val errorFormatter: ErrorFormatter,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val colorProvider: ColorProvider,
private val dimensionConverter: DimensionConverter,
private val vectorPreferences: VectorPreferences) : EpoxyController() { private val vectorPreferences: VectorPreferences) : EpoxyController() {
var callback: Callback? = null var callback: Callback? = null
@ -68,30 +71,51 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor
listener { callback?.retry() } listener { callback?.retry() }
} }
is Success -> is Success ->
buildDevicesList(devices(), state.cryptoDevices(), state.myDeviceId) buildDevicesList(devices(), state.myDeviceId, !state.hasAccountCrossSigning, state.accountCrossSigningIsTrusted)
} }
} }
private fun buildDevicesList(devices: List<DeviceInfo>, cryptoDevices: List<CryptoDeviceInfo>?, myDeviceId: String) { private fun buildDevicesList(devices: List<DeviceFullInfo>,
myDeviceId: String,
legacyMode: Boolean,
currentSessionCrossTrusted: Boolean) {
devices
.firstOrNull {
it.deviceInfo.deviceId == myDeviceId
}?.let { fullInfo ->
val deviceInfo = fullInfo.deviceInfo
// Current device // Current device
genericItemHeader { genericItemHeader {
id("current") id("current")
text(stringProvider.getString(R.string.devices_current_device)) text(stringProvider.getString(R.string.devices_current_device))
} }
devices
.filter {
it.deviceId == myDeviceId
}
.forEachIndexed { idx, deviceInfo ->
deviceItem { deviceItem {
id("myDevice$idx") id("myDevice${deviceInfo.deviceId}")
legacyMode(legacyMode)
trustedSession(currentSessionCrossTrusted)
dimensionConverter(dimensionConverter)
colorProvider(colorProvider)
detailedMode(vectorPreferences.developerMode()) detailedMode(vectorPreferences.developerMode())
deviceInfo(deviceInfo) deviceInfo(deviceInfo)
currentDevice(true) currentDevice(true)
e2eCapable(true)
itemClickAction { callback?.onDeviceClicked(deviceInfo) } 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 // Other devices
@ -103,19 +127,23 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor
devices devices
.filter { .filter {
it.deviceId != myDeviceId it.deviceInfo.deviceId != myDeviceId
} }
// sort before display: most recent first .forEachIndexed { idx, deviceInfoPair ->
.sortByLastSeen() val deviceInfo = deviceInfoPair.deviceInfo
.forEachIndexed { idx, deviceInfo -> val cryptoInfo = deviceInfoPair.cryptoDeviceInfo
val isCurrentDevice = deviceInfo.deviceId == myDeviceId
deviceItem { deviceItem {
id("device$idx") id("device$idx")
legacyMode(legacyMode)
trustedSession(currentSessionCrossTrusted)
dimensionConverter(dimensionConverter)
colorProvider(colorProvider)
detailedMode(vectorPreferences.developerMode()) detailedMode(vectorPreferences.developerMode())
deviceInfo(deviceInfo) deviceInfo(deviceInfo)
currentDevice(isCurrentDevice) currentDevice(false)
itemClickAction { callback?.onDeviceClicked(deviceInfo) } itemClickAction { callback?.onDeviceClicked(deviceInfo) }
trusted(cryptoDevices?.firstOrNull { it.deviceId == deviceInfo.deviceId }?.isVerified) e2eCapable(cryptoInfo != null)
trusted(cryptoInfo?.trustLevel)
} }
} }
} }

View File

@ -17,6 +17,8 @@
package im.vector.riotx.features.settings.devices 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.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.riotx.core.platform.VectorViewEvents import im.vector.riotx.core.platform.VectorViewEvents
@ -35,4 +37,10 @@ sealed class DevicesViewEvents : VectorViewEvents {
val userId: String, val userId: String,
val transactionId: String? val transactionId: String?
) : DevicesViewEvents() ) : DevicesViewEvents()
data class SelfVerification(
val session: Session
) : DevicesViewEvents()
data class ShowManuallyVerify(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesViewEvents()
} }

View File

@ -16,6 +16,7 @@
package im.vector.riotx.features.settings.devices package im.vector.riotx.features.settings.devices
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext 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.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback 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.failure.Failure
import im.vector.matrix.android.api.session.Session 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.VerificationService
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes 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.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.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.matrix.rx.rx
import im.vector.riotx.core.platform.VectorViewModel 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( data class DevicesViewState(
val myDeviceId: String = "", val myDeviceId: String = "",
val devices: Async<List<DeviceInfo>> = Uninitialized, // val devices: Async<List<DeviceInfo>> = Uninitialized,
val cryptoDevices: Async<List<CryptoDeviceInfo>> = Uninitialized, // val cryptoDevices: Async<List<CryptoDeviceInfo>> = Uninitialized,
val devices: Async<List<DeviceFullInfo>> = Uninitialized,
// TODO Replace by isLoading boolean // TODO Replace by isLoading boolean
val request: Async<Unit> = Uninitialized val request: Async<Unit> = Uninitialized,
val hasAccountCrossSigning: Boolean = false,
val accountCrossSigningIsTrusted: Boolean = false
) : MvRxState ) : MvRxState
data class DeviceFullInfo(
val deviceInfo: DeviceInfo,
val cryptoDeviceInfo: CryptoDeviceInfo?
)
class DevicesViewModel @AssistedInject constructor( class DevicesViewModel @AssistedInject constructor(
@Assisted initialState: DevicesViewState, @Assisted initialState: DevicesViewState,
private val session: Session, private val session: Session
private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider) ) : VectorViewModel<DevicesViewState, DevicesAction, DevicesViewEvents>(initialState), VerificationService.Listener {
: VectorViewModel<DevicesViewState, DevicesAction, DevicesViewEvents>(initialState), VerificationService.Listener {
@AssistedInject.Factory @AssistedInject.Factory
interface Factory { interface Factory {
@ -74,16 +88,76 @@ class DevicesViewModel @AssistedInject constructor(
private var _currentDeviceId: String? = null private var _currentDeviceId: String? = null
private var _currentSession: String? = null private var _currentSession: String? = null
init { private val refreshPublisher: PublishSubject<Unit> = PublishSubject.create()
refreshDevicesList()
session.cryptoService().verificationService().addListener(this)
session.rx().liveUserCryptoDevices(session.myUserId) init {
.execute {
setState {
copy( copy(
cryptoDevices = it hasAccountCrossSigning = session.cryptoService().crossSigningService().getMyCrossSigningKeys() != null,
accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified(),
myDeviceId = session.sessionParams.credentials.deviceId ?: ""
) )
} }
Observable.combineLatest<List<CryptoDeviceInfo>, List<DeviceInfo>, List<DeviceFullInfo>>(
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(
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() { override fun onCleared() {
@ -93,7 +167,7 @@ class DevicesViewModel @AssistedInject constructor(
override fun transactionUpdated(tx: VerificationTransaction) { override fun transactionUpdated(tx: VerificationTransaction) {
if (tx.state == VerificationTxState.Verified) { 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. * The devices list is the list of the devices where the user is logged in.
* It can be any mobile devices, and any browsers. * It can be any mobile devices, and any browsers.
*/ */
private fun refreshDevicesList() { private fun queryRefreshDevicesList() {
if (!session.sessionParams.credentials.deviceId.isNullOrEmpty()) { refreshPublisher.onNext(Unit)
// 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<DevicesListResponse> {
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<MXUsersDevicesMap<CryptoDeviceInfo>> {
override fun onSuccess(data: MXUsersDevicesMap<CryptoDeviceInfo>) {
setState {
copy(
cryptoDevices = Success(session.cryptoService().getUserDevices(session.myUserId))
)
}
}
})
} else {
// Should not happen
}
} }
override fun handle(action: DevicesAction) { override fun handle(action: DevicesAction) {
return when (action) { return when (action) {
is DevicesAction.Retry -> refreshDevicesList() is DevicesAction.Refresh -> queryRefreshDevicesList()
is DevicesAction.Delete -> handleDelete(action) is DevicesAction.Delete -> handleDelete(action)
is DevicesAction.Password -> handlePassword(action) is DevicesAction.Password -> handlePassword(action)
is DevicesAction.Rename -> handleRename(action) is DevicesAction.Rename -> handleRename(action)
is DevicesAction.PromptRename -> handlePromptRename(action) is DevicesAction.PromptRename -> handlePromptRename(action)
is DevicesAction.VerifyMyDevice -> handleVerify(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() val txID = session.cryptoService()
.verificationService() .verificationService()
.requestKeyVerification(supportedVerificationMethodsProvider.provide(), session.myUserId, listOf(action.deviceId)) .beginKeyVerification(VerificationMethod.SAS, session.myUserId, action.deviceId, null)
_viewEvents.post(DevicesViewEvents.ShowVerifyDevice( _viewEvents.post(DevicesViewEvents.ShowVerifyDevice(
session.myUserId, 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<Unit> {
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 -> 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) { 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 // force settings update
refreshDevicesList() queryRefreshDevicesList()
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
@ -270,7 +319,7 @@ class DevicesViewModel @AssistedInject constructor(
) )
} }
// force settings update // force settings update
refreshDevicesList() queryRefreshDevicesList()
} }
}) })
} }
@ -299,7 +348,7 @@ class DevicesViewModel @AssistedInject constructor(
) )
} }
// force settings update // force settings update
refreshDevicesList() queryRefreshDevicesList()
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {

View File

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

View File

@ -27,6 +27,7 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.dialogs.ManuallyVerifyDialog
import im.vector.riotx.core.dialogs.PromptPasswordDialog import im.vector.riotx.core.dialogs.PromptPasswordDialog
import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.extensions.configureWith
@ -73,6 +74,15 @@ class VectorSettingsDevicesFragment @Inject constructor(
transactionId = it.transactionId transactionId = it.transactionId
).show(childFragmentManager, "REQPOP") ).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 }.exhaustive
} }
} }
@ -92,8 +102,8 @@ class VectorSettingsDevicesFragment @Inject constructor(
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_active_sessions_manage) (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_active_sessions_manage)
viewModel.handle(DevicesAction.Refresh)
} }
override fun onDeviceClicked(deviceInfo: DeviceInfo) { override fun onDeviceClicked(deviceInfo: DeviceInfo) {
@ -112,7 +122,7 @@ class VectorSettingsDevicesFragment @Inject constructor(
// } // }
override fun retry() { override fun retry() {
viewModel.handle(DevicesAction.Retry) viewModel.handle(DevicesAction.Refresh)
} }
/** /**

View File

@ -26,6 +26,12 @@ import javax.inject.Inject
*/ */
class SharedPreferencesUiStateRepository @Inject constructor(private val sharedPreferences: SharedPreferences) : UiStateRepository { class SharedPreferencesUiStateRepository @Inject constructor(private val sharedPreferences: SharedPreferences) : UiStateRepository {
override fun reset() {
sharedPreferences.edit {
remove(KEY_DISPLAY_MODE)
}
}
override fun getDisplayMode(): RoomListDisplayMode { override fun getDisplayMode(): RoomListDisplayMode {
return when (sharedPreferences.getInt(KEY_DISPLAY_MODE, VALUE_DISPLAY_MODE_CATCHUP)) { return when (sharedPreferences.getInt(KEY_DISPLAY_MODE, VALUE_DISPLAY_MODE_CATCHUP)) {
VALUE_DISPLAY_MODE_PEOPLE -> RoomListDisplayMode.PEOPLE VALUE_DISPLAY_MODE_PEOPLE -> RoomListDisplayMode.PEOPLE

View File

@ -23,6 +23,11 @@ import im.vector.riotx.features.home.RoomListDisplayMode
*/ */
interface UiStateRepository { interface UiStateRepository {
/**
* Reset all the saved data
*/
fun reset()
fun getDisplayMode(): RoomListDisplayMode fun getDisplayMode(): RoomListDisplayMode
fun storeDisplayMode(displayMode: RoomListDisplayMode) fun storeDisplayMode(displayMode: RoomListDisplayMode)

View File

@ -33,7 +33,7 @@
<LinearLayout <LinearLayout
android:id="@+id/alerter_texts" android:id="@+id/alerter_texts"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@ -68,6 +68,7 @@
android:textAppearance="@style/AlertTextAppearance.Text" android:textAppearance="@style/AlertTextAppearance.Text"
android:visibility="gone" android:visibility="gone"
tools:text="Text" tools:text="Text"
android:maxLines="3"
tools:visibility="visible" /> tools:visibility="visible" />
</LinearLayout> </LinearLayout>

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android" <androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
@ -12,10 +11,9 @@
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="16dp" android:paddingTop="16dp"
android:paddingBottom="16dp" android:paddingBottom="16dp">
android:layout_height="wrap_content">
<ImageView <ImageView
android:id="@+id/bootstrapIcon" android:id="@+id/bootstrapIcon"
@ -28,23 +26,19 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<TextView <TextView
android:id="@+id/bootstrapTitleText" android:id="@+id/bootstrapTitleText"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_weight="1"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="2"
android:textColor="?riotx_text_primary" android:textColor="?riotx_text_primary"
android:textSize="20sp" android:textSize="20sp"
android:textStyle="bold" android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@+id/bootstrapIcon"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/bootstrapIcon" app:layout_constraintStart_toEndOf="@+id/bootstrapIcon"
app:layout_constraintTop_toTopOf="@+id/bootstrapIcon" app:layout_constraintTop_toTopOf="parent"
tools:text="@string/recovery_passphrase" /> tools:text="@string/recovery_passphrase" />
<androidx.fragment.app.FragmentContainerView <androidx.fragment.app.FragmentContainerView

View File

@ -44,13 +44,11 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_weight="1"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="2" android:maxLines="2"
android:textColor="?riotx_text_primary" android:textColor="?riotx_text_primary"
android:textSize="20sp" android:textSize="20sp"
android:textStyle="bold" android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@+id/verificationRequestAvatar"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/verificationRequestAvatar" app:layout_constraintStart_toEndOf="@+id/verificationRequestAvatar"
app:layout_constraintTop_toTopOf="@+id/verificationRequestAvatar" app:layout_constraintTop_toTopOf="@+id/verificationRequestAvatar"

View File

@ -12,11 +12,11 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
tools:text="@string/enter_account_password"
android:textColor="?riotx_text_primary" android:textColor="?riotx_text_primary"
android:textSize="14sp" android:textSize="14sp"
app:layout_constraintBottom_toTopOf="@id/bootstrapAccountPasswordTil" app:layout_constraintBottom_toTopOf="@id/bootstrapAccountPasswordTil"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent"
tools:text="@string/enter_account_password" />
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/bootstrapAccountPasswordTil" android:id="@+id/bootstrapAccountPasswordTil"
@ -59,9 +59,7 @@
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/bootstrapPasswordButton" android:id="@+id/bootstrapPasswordButton"
style="@style/VectorButtonStyleText" style="@style/VectorButtonStyleText"
android:layout_gravity="end"
android:layout_marginTop="@dimen/layout_vertical_margin" android:layout_marginTop="@dimen/layout_vertical_margin"
android:padding="8dp"
android:text="@string/_continue" android:text="@string/_continue"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
@ -25,7 +24,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
app:errorEnabled="true" app:errorEnabled="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/ssss_view_show_password" app:layout_constraintEnd_toStartOf="@id/ssss_view_show_password"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/bootstrapDescriptionText"> app:layout_constraintTop_toBottomOf="@id/bootstrapDescriptionText">
@ -34,11 +32,11 @@
android:id="@+id/ssss_passphrase_enter_edittext" android:id="@+id/ssss_passphrase_enter_edittext"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:hint="@string/passphrase_enter_passphrase"
android:imeOptions="actionDone" android:imeOptions="actionDone"
android:maxLines="3" android:maxLines="3"
android:singleLine="false" android:singleLine="false"
android:textColor="?android:textColorPrimary" android:textColor="?android:textColorPrimary"
tools:hint="@string/passphrase_enter_passphrase"
tools:inputType="textPassword" /> tools:inputType="textPassword" />
<!-- This is inside the TIL, if not the keyboard will hide it when in bottomsheet --> <!-- This is inside the TIL, if not the keyboard will hide it when in bottomsheet -->
@ -46,9 +44,9 @@
<im.vector.riotx.core.ui.views.PasswordStrengthBar <im.vector.riotx.core.ui.views.PasswordStrengthBar
android:id="@+id/ssss_passphrase_security_progress" android:id="@+id/ssss_passphrase_security_progress"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_marginBottom="2dp" android:layout_height="4dp"
android:layout_marginTop="2dp" android:layout_marginTop="2dp"
android:layout_height="4dp" /> android:layout_marginBottom="2dp" />
<TextView <TextView
android:id="@+id/bootstrapWarningInfo" android:id="@+id/bootstrapWarningInfo"
@ -56,12 +54,12 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:textSize="12sp"
android:gravity="center_vertical"
android:drawableStart="@drawable/ic_alert_triangle" android:drawableStart="@drawable/ic_alert_triangle"
android:drawableTint="@color/riotx_destructive_accent"
android:drawablePadding="4dp" android:drawablePadding="4dp"
android:text="@string/bootstrap_dont_reuse_pwd" /> android:drawableTint="@color/riotx_destructive_accent"
android:gravity="center_vertical"
android:text="@string/bootstrap_dont_reuse_pwd"
android:textSize="12sp" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
@ -78,6 +76,13 @@
app:layout_constraintStart_toEndOf="@+id/ssss_passphrase_enter_til" app:layout_constraintStart_toEndOf="@+id/ssss_passphrase_enter_til"
app:layout_constraintTop_toTopOf="@+id/ssss_passphrase_enter_til" /> app:layout_constraintTop_toTopOf="@+id/ssss_passphrase_enter_til" />
<com.google.android.material.button.MaterialButton
android:id="@+id/bootstrapSubmit"
style="@style/VectorButtonStyleText"
android:text="@string/_continue"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ssss_passphrase_enter_til" />
<!-- <TextView--> <!-- <TextView-->
<!-- android:id="@+id/bootstrapWarningInfo"--> <!-- android:id="@+id/bootstrapWarningInfo"-->

View File

@ -79,7 +79,6 @@
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/bootstrapMigrateContinueButton" android:id="@+id/bootstrapMigrateContinueButton"
style="@style/VectorButtonStyleText" style="@style/VectorButtonStyleText"
android:layout_gravity="end"
android:layout_marginTop="@dimen/layout_vertical_margin" android:layout_marginTop="@dimen/layout_vertical_margin"
android:text="@string/_continue" android:text="@string/_continue"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:padding="16dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/itemGenericItemButton"
style="@style/Widget.MaterialComponents.Button.TextButton.Icon"
android:textAllCaps="false"
tools:icon="@drawable/ic_shield_warning"
app:iconGravity="textStart"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Action Name" />
</LinearLayout>

View File

@ -58,7 +58,7 @@
android:id="@+id/messageContentMergedCreationStub" android:id="@+id/messageContentMergedCreationStub"
style="@style/TimelineContentStubBaseParams" style="@style/TimelineContentStubBaseParams"
android:layout="@layout/item_timeline_event_merged_room_creation_stub" android:layout="@layout/item_timeline_event_merged_room_creation_stub"
tools:layout_marginTop="160dp" tools:layout_marginTop="240dp"
tools:visibility="visible" /> tools:visibility="visible" />
</FrameLayout> </FrameLayout>

View File

@ -594,7 +594,7 @@ Vaši e-mailovou adresu můžete přidat k profilu v nastavení.</string>
<string name="settings_notification_advanced_summary">Nastavit důležitost oznámení na základě události, nastavení zvuku, LED, vibrací</string> <string name="settings_notification_advanced_summary">Nastavit důležitost oznámení na základě události, nastavení zvuku, LED, vibrací</string>
<string name="settings_notification_by_event">Důležitost oznámení na základě události</string> <string name="settings_notification_by_event">Důležitost oznámení na základě události</string>
<string name="notification_sync_init">Inicializace služby</string> <string name="notification_sync_init">Spouštím služby</string>
<string name="title_activity_verify_device">Ověřte relaci</string> <string name="title_activity_verify_device">Ověřte relaci</string>
<string name="disconnect">Odpojit</string> <string name="disconnect">Odpojit</string>
@ -1634,4 +1634,711 @@ Vaši e-mailovou adresu můžete přidat k profilu v nastavení.</string>
<string name="store_riotx_title">RiotX - Klient Matrixu pro příští generaci</string> <string name="store_riotx_title">RiotX - Klient Matrixu pro příští generaci</string>
<string name="store_riotx_short_description">Rychlejši a lehčí klient Matrixu s nejnovějšími konstrukcemi Androidu</string> <string name="store_riotx_short_description">Rychlejši a lehčí klient Matrixu s nejnovějšími konstrukcemi Androidu</string>
<string name="store_riotx_full_description">"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 • …"</string>
<string name="bottom_action_people_x">Přímé zprávy</string>
<string name="send_file_step_idle">Čekám…</string>
<string name="send_file_step_encrypting_thumbnail">Šifruji miniaturu…</string>
<string name="send_file_step_sending_thumbnail">Odesílám miniaturu (%1$s / %2$s)</string>
<string name="send_file_step_encrypting_file">Šifruji soubor…</string>
<string name="send_file_step_sending_file">Odesílám soubor (%1$s / %2$s)</string>
<string name="downloading_file">Stahuji soubor %1$s…</string>
<string name="downloaded_file">Soubor %1$s byl stažen!</string>
<string name="edited_suffix">(upraveno)</string>
<string name="riotx_no_registration_notice">%1$s pro založení účtu.</string>
<string name="riotx_no_registration_notice_colored_part">Použít zastaralou aplikaci</string>
<string name="message_edits">Úpravy zpráv</string>
<string name="no_message_edits_found">Úpravy nenalezeny</string>
<string name="room_filtering_filter_hint">Vytřídit konverzace…</string>
<string name="room_filtering_footer_title">Nemůžete najít, co hledáte\?</string>
<string name="room_filtering_footer_create_new_room">Založit novou místnost</string>
<string name="room_filtering_footer_create_new_direct_message">Poslat novou přímou zprávu</string>
<string name="room_filtering_footer_open_room_directory">Ukázat adresář místností</string>
<string name="room_directory_search_hint">Jméno nebo ID (#example:matrix.org)</string>
<string name="labs_swipe_to_reply_in_timeline">Zapnout přetažení pro odpověď v časovém sledu</string>
<string name="link_copied_to_clipboard">Odkaz zkopírován do schránky</string>
<string name="add_by_matrix_id">Přidat pomocí matrix ID</string>
<string name="creating_direct_room">Zakládám místnost…</string>
<string name="direct_room_no_known_users">Žádný výsledek nenalezen, použijte Přidat pomocí matrix ID k hledání na serveru.</string>
<string name="direct_room_start_search">K obdržení výsledků začněte zadávat</string>
<string name="direct_room_filter_hint">Vytřídit uživatelským jménem nebo ID…</string>
<string name="joining_room">Vstupuji do místnosti…</string>
<string name="message_view_edit_history">Ukázat historii úprav</string>
<string name="terms_of_service">Všeobecné podmínky</string>
<string name="review_terms">Pročíst všeobecné podmínky</string>
<string name="terms_description_for_identity_server">Nechte se nalézt druhými</string>
<string name="terms_description_for_integration_manager">Použijte boty, můstky, widgety a nálepky</string>
<string name="read_at">Čtěte na</string>
<string name="identity_server">Server pro identity</string>
<string name="disconnect_identity_server">Odpojit server pro identity</string>
<string name="add_identity_server">Nastavit server pro identity</string>
<string name="change_identity_server">Změnit server pro identity</string>
<string name="settings_discovery_identity_server_info">Nyní používáte %1$s, abyste nalezli a byli nalezeni existujícími kontakty, které znáte.</string>
<string name="settings_discovery_identity_server_info_none">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.</string>
<string name="settings_discovery_emails_title">Emailová adresa k nalezení</string>
<string name="settings_discovery_no_mails">Volby pro nalezení se ukážou, jakmile doplníte email.</string>
<string name="settings_discovery_no_msisdn">Volby pro nalezení se ukážou, jakmile doplníte telefonní číslo.</string>
<string name="settings_discovery_disconnect_identity_server_info">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.</string>
<string name="settings_discovery_msisdn_title">Telefonní čísla pro nalezení</string>
<string name="settings_discovery_confirm_mail">Poslali jsme Vám potvrzovací email na %s, podívejte se do emailu a klikněte na protvrzovací odkaz</string>
<string name="settings_discovery_mail_pending">Nevyřízený</string>
<string name="settings_discovery_enter_identity_server">Zadejte nový server pro identity</string>
<string name="settings_discovery_bad_identity_server">Nemohl jsem se spojit se serverem pro identity</string>
<string name="settings_discovery_please_enter_server">Prosím, zadejte url serveru pro identity</string>
<string name="settings_discovery_no_terms_title">Server pro identity nemá žádné všeobecné podmínky</string>
<string name="settings_discovery_no_terms">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</string>
<string name="settings_text_message_sent">Textová zpráva byla odeslána na %s. Prosím, zadejte ověřovací kód v ní obsažený.</string>
<string name="settings_discovery_disconnect_with_bound_pid">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.</string>
<string name="settings_agree_to_terms">Souhlaste se všeobecnými podmínkami serveru identit (%s), abyste byli k nalezení podle emailové adresy nebo telefonního čísla.</string>
<string name="labs_allow_extended_logging">Zapnout podrobné záznamy.</string>
<string name="labs_allow_extended_logging_summary">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.</string>
<string name="error_terms_not_accepted">Prosím, opakujte, jakmile jste přijali všeobecné podmínky svého homeserveru.</string>
<string name="error_network_timeout">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.</string>
<string name="send_attachment">Poslat přílohu</string>
<string name="a11y_open_drawer">Otevřít navigační zásuvku</string>
<string name="a11y_create_menu_open">Otevřít menu založení místnosti</string>
<string name="a11y_create_menu_close">Zavřít menu založení místnosti…</string>
<string name="a11y_create_direct_message">Založit novou přímou konverzaci</string>
<string name="a11y_create_room">Založit novou místnost</string>
<string name="a11y_close_keys_backup_banner">Zavřít titulek zálohy klíčů</string>
<string name="a11y_show_password">Ukázat heslo</string>
<string name="a11y_hide_password">Skrýt heslo</string>
<string name="a11y_jump_to_bottom">Přeskočit až dolů</string>
<string name="two_and_some_others_read">%1$s, %2$s a %3$d dalších přečetli</string>
<string name="three_users_read">%1$s, %2$s a %3$s přečetli</string>
<string name="two_users_read">%1$s a %2$s přečetli</string>
<string name="one_user_read">%s přečetl(a)</string>
<plurals name="fallback_users_read">
<item quantity="one">1 uživatel přečetl</item>
<item quantity="few">%d uživatelé přečetli</item>
<item quantity="other">%d uživatelů přečetlo</item>
</plurals>
<string name="error_file_too_big">Soubor \'%1$s\' (%2$s) je příliš velký k nahrání. Limit je %3$s.</string>
<string name="error_attachment">Během načítání přílohy došlo k chybě.</string>
<string name="attachment_type_file">Soubor</string>
<string name="attachment_type_contact">Kontakt</string>
<string name="attachment_type_camera">Fotoaparát</string>
<string name="attachment_type_audio">Audio</string>
<string name="attachment_type_gallery">Galerie</string>
<string name="attachment_type_sticker">Nálepka</string>
<string name="error_handling_incoming_share">Nemohl jsem zpracovat sdílená data</string>
<string name="report_content_spam">Je to spam</string>
<string name="report_content_inappropriate">Je to nepatřičné</string>
<string name="report_content_custom">Vlastní hlášení…</string>
<string name="report_content_custom_title">Nahlásit tento obsah</string>
<string name="report_content_custom_hint">Důvod k nahlášení tohoto obsahu</string>
<string name="report_content_custom_submit">HLÁŠENÍ</string>
<string name="block_user">ZABLOKOVAT UŽIVATELE</string>
<string name="content_reported_title">Obsah ohlášen</string>
<string name="content_reported_content">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</string>
<string name="content_reported_as_spam_title">Nahlášeno jako spam</string>
<string name="content_reported_as_spam_content">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</string>
<string name="content_reported_as_inappropriate_title">Nahlášeno jako nepatřičné</string>
<string name="content_reported_as_inappropriate_content">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</string>
<string name="permissions_rationale_msg_keys_backup_export">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ě.</string>
<string name="no_network_indicator">Právě nyní není k dispozici žádné síťové spojení</string>
<string name="message_ignore_user">Zablokovat uživatele</string>
<string name="room_list_quick_actions_notifications_all_noisy">Všechny zprávy (hlučné)</string>
<string name="room_list_quick_actions_notifications_all">Všechny zprávy</string>
<string name="room_list_quick_actions_notifications_mentions">Pouze zmínky</string>
<string name="room_list_quick_actions_notifications_mute">Utišit</string>
<string name="room_list_quick_actions_settings">Nastavení</string>
<string name="room_list_quick_actions_leave">Opustit místnost</string>
<string name="notice_member_no_changes">%1$s neučinil žádné změny</string>
<string name="command_description_spoiler">Odeslat danou zprávu jako spoiler</string>
<string name="spoiler">Spoiler</string>
<string name="reaction_search_type_hint">Pro nalezení reakce zadejte klíčové slovo.</string>
<string name="no_ignored_users">Neignorujete žádné uživatele</string>
<string name="help_long_click_on_room_for_more_options">Více možností po dlouhém stisku na místnost</string>
<string name="room_join_rules_public">%1$s učinil místnost veřejnou pro kohokoli znalého odkazu.</string>
<string name="room_join_rules_invite">%1$s nastavil místnost jen pro pozvané.</string>
<string name="timeline_unread_messages">Nepřečtené zprávy</string>
<string name="login_splash_title">Osvoboďte svou komunikaci</string>
<string name="login_splash_text1">Chatujte s lidmi přímo nebo ve skupinách</string>
<string name="login_splash_text2">Udržujte konverzace soukromé pomocí šifrování</string>
<string name="login_splash_text3">Rozšiřte &amp; upravte si svůj zážitek</string>
<string name="login_splash_submit">Můžeme začít</string>
<string name="login_server_title">Vybrat server</string>
<string name="login_server_text">Jako email, účty mají jeden domov, ačkoli můžete mluvit s kýmkoli</string>
<string name="login_server_matrix_org_text">Přidejte se k miliónům svobodným na největším veřejném serveru</string>
<string name="login_server_modular_text">Prémiový hosting pro organizace</string>
<string name="login_server_modular_learn_more">Dozvědět se víc</string>
<string name="login_server_other_title">Další</string>
<string name="login_server_other_text">Vlastní &amp; pokročilá nastavení</string>
<string name="login_continue">Pokračovat</string>
<string name="login_connect_to">Připojit k %1$s</string>
<string name="login_connect_to_modular">Připojit k Modular</string>
<string name="login_connect_to_a_custom_server">Upravit připojení k serveru</string>
<string name="login_signin_to">Přihlásit se na %1$s</string>
<string name="login_signup">Založit účet</string>
<string name="login_signin">Přihlásit se</string>
<string name="login_signin_sso">Pokračovat s SSO</string>
<string name="login_server_url_form_modular_hint">Adresa Modular</string>
<string name="login_server_url_form_other_hint">Adresa</string>
<string name="login_server_url_form_modular_text">Prémiový hosting pro organizace</string>
<string name="login_server_url_form_modular_notice">Zadejte adresu Modular RIot nebo serveru, který chcete použít</string>
<string name="login_server_url_form_other_notice">Zadejte adresu serveru nebo Riotu, k němuž se chcete připojit</string>
<string name="login_sso_error_message">Při načítání stránky došlo k chybě: %1$s (%2$d)</string>
<string name="login_mode_not_supported">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\?</string>
<string name="login_registration_disabled">Omlouváme se, tento server již nepřijímá nové účty.</string>
<string name="login_registration_not_supported">Aplikace nemůže založit účet na tomto homeserveru.
\n
\nChcete se přihlásit webovým klientem\?</string>
<string name="login_login_with_email_error">Tato emailová adresa se nevztahuje k žádnému účtu.</string>
<string name="login_reset_password_on">Resetovat heslo na %1$s</string>
<string name="login_reset_password_notice">Ověřovací email bude odeslán do Vašeho mailboxu za účelem potvrzení nastavení nového heslo.</string>
<string name="login_reset_password_submit">Dále</string>
<string name="login_reset_password_email_hint">Email</string>
<string name="login_reset_password_password_hint">Nové heslo</string>
<string name="login_reset_password_warning_title">Varování!</string>
<string name="login_reset_password_warning_content">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.</string>
<string name="login_reset_password_warning_submit">Pokračovat</string>
<string name="login_reset_password_error_not_found">Tato emailová adresa se nevztahuje k žádnému účtu</string>
<string name="login_reset_password_mail_confirmation_title">Nahlédněte do inboxu</string>
<string name="login_reset_password_mail_confirmation_notice">Ověřovací email byl odeslán na %1$s.</string>
<string name="login_reset_password_mail_confirmation_notice_2">Pro potvrzení svého nového hesla klepněte na odkaz. Jakmile otevřete v něm uvedený odkaz, klikněte níže.</string>
<string name="login_reset_password_mail_confirmation_submit">Ověřil(a) jsem svou emailovou adresu</string>
<string name="login_reset_password_success_title">Hotovo!</string>
<string name="login_reset_password_success_notice">Vaše heslo bylo přenastaveno.</string>
<string name="login_reset_password_success_notice_2">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í.</string>
<string name="login_reset_password_success_submit">Zpět na přihlášení</string>
<string name="login_reset_password_cancel_confirmation_title">Varování</string>
<string name="login_reset_password_cancel_confirmation_content">Vaše heslo nebylo dosud změněno.
\n
\nZastavit proces změny hesla\?</string>
<string name="login_set_email_title">Nastavit emailovou adresu</string>
<string name="login_set_email_notice">Nastavte emailovou adresu pro obnově svého účtu. Později můžete volitelně dovolit lidem, které znáte, aby Vás podle emailu nalezli.</string>
<string name="login_set_email_mandatory_hint">Email</string>
<string name="login_set_email_optional_hint">Email (volitelné)</string>
<string name="login_set_email_submit">Dále</string>
<string name="login_set_msisdn_title">Nastavit telefonní číslo</string>
<string name="login_set_msisdn_notice">Nastavte telefonní číslo, abyste volitelně dovolili lidem, které znáte, aby Vás nalezli.</string>
<string name="login_set_msisdn_notice2">Prosím, použijte mezinárodní formát.</string>
<string name="login_set_msisdn_mandatory_hint">Telefonní číslo</string>
<string name="login_set_msisdn_optional_hint">Telefonní číslo (volitelné)</string>
<string name="login_set_msisdn_submit">Dále</string>
<string name="login_msisdn_confirm_title">Potvrdit telefonní číslo</string>
<string name="login_msisdn_confirm_notice">Právě jsme poslali kód na %1$s. Zadejte jej níže pro ověření, že jste to Vy.</string>
<string name="login_msisdn_confirm_hint">Zadejte kód</string>
<string name="login_msisdn_confirm_send_again">Poslat znovu</string>
<string name="login_msisdn_confirm_submit">Dále</string>
<string name="login_msisdn_error_not_international">Mezinárodní telefonní čísla musí začít s \'+\'</string>
<string name="login_msisdn_error_other">Telefonní číslo se zdá neplatné. Prosím, zkontrolujte</string>
<string name="login_signup_to">Přihlásit se k %1$s</string>
<string name="login_signin_username_hint">Uživatelské jméno nebo email</string>
<string name="login_signup_username_hint">Uživatelské jméno</string>
<string name="login_signup_error_user_in_use">Toto uživatelské jméno je již obsazeno</string>
<string name="login_signup_cancel_confirmation_title">Varování</string>
<string name="login_signup_cancel_confirmation_content">Váš účet nebyl ještě založen.
\n
\nZastavit registrační proces\?</string>
<string name="login_a11y_choose_matrix_org">Vybrat matrix.org</string>
<string name="login_a11y_choose_modular">Vybrat modular</string>
<string name="login_a11y_choose_other">Vybrat upravený homeserver</string>
<string name="login_a11y_captcha_container">Prosím, proveďte vybídnutí captcha</string>
<string name="login_terms_title">Přijmout všeobecné podmínky a pokračovat</string>
<string name="login_wait_for_email_title">Prosím, nahlédněte do svého emailu</string>
<string name="login_wait_for_email_notice">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ý.</string>
<string name="login_validation_code_is_not_correct">Zadaný kód není správný. Prosím, zkontrolujte.</string>
<string name="login_error_outdated_homeserver_title">Zastaralý homeserver</string>
<string name="login_error_outdated_homeserver_content">Tento homeserver používá za účelem spojení příliš starou verzi. Požádejte správce, aby provedl aktualizaci.</string>
<plurals name="login_error_limit_exceeded_retry_after">
<item quantity="one">Bylo odesláno příliš mnoho požadavků. Můžete opakovat za %1$d vteřinu…</item>
<item quantity="few">Bylo odesláno příliš mnoho požadavků. Můžete opakovat za %1$d vteřiny…</item>
<item quantity="other">Bylo odesláno příliš mnoho požadavků. Můžete opakovat za %1$d vteřin…</item>
</plurals>
<string name="seen_by">Viděno</string>
<string name="signed_out_title">Právě jste se odhlásili</string>
<string name="signed_out_notice">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ů.</string>
<string name="signed_out_submit">Znovu se přihlásit</string>
<string name="soft_logout_title">Právě jste se odhlásili</string>
<string name="soft_logout_signin_title">Přihlásit se</string>
<string name="soft_logout_signin_notice">Správce Vašeho homeserveru (%1$s) Vás odhlásil z Vašeho účtu %2$s (%3$s).</string>
<string name="soft_logout_signin_e2e_warning_notice">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í.</string>
<string name="soft_logout_signin_submit">Přihlásit</string>
<string name="soft_logout_clear_data_title">Vyčistit osobní údaje</string>
<string name="soft_logout_clear_data_notice">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.</string>
<string name="soft_logout_clear_data_submit">Vyčistit všechna data</string>
<string name="soft_logout_clear_data_dialog_title">Vyčistit data</string>
<string name="soft_logout_clear_data_dialog_content">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.</string>
<string name="soft_logout_clear_data_dialog_e2e_warning_content">Ztratíte přístup k šifrovaným zprávám, pokud se nepřihlásíte za účelem obnovy šifrovacích klíčů.</string>
<string name="soft_logout_clear_data_dialog_submit">Vyčistit data</string>
<string name="soft_logout_sso_not_same_user_error">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.</string>
<string name="permalink_malformed">Váš odkaz matrix.to byl chybný</string>
<string name="bug_report_error_too_short">Popis je příliš krátký</string>
<string name="notification_initial_sync">Prvotní sync…</string>
<string name="settings_show_devices_list">Ukázat všechny mé relace</string>
<string name="settings_advanced_settings">Pokročilá nastavení</string>
<string name="settings_developer_mode">Vývojářský režim</string>
<string name="settings_developer_mode_summary">Vývojářský režim aktivuje skryté funkce a může tak učinit aplikaci méně stabilní. Jen pro vývojáře!</string>
<string name="settings_rageshake">Rageshake</string>
<string name="settings_rageshake_detection_threshold">Práh detekce</string>
<string name="settings_rageshake_detection_threshold_summary">Pro test prahu detekce zatřeste svým telefonem</string>
<string name="rageshake_detected">Zatřesení detekováno!</string>
<string name="settings">Nastavení</string>
<string name="devices_current_device">Nynější relace</string>
<string name="devices_other_devices">Jiné relace</string>
<string name="autocomplete_limited_results">Ukazuji jen první výsledky, zadejte více znaků…</string>
<string name="settings_developer_mode_fail_fast_title">Fail-fast</string>
<string name="settings_developer_mode_fail_fast_summary">RiotX se může zbořit častěji, když se objeví neočekávané chyby</string>
<string name="command_description_verify">Požadavek ověření daného uživatelského ID</string>
<string name="command_description_shrug">Předsune ¯\\_(ツ)_/¯ do textové zprávy</string>
<string name="create_room_encryption_title">Zapnout šifrování</string>
<string name="create_room_encryption_description">Jakmile zapnuto, šifrování nelze vypnout.</string>
<string name="login_error_threepid_denied">Vaše emailová doména není autorizována registrovat se na tomto serveru</string>
<string name="verification_conclusion_warning">Nedůvěryhodné přihlášení</string>
<string name="verification_sas_match">Shodují se</string>
<string name="verification_sas_do_not_match">Neshodují se</string>
<string name="verify_user_sas_emoji_help_text">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í.</string>
<string name="verify_user_sas_emoji_security_tip">Pro nejvyšší zabezpečení použijte další důvěryhodný způsob komunikace nebo proveďte osobně.</string>
<string name="verification_green_shield">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.</string>
<string name="verification_conclusion_not_secure">Nezabezpečené</string>
<string name="verification_conclusion_compromised">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ů</string>
<string name="sent_a_video">Video.</string>
<string name="sent_an_image">Obrázek.</string>
<string name="sent_an_audio_file">Audio</string>
<string name="sent_a_file">Soubor</string>
<string name="verification_request_waiting">Čekám…</string>
<string name="verification_request_other_cancelled">%s zrušeno</string>
<string name="verification_request_you_cancelled">Zrušili jste</string>
<string name="verification_request_other_accepted">%s přijal</string>
<string name="verification_request_you_accepted">Přijali jste</string>
<string name="verification_sent">Ověření odesláno</string>
<string name="verification_request">Požadavek na ověření</string>
<string name="verification_verify_device">Ověřit tuto relaci</string>
<string name="verification_verify_device_manually">Ověřit manuálně</string>
<string name="you">Vy</string>
<string name="verification_scan_notice">Pro bezpečné vzájemné ověření oskenujte kód zařízením druhého uživatele</string>
<string name="verification_scan_their_code">Skenovat kód</string>
<string name="verification_scan_emoji_title">Nelze skenovat</string>
<string name="verification_scan_emoji_subtitle">V případě neosobního ověření porovnejte emoji</string>
<string name="verification_no_scan_emoji_title">Ověřit porovnáním emoji</string>
<string name="verify_by_emoji_title">Ověřit pomocí emoji</string>
<string name="verify_by_emoji_description">Nemůžete-li skenovat kód nahoře, ověřte porovnáním krátkého, ojedinělého výběru emoji.</string>
<string name="a13n_qr_code_description">Obrázek QR kódu</string>
<string name="verification_verify_user">Ověřit %s</string>
<string name="verification_verified_user">Ověřeno %s</string>
<string name="verification_request_waiting_for">Čekám na %s…</string>
<string name="verification_request_alert_description">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ě.</string>
<string name="room_profile_not_encrypted_subtitle">Zprávy v této místnosti nejsou šifrovány end-to-end.</string>
<string name="room_profile_encrypted_subtitle">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í.</string>
<string name="room_profile_section_security">Zabezpečení</string>
<string name="room_profile_section_security_learn_more">Dozvědět se víc</string>
<string name="room_profile_section_more">Více</string>
<string name="room_profile_section_more_settings">Nastavení místnosti</string>
<string name="room_profile_section_more_notifications">Oznámení</string>
<plurals name="room_profile_section_more_member_list">
<item quantity="one">Jedna osoba</item>
<item quantity="few">%1$d osoby</item>
<item quantity="other">%1$d osob</item>
</plurals>
<string name="room_profile_section_more_uploads">Nahrání</string>
<string name="room_profile_section_more_leave">Opustit místnost</string>
<string name="room_profile_leaving_room">Opouštím místnost…</string>
<string name="room_member_power_level_admins">Správci</string>
<string name="room_member_power_level_moderators">Moderátoři</string>
<string name="room_member_power_level_custom">Vlastní</string>
<string name="room_member_power_level_invites">Pozvánky</string>
<string name="room_member_power_level_users">Uživatelé</string>
<string name="room_member_power_level_admin_in">Správce v %1$s</string>
<string name="room_member_power_level_moderator_in">Moderátor v %1$s</string>
<string name="room_member_power_level_custom_in">Vlastní (%1$d) in %2$s</string>
<string name="room_member_jump_to_read_receipt">Přeskočit k potvrzení přečtení</string>
<string name="rendering_event_error_type_of_event_not_handled">RiotX neobstarává události typu \'%1$s\'</string>
<string name="rendering_event_error_type_of_message_not_handled">RiotX neobstarává zprávy typu \'%1$s\'</string>
<string name="rendering_event_error_exception">RiotX narazil na chybu při převádění obsahu události s id \'%1$s\'</string>
<string name="unignore">Odignorovat</string>
<string name="verify_cannot_cross_sign">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.</string>
<string name="room_list_sharing_header_recent_rooms">Poslední místnosti</string>
<string name="room_list_sharing_header_other_rooms">Další místnosti</string>
<string name="command_description_rainbow">Odešle danou zprávu zabarvenou jako duha</string>
<string name="command_description_rainbow_emote">Odešle daný emote zabarvený jako duha</string>
<string name="settings_category_timeline">Časová osa</string>
<string name="settings_category_composer">Editor zpráv</string>
<string name="room_settings_enable_encryption">Zapnout šifrování end-to-end</string>
<string name="room_settings_enable_encryption_warning">Jakmile zapnuto, šifrování nelze vypnout.</string>
<string name="room_settings_enable_encryption_dialog_title">Zapnout šifrování\?</string>
<string name="room_settings_enable_encryption_dialog_content">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.</string>
<string name="room_settings_enable_encryption_dialog_submit">Zapnout šifrování</string>
<string name="verification_request_notice">Za účelem bezpečnosti ověřte %s kontrolou jednorázového kódu.</string>
<string name="verification_request_start_notice">Za účelem bezpečnosti to proveďte osobně nebo použijte jiný způsob komunikace.</string>
<string name="verification_emoji_notice">Porovnejte jedinečná emoji a ujistěte se, že se ukážou ve stejném pořadí.</string>
<string name="verification_code_notice">Porovnejte kód s tím na obrazovce druhého uživatele.</string>
<string name="verification_conclusion_ok_notice">Zprávy s tímto uživatelem jsou zašifrovány end-to-end a nemohou být čteny třetími stranami.</string>
<string name="verification_conclusion_ok_self_notice">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.</string>
<string name="encryption_information_cross_signing_state">Křížový podpis</string>
<string name="encryption_information_dg_xsigning_complete">Křížový podpis je zapnut.
\nPrivátní klíče v zařízení.</string>
<string name="encryption_information_dg_xsigning_trusted">Křížový podpis je zapnut
\nKlíče jsou důvěryhodné.
\nPrivátní klíče nejsou známy</string>
<string name="encryption_information_dg_xsigning_not_trusted">Křížový podpis je zapnut.
\nKlíče nejsou důvěryhodné</string>
<string name="encryption_information_dg_xsigning_disabled">Křížový podpis není zapnut</string>
<string name="settings_active_sessions_list">Aktivní relace</string>
<string name="settings_active_sessions_show_all">Ukázat všechny relace</string>
<string name="settings_active_sessions_manage">Správa relací</string>
<string name="settings_active_sessions_signout_device">Odhlásit se z této relace</string>
<string name="settings_failed_to_get_crypto_device_info">Žádná kryptografická informace není k dispozici</string>
<string name="settings_active_sessions_verified_device_desc">Tato relace je důvěryhodná pro bezpečnou komunikaci, protože jste ji ověřili:</string>
<string name="settings_active_sessions_unverified_device_desc">Ověřte tuto relaci a tím ji označíte za důvěryhodnou &amp; 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:</string>
<plurals name="settings_active_sessions_count">
<item quantity="one">%d aktivní relace</item>
<item quantity="few">%d aktivní relace</item>
<item quantity="other">%d aktivních relací</item>
</plurals>
<string name="crosssigning_verify_this_session">Ověřit tuto relaci</string>
<string name="crosssigning_other_user_not_trust">Ostatní uživatelé ji nemusí důvěřovat</string>
<string name="complete_security">Dokončit zabezpečení</string>
<string name="verification_open_other_to_verify">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.</string>
<string name="verification_profile_verify">Ověřit</string>
<string name="verification_profile_verified">Ověřeno</string>
<string name="verification_profile_warning">Varování</string>
<string name="room_member_profile_failed_to_get_devices">Načtení relací selhalo</string>
<string name="room_member_profile_sessions_section_title">Relace</string>
<string name="trusted">Důvěryhodné</string>
<string name="not_trusted">Nedůvěryhodné</string>
<string name="verification_profile_device_verified_because">Tato relace je důvěryhodná pro bezpečnou komunikaci, protože %1$s (%2$s) ji ověřili:</string>
<string name="verification_profile_device_new_signing">%1$s (%2$s) se příhlásili skrze novou relaci:</string>
<string name="verification_profile_device_untrust_info">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.</string>
<string name="initialize_cross_signing">Spustit křížové podepsání</string>
<string name="reset_cross_signing">Resetovat klíče</string>
<string name="a11y_qr_code_for_verification">QR kód</string>
<string name="qr_code_scanned_by_other_notice">Skoro u konce! Ukazuje %s totožný štít\?</string>
<string name="qr_code_scanned_by_other_yes">Ano</string>
<string name="qr_code_scanned_by_other_no">Ne</string>
<string name="no_connectivity_to_the_server_indicator">Spojení k serveru bylo ztraceno</string>
<string name="settings_dev_tools">Vývojářské nástroje</string>
<string name="settings_account_data">Údaje účtu</string>
<plurals name="poll_info">
<item quantity="one">%d hlas</item>
<item quantity="few">%d hlasy</item>
<item quantity="other">%d hlasů</item>
</plurals>
<plurals name="poll_info_final">
<item quantity="one">%d hlas - Konečné výsledky</item>
<item quantity="few">%d hlasy - Konečné výsledky</item>
<item quantity="other">%d hlasů - Konečné výsledky</item>
</plurals>
<string name="poll_item_selected_aria">Zvolená možnost</string>
<string name="command_description_poll">Založí jednoduchou anketu</string>
<string name="verification_cannot_access_other_session">Použijte metodu obnovy</string>
<string name="verification_use_passphrase">Pokud se nemůžete dostat do existující relace</string>
<string name="new_signin">Nové přihlášení</string>
<string name="enter_secret_storage_invalid">Nemohu najít přihlašovací data v úložišti</string>
<string name="enter_secret_storage_passphrase">Zadejte heslo pro úložište údajů</string>
<string name="enter_secret_storage_passphrase_warning">Varování:</string>
<string name="enter_secret_storage_passphrase_warning_text">Měli byste otevřít úložiště údajů z důvěryhodného zařízení</string>
<string name="message_action_item_redact">Odstranit…</string>
<string name="share_confirm_room">Chcete %1$s poslat tuto přílohu\?</string>
<plurals name="send_images_with_original_size">
<item quantity="one">Odeslat obrázek v původní velikosti</item>
<item quantity="few">Odeslat obrázky v původní velikosti</item>
<item quantity="other">Odeslat obrázky v původní velikosti</item>
</plurals>
<string name="delete_event_dialog_title">Potvrďte odstranění</string>
<string name="delete_event_dialog_content">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.</string>
<string name="delete_event_dialog_reason_checkbox">Udejte důvod</string>
<string name="delete_event_dialog_reason_hint">Důvod pro úpravu</string>
<string name="event_redacted_by_user_reason_with_reason">Událost smazána uživatelem, důvod: %1$s</string>
<string name="event_redacted_by_admin_reason_with_reason">Událost moderována správcem místnosti, důvod: %1$s</string>
<string name="keys_backup_restore_success_title_already_up_to_date">Klíče jsou již aktuální!</string>
<string name="login_mobile_device_riotx">RiotX Android</string>
<string name="settings_key_requests">Požadavky na klíče</string>
<string name="e2e_use_keybackup">Odemknout zašifrovanou historii zpráv</string>
<string name="refresh">Obnovit</string>
<string name="new_session">Neověřené přihlášení. Byli jste to Vy\?</string>
<string name="new_session_review">Klepněte pro přehled &amp; ověření</string>
<string name="verify_new_session_notice">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.</string>
<string name="verify_new_session_was_not_me">To jsem nebyl(a) já</string>
<string name="verify_new_session_compromized">Váš účet může být ohrožen</string>
<string name="verify_cancel_self_verification_from_untrusted">Pokud přerušíte, nebudete moci číst zašifrované zprávy na tomto zařízení a ostatní uživatelé mu nebudou důvěřovat</string>
<string name="verify_cancel_self_verification_from_trusted">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</string>
<string name="verify_cancel_other">Nebudete moci ověřit %1$s (%2$s), pokud nyní přerušíte. Začněte znovu v jejich uživatelském profilu.</string>
<string name="verify_not_me_self_verification">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 &amp; klíč obnovy v nastavení.</string>
<string name="verify_cancelled_notice">Ověřit svá zařízení v nastavení.</string>
<string name="verification_cancelled">Ověření zrušeno</string>
<string name="recovery_passphrase">Heslo obnovy</string>
<string name="message_key">Klíč zpráv</string>
<string name="account_password">Heslo účtu</string>
<string name="set_recovery_passphrase">Nastavte %s</string>
<string name="generate_message_key">Generovat klíč zpráv</string>
<string name="confirm_recovery_passphrase">Potvrďte %s</string>
<string name="enter_account_password">Zadejte své %s a pokračujte.</string>
<string name="bootstrap_info_text">Zabezpečit &amp; odemknout zašifrované zprávy a důvěru s %s.</string>
<string name="bootstrap_info_confirm_text">Zadejte opět své %s a potvrďte.</string>
<string name="bootstrap_dont_reuse_pwd">Nepoužívejte heslo účtu opakovaně.</string>
<string name="bootstrap_loading_text">To může několik vteřin trvat, prosím, buďte trpěliví.</string>
<string name="bootstrap_loading_title">Nastavuji obnovení.</string>
<string name="your_recovery_key">Váš klíč obnovení</string>
<string name="bootstrap_finish_title">Hotovo!</string>
<string name="keep_it_safe">Udržujte v bezpečí</string>
<string name="finish">Dokončit</string>
<string name="bootstrap_save_key_description">Použijte tento %1$s jako záchrannou síť v případě, že zapomenete své %2$s.</string>
<string name="bootstrap_crosssigning_progress_initializing">Zvěřejňuji založené klíče identity</string>
<string name="bootstrap_crosssigning_progress_pbkdf2">Generuji zabezpečené klíče z hesla</string>
<string name="bootstrap_crosssigning_progress_default_key">Určuji výchozí klíč SSSS</string>
<string name="bootstrap_crosssigning_progress_save_msk">Synchronizuji hlavní klíč</string>
<string name="bootstrap_crosssigning_progress_save_usk">Synchronizuji uživatelský klíč</string>
<string name="bootstrap_crosssigning_progress_save_ssk">Synchronizuji sebepodpisový klíč</string>
<string name="bootstrap_crosssigning_progress_key_backup">Nastavuji zálohu klíčů</string>
<string name="bootstrap_cross_signing_success">Vaše %2$s &amp; %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.</string>
<string name="bootstrap_crosssigning_print_it">Vytiskněte a uložte na bezpečném místě</string>
<string name="bootstrap_crosssigning_save_usb">Uložte je na USB nebo zálohový disk</string>
<string name="bootstrap_crosssigning_save_cloud">Nahrajte do svého osobního úložiště v cloudu</string>
<string name="auth_flow_not_supported">To nelze provést z mobilního zařízení</string>
<string name="bootstrap_skip_text">Nastavení hesla pro obnovení Vám umožní zabezpečit &amp; odemknout zašifrované zprávy a důvěryhodnost.
\n
\nPokud nechcete nastavit heslo pro zprávy, založte klíč pro zprávy.</string>
<string name="bootstrap_skip_text_no_gen_key">Nastavení hesla pro obnovení Vám umožní zabezpečit &amp; odemknout zašifrované zprávy a důvěryhodnost.</string>
<string name="encryption_enabled">Šifrování zapnuto</string>
<string name="encryption_enabled_tile_description">Zprávy v této místnosti jsou zašifrovány end-to-end. Poučte se více &amp; ověřte uživatele v jejich profilech.</string>
<string name="encryption_not_enabled">Šifrování není zapnuto</string>
<string name="encryption_unknown_algorithm_tile_description">Šifrování použité v této místnosti není podporováno</string>
<string name="room_created_summary_item">%s založil a nastavil tuto místnost.</string>
<string name="qr_code_scanned_self_verif_notice">Skoro u konce! Ukazuje druhé zařízení stejný štít\?</string>
<string name="qr_code_scanned_verif_waiting_notice">Skoro u konce! Čekám na potvrzení…</string>
<string name="qr_code_scanned_verif_waiting">Čekám na %s…</string>
<string name="error_failed_to_import_keys">Import klíčů selhal</string>
<string name="settings_notification_configuration">Konfigurace oznámení</string>
<string name="settings_messages_at_room">Zprávy obsahující @room</string>
<string name="settings_messages_in_e2e_one_to_one">Zašifrované zprávy v chatech one-to-one</string>
<string name="settings_messages_in_e2e_group_chat">Zašifrované zprávy ve skupinových chatech</string>
<string name="settings_when_rooms_are_upgraded">Když dojde k upgradu místností</string>
<string name="settings_troubleshoot_title">Řešit problémy</string>
<string name="settings_notification_advanced_summary_riotx">Nastavit důležitost oznámení podle události</string>
<string name="command_description_plain">Odešle zprávu jako prostý text bez interpretace markdown</string>
<string name="auth_invalid_login_param_space_in_password">Nesprávné uživatelské jméno a/nebo heslo. Zadané heslo začíná nebo končí mezerami, prosím, zkontrolovat.</string>
<string name="room_message_placeholder">Zpráva…</string>
<string name="upgrade_security">Upgrade šifrování je k dispozici</string>
<string name="security_prompt_text">Ověřit sebe &amp; ostatní za účelem bezpečí chatů</string>
<string name="bootstrap_enter_recovery">Zadejte svůj %s a pokračujte</string>
<string name="use_file">Použít soubor</string>
<string name="enter_backup_passphrase">Zadejte %s</string>
<string name="backup_recovery_passphrase">Heslo obnovení</string>
<string name="bootstrap_invalid_recovery_key">To není platný klíč obnovení</string>
<string name="recovery_key_empty_error_message">Prosím, zadejte klíč obnovení</string>
<string name="bootstrap_progress_checking_backup">Kontroluji klíč zálohy</string>
<string name="bootstrap_progress_checking_backup_with_info">Kontroluji klíč zálohy (%s)</string>
<string name="bootstrap_progress_compute_curve_key">Generuji klíč curve</string>
<string name="bootstrap_progress_generating_ssss">Generuji klíč SSSS z hesla</string>
<string name="bootstrap_progress_generating_ssss_with_info">Generuji klíč SSSS z hesla (%s)</string>
<string name="bootstrap_progress_generating_ssss_recovery">Generuji klíč SSSS z klíče obnovení</string>
<string name="bootstrap_progress_storing_in_sss">Ukládám heslo pro zálohu klíče v SSSS</string>
<string name="new_session_review_with_info">%1$s (%2$s)</string>
<string name="bootstrap_migration_enter_backup_password">Zadejte své heslo pro zálohu klíče a pokračujte.</string>
<string name="bootstrap_migration_use_recovery_key">použít svůj klíč obnovy zálohy klíčů</string>
<string name="bootstrap_migration_with_passphrase_helper_with_link">Neznáte-li své heslo zálohy klíčů, můžete %s.</string>
<string name="bootstrap_migration_backup_recovery_key">Klíč pro obnovu zálohy klíčů</string>
<string name="settings_security_prevent_screenshots_title">Zamezte screenshotům aplikace</string>
<string name="settings_security_prevent_screenshots_summary">Zapnutí toho nastavení doplní značku FLAG_SECURE ke všem aktivitám. Pro aktivaci změny restartujte aplikaci.</string>
<string name="media_file_added_to_gallery">Mediální soubor byl připojen do galerie</string>
<string name="error_adding_media_file_to_gallery">Mediální soubor nemohl být připojen do galerie</string>
<string name="change_password_summary">Nastavit nové heslo účtu…</string>
</resources> </resources>

Some files were not shown because too many files have changed in this diff Show More