mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-15 01:35:07 +08:00
Merge branch 'develop' into feature/e2e_timeline_decoration
This commit is contained in:
commit
7b20db64a5
@ -29,6 +29,8 @@ Improvements 🙌:
|
||||
- Cross-Signing | Hide Use recovery key when 4S is not setup (#1007)
|
||||
- Cross-Signing | Trust account xSigning keys by entering Recovery Key (select file or copy) #1199
|
||||
- E2E timeline decoration (#1279)
|
||||
- Manage Session Settings / Cross Signing update (#1295)
|
||||
- Cross-Signing | Review sessions toast update old vs new (#1293, #1306)
|
||||
|
||||
Bugfix 🐛:
|
||||
- Fix summary notification staying after "mark as read"
|
||||
@ -44,6 +46,9 @@ Bugfix 🐛:
|
||||
- RiotX now uses as many threads as it needs to do work and send messages (#1221)
|
||||
- Fix issue with media path (#1227)
|
||||
- Add user to direct chat by user id (#1065)
|
||||
- Use correct URL for SSO connection (#1178)
|
||||
- Emoji completion :tada: does not completes to 🎉 like on web (#1285)
|
||||
- Fix bad Shield Logic for DM (#963)
|
||||
|
||||
Translations 🗣:
|
||||
-
|
||||
|
@ -31,6 +31,8 @@ import im.vector.matrix.android.api.util.JsonDict
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import im.vector.matrix.android.api.util.toOptional
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
@ -58,6 +60,13 @@ class RxSession(private val session: Session) {
|
||||
}
|
||||
}
|
||||
|
||||
fun liveMyDeviceInfo(): Observable<List<DeviceInfo>> {
|
||||
return session.cryptoService().getLiveMyDevicesInfo().asObservable()
|
||||
.startWithCallable {
|
||||
session.cryptoService().getMyDevicesInfo()
|
||||
}
|
||||
}
|
||||
|
||||
fun liveSyncState(): Observable<SyncState> {
|
||||
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>> {
|
||||
return session.getLiveAccountDataEvents(types).asObservable()
|
||||
.startWithCallable {
|
||||
|
@ -20,6 +20,8 @@ import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreModule
|
||||
import im.vector.matrix.android.internal.crypto.store.db.mapper.CrossSigningKeysMapper
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import io.realm.RealmConfiguration
|
||||
import kotlin.random.Random
|
||||
|
||||
@ -31,6 +33,7 @@ internal class CryptoStoreHelper {
|
||||
.name("test.realm")
|
||||
.modules(RealmCryptoStoreModule())
|
||||
.build(),
|
||||
crossSigningKeysMapper = CrossSigningKeysMapper(MoshiProvider.providesMoshi()),
|
||||
credentials = createCredential())
|
||||
}
|
||||
|
||||
|
@ -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"
|
@ -16,6 +16,10 @@
|
||||
|
||||
package im.vector.matrix.android.api.failure
|
||||
|
||||
import im.vector.matrix.android.api.extensions.tryThis
|
||||
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import java.io.IOException
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
|
||||
fun Throwable.is401() =
|
||||
@ -29,6 +33,7 @@ fun Throwable.isTokenError() =
|
||||
|
||||
fun Throwable.shouldBeRetried(): Boolean {
|
||||
return this is Failure.NetworkConnection
|
||||
|| this is IOException
|
||||
|| (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED)
|
||||
}
|
||||
|
||||
@ -37,3 +42,18 @@ fun Throwable.isInvalidPassword(): Boolean {
|
||||
&& error.code == MatrixError.M_FORBIDDEN
|
||||
&& error.message == "Invalid password"
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to convert to a RegistrationFlowResponse. Return null in the cases it's not possible
|
||||
*/
|
||||
fun Throwable.toRegistrationFlowResponse(): RegistrationFlowResponse? {
|
||||
return if (this is Failure.OtherServerError && this.httpCode == 401) {
|
||||
tryThis {
|
||||
MoshiProvider.providesMoshi()
|
||||
.adapter(RegistrationFlowResponse::class.java)
|
||||
.fromJson(this.errorBody)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
@ -98,7 +98,9 @@ interface CryptoService {
|
||||
|
||||
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>)
|
||||
|
||||
|
@ -55,6 +55,8 @@ interface CrossSigningService {
|
||||
|
||||
fun getCrossSigningPrivateKeys(): PrivateKeysInfo?
|
||||
|
||||
fun getLiveCrossSigningPrivateKeys(): LiveData<Optional<PrivateKeysInfo>>
|
||||
|
||||
fun canCrossSign(): Boolean
|
||||
|
||||
fun trustUser(otherUserId: String,
|
||||
|
@ -50,7 +50,6 @@ data class RoomSummary constructor(
|
||||
val inviterId: String? = null,
|
||||
val typingRoomMemberIds: List<String> = emptyList(),
|
||||
val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS,
|
||||
// TODO Plug it
|
||||
val roomEncryptionTrustLevel: RoomEncryptionTrustLevel? = null
|
||||
) {
|
||||
|
||||
|
@ -18,8 +18,8 @@ package im.vector.matrix.android.internal.auth.registration
|
||||
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.api.failure.toRegistrationFlowResponse
|
||||
import im.vector.matrix.android.internal.auth.AuthAPI
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
|
||||
@ -39,25 +39,9 @@ internal class DefaultRegisterTask(
|
||||
apiCall = authAPI.register(params.registrationParams)
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
if (throwable is Failure.OtherServerError && throwable.httpCode == 401) {
|
||||
// Parse to get a RegistrationFlowResponse
|
||||
val registrationFlowResponse = try {
|
||||
MoshiProvider.providesMoshi()
|
||||
.adapter(RegistrationFlowResponse::class.java)
|
||||
.fromJson(throwable.errorBody)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
// check if the server response can be cast
|
||||
if (registrationFlowResponse != null) {
|
||||
throw Failure.RegistrationFlowError(registrationFlowResponse)
|
||||
} else {
|
||||
throw throwable
|
||||
}
|
||||
} else {
|
||||
// Other error
|
||||
throw throwable
|
||||
}
|
||||
throw throwable.toRegistrationFlowResponse()
|
||||
?.let { Failure.RegistrationFlowError(it) }
|
||||
?: throwable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -112,6 +112,7 @@ internal abstract class CryptoModule {
|
||||
@SessionScope
|
||||
fun providesRealmConfiguration(@SessionFilesDirectory directory: File,
|
||||
@UserMd5 userMd5: String,
|
||||
realmCryptoStoreMigration: RealmCryptoStoreMigration,
|
||||
realmKeysUtils: RealmKeysUtils): RealmConfiguration {
|
||||
return RealmConfiguration.Builder()
|
||||
.directory(directory)
|
||||
@ -121,7 +122,7 @@ internal abstract class CryptoModule {
|
||||
.name("crypto_store.realm")
|
||||
.modules(RealmCryptoStoreModule())
|
||||
.schemaVersion(RealmCryptoStoreMigration.CRYPTO_STORE_SCHEMA_VERSION)
|
||||
.migration(RealmCryptoStoreMigration)
|
||||
.migration(realmCryptoStoreMigration)
|
||||
.build()
|
||||
}
|
||||
|
||||
|
@ -251,15 +251,33 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
return myDeviceInfoHolder.get().myDevice
|
||||
}
|
||||
|
||||
override fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) {
|
||||
override fun fetchDevicesList(callback: MatrixCallback<DevicesListResponse>) {
|
||||
getDevicesTask
|
||||
.configureWith {
|
||||
// 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)
|
||||
}
|
||||
|
||||
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>) {
|
||||
getDeviceInfoTask
|
||||
.configureWith(GetDeviceInfoTask.Params(deviceId)) {
|
||||
@ -318,6 +336,8 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
internalStart(isInitialSync)
|
||||
}
|
||||
// Just update
|
||||
fetchDevicesList(NoOpMatrixCallback())
|
||||
}
|
||||
|
||||
private suspend fun internalStart(isInitialSync: Boolean) {
|
||||
|
@ -19,6 +19,7 @@ import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
|
||||
import im.vector.matrix.android.api.extensions.orFalse
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
@ -26,17 +27,28 @@ import javax.inject.Inject
|
||||
|
||||
internal interface ComputeTrustTask : Task<ComputeTrustTask.Params, RoomEncryptionTrustLevel> {
|
||||
data class Params(
|
||||
val userIds: List<String>
|
||||
val activeMemberUserIds: List<String>,
|
||||
val isDirectRoom: Boolean
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultComputeTrustTask @Inject constructor(
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
@UserId private val userId: String,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers
|
||||
) : ComputeTrustTask {
|
||||
|
||||
override suspend fun execute(params: ComputeTrustTask.Params): RoomEncryptionTrustLevel = withContext(coroutineDispatchers.crypto) {
|
||||
val allTrustedUserIds = params.userIds
|
||||
// The set of “all users” depends on the type of room:
|
||||
// For regular / topic rooms, all users including yourself, are considered when decorating a room
|
||||
// For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room
|
||||
val listToCheck = if (params.isDirectRoom) {
|
||||
params.activeMemberUserIds.filter { it != userId }
|
||||
} else {
|
||||
params.activeMemberUserIds
|
||||
}
|
||||
|
||||
val allTrustedUserIds = listToCheck
|
||||
.filter { userId -> getUserCrossSigningKeys(userId)?.isTrusted() == true }
|
||||
|
||||
if (allTrustedUserIds.isEmpty()) {
|
||||
@ -60,7 +72,7 @@ internal class DefaultComputeTrustTask @Inject constructor(
|
||||
if (hasWarning) {
|
||||
RoomEncryptionTrustLevel.Warning
|
||||
} else {
|
||||
if (params.userIds.size == allTrustedUserIds.size) {
|
||||
if (listToCheck.size == allTrustedUserIds.size) {
|
||||
// all users are trusted and all devices are verified
|
||||
RoomEncryptionTrustLevel.Trusted
|
||||
} else {
|
||||
|
@ -470,6 +470,10 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||
return cryptoStore.getCrossSigningPrivateKeys()
|
||||
}
|
||||
|
||||
override fun getLiveCrossSigningPrivateKeys(): LiveData<Optional<PrivateKeysInfo>> {
|
||||
return cryptoStore.getLiveCrossSigningPrivateKeys()
|
||||
}
|
||||
|
||||
override fun canCrossSign(): Boolean {
|
||||
return checkSelfTrust().isVerified() && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null
|
||||
&& cryptoStore.getCrossSigningPrivateKeys()?.user != null
|
||||
|
@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.crypto.crosssigning
|
||||
|
||||
data class SessionToCryptoRoomMembersUpdate(
|
||||
val roomId: String,
|
||||
val isDirect: Boolean,
|
||||
val userIds: List<String>
|
||||
)
|
||||
|
||||
|
@ -15,18 +15,20 @@
|
||||
*/
|
||||
package im.vector.matrix.android.internal.crypto.crosssigning
|
||||
|
||||
import im.vector.matrix.android.api.extensions.orFalse
|
||||
import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields
|
||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
|
||||
import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import im.vector.matrix.android.internal.util.createBackgroundHandler
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import kotlinx.coroutines.android.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import timber.log.Timber
|
||||
@ -38,13 +40,13 @@ internal class ShieldTrustUpdater @Inject constructor(
|
||||
private val eventBus: EventBus,
|
||||
private val computeTrustTask: ComputeTrustTask,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
@SessionDatabase private val sessionRealmConfiguration: RealmConfiguration,
|
||||
private val roomSummaryUpdater: RoomSummaryUpdater
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private val BACKGROUND_HANDLER = createBackgroundHandler("SHIELD_CRYPTO_DB_THREAD")
|
||||
private val BACKGROUND_HANDLER_DISPATCHER = BACKGROUND_HANDLER.asCoroutineDispatcher()
|
||||
}
|
||||
|
||||
private val backgroundSessionRealm = AtomicReference<Realm>()
|
||||
@ -76,14 +78,11 @@ internal class ShieldTrustUpdater @Inject constructor(
|
||||
if (!isStarted.get()) {
|
||||
return
|
||||
}
|
||||
taskExecutor.executorScope.launch(coroutineDispatchers.crypto) {
|
||||
val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(update.userIds))
|
||||
taskExecutor.executorScope.launch(BACKGROUND_HANDLER_DISPATCHER) {
|
||||
val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(update.userIds, update.isDirect))
|
||||
// We need to send that back to session base
|
||||
|
||||
BACKGROUND_HANDLER.post {
|
||||
backgroundSessionRealm.get()?.executeTransaction { realm ->
|
||||
roomSummaryUpdater.updateShieldTrust(realm, update.roomId, updatedTrust)
|
||||
}
|
||||
backgroundSessionRealm.get()?.executeTransaction { realm ->
|
||||
roomSummaryUpdater.updateShieldTrust(realm, update.roomId, updatedTrust)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -93,45 +92,31 @@ internal class ShieldTrustUpdater @Inject constructor(
|
||||
if (!isStarted.get()) {
|
||||
return
|
||||
}
|
||||
|
||||
onCryptoDevicesChange(update.userIds)
|
||||
}
|
||||
|
||||
private fun onCryptoDevicesChange(users: List<String>) {
|
||||
BACKGROUND_HANDLER.post {
|
||||
val impactedRoomsId = backgroundSessionRealm.get()?.where(RoomMemberSummaryEntity::class.java)
|
||||
?.`in`(RoomMemberSummaryEntityFields.USER_ID, users.toTypedArray())
|
||||
?.findAll()
|
||||
?.map { it.roomId }
|
||||
?.distinct()
|
||||
taskExecutor.executorScope.launch(BACKGROUND_HANDLER_DISPATCHER) {
|
||||
val realm = backgroundSessionRealm.get() ?: return@launch
|
||||
val distinctRoomIds = realm.where(RoomMemberSummaryEntity::class.java)
|
||||
.`in`(RoomMemberSummaryEntityFields.USER_ID, users.toTypedArray())
|
||||
.distinct(RoomMemberSummaryEntityFields.ROOM_ID)
|
||||
.findAll()
|
||||
.map { it.roomId }
|
||||
|
||||
val map = HashMap<String, List<String>>()
|
||||
impactedRoomsId?.forEach { roomId ->
|
||||
backgroundSessionRealm.get()?.let { realm ->
|
||||
RoomMemberSummaryEntity.where(realm, roomId)
|
||||
.findAll()
|
||||
.let { results ->
|
||||
map[roomId] = results.map { it.userId }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
map.forEach { entry ->
|
||||
val roomId = entry.key
|
||||
val userList = entry.value
|
||||
taskExecutor.executorScope.launch {
|
||||
withContext(coroutineDispatchers.crypto) {
|
||||
try {
|
||||
// Can throw if the crypto database has been closed in between, in this case log and ignore?
|
||||
val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(userList))
|
||||
BACKGROUND_HANDLER.post {
|
||||
backgroundSessionRealm.get()?.executeTransaction { realm ->
|
||||
roomSummaryUpdater.updateShieldTrust(realm, roomId, updatedTrust)
|
||||
}
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e(failure)
|
||||
distinctRoomIds.forEach { roomId ->
|
||||
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
|
||||
if (roomSummary?.isEncrypted.orFalse()) {
|
||||
val allActiveRoomMembers = RoomMemberHelper(realm, roomId).getActiveRoomMemberIds()
|
||||
try {
|
||||
val updatedTrust = computeTrustTask.execute(
|
||||
ComputeTrustTask.Params(allActiveRoomMembers, roomSummary?.isDirect == true)
|
||||
)
|
||||
realm.executeTransaction {
|
||||
roomSummaryUpdater.updateShieldTrust(it, roomId, updatedTrust)
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,8 @@ data class CryptoDeviceInfo(
|
||||
override val signatures: Map<String, Map<String, String>>? = null,
|
||||
val unsigned: JsonDict? = null,
|
||||
var trustLevel: DeviceTrustLevel? = null,
|
||||
var isBlocked: Boolean = false
|
||||
var isBlocked: Boolean = false,
|
||||
val firstTimeSeenLocalTs: Long? = null
|
||||
) : CryptoInfo {
|
||||
|
||||
val isVerified: Boolean
|
||||
|
@ -61,20 +61,4 @@ internal object CryptoInfoMapper {
|
||||
signatures = keyInfo.signatures
|
||||
)
|
||||
}
|
||||
|
||||
fun RestDeviceInfo.toCryptoModel(): CryptoDeviceInfo {
|
||||
return map(this)
|
||||
}
|
||||
|
||||
fun CryptoDeviceInfo.toRest(): RestDeviceInfo {
|
||||
return map(this)
|
||||
}
|
||||
|
||||
// fun RestKeyInfo.toCryptoModel(): CryptoCrossSigningKey {
|
||||
// return map(this)
|
||||
// }
|
||||
|
||||
fun CryptoCrossSigningKey.toRest(): RestKeyInfo {
|
||||
return map(this)
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
|
||||
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
|
||||
import org.matrix.olm.OlmAccount
|
||||
@ -218,6 +219,9 @@ internal interface IMXCryptoStore {
|
||||
// TODO temp
|
||||
fun getLiveDeviceList(): LiveData<List<CryptoDeviceInfo>>
|
||||
|
||||
fun getMyDevicesInfo() : List<DeviceInfo>
|
||||
fun getLiveMyDevicesInfo() : LiveData<List<DeviceInfo>>
|
||||
fun saveMyDevicesInfo(info: List<DeviceInfo>)
|
||||
/**
|
||||
* Store the crypto algorithm for a room.
|
||||
*
|
||||
@ -405,6 +409,7 @@ internal interface IMXCryptoStore {
|
||||
fun storeUSKPrivateKey(usk: String?)
|
||||
|
||||
fun getCrossSigningPrivateKeys(): PrivateKeysInfo?
|
||||
fun getLiveCrossSigningPrivateKeys(): LiveData<Optional<PrivateKeysInfo>>
|
||||
|
||||
fun saveBackupRecoveryKey(recoveryKey: String?, version: String?)
|
||||
fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo?
|
||||
|
@ -62,6 +62,7 @@ fun doRealmTransaction(realmConfiguration: RealmConfiguration, action: (Realm) -
|
||||
realm.executeTransaction { action.invoke(it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun doRealmTransactionAsync(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) {
|
||||
Realm.getInstance(realmConfiguration).use { realm ->
|
||||
realm.executeTransactionAsync { action.invoke(it) }
|
||||
@ -79,31 +80,26 @@ fun serializeForRealm(o: Any?): String? {
|
||||
val baos = ByteArrayOutputStream()
|
||||
val gzis = CompatUtil.createGzipOutputStream(baos)
|
||||
val out = ObjectOutputStream(gzis)
|
||||
|
||||
out.writeObject(o)
|
||||
out.close()
|
||||
|
||||
out.use {
|
||||
it.writeObject(o)
|
||||
}
|
||||
return Base64.encodeToString(baos.toByteArray(), Base64.DEFAULT)
|
||||
}
|
||||
|
||||
/**
|
||||
* Do the opposite of serializeForRealm.
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T> deserializeFromRealm(string: String?): T? {
|
||||
if (string == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val decodedB64 = Base64.decode(string.toByteArray(), Base64.DEFAULT)
|
||||
|
||||
val bais = ByteArrayInputStream(decodedB64)
|
||||
val gzis = GZIPInputStream(bais)
|
||||
val ois = ObjectInputStream(gzis)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val result = ois.readObject() as T
|
||||
|
||||
ois.close()
|
||||
|
||||
return result
|
||||
return ois.use {
|
||||
it.readObject() as T
|
||||
}
|
||||
}
|
||||
|
@ -36,16 +36,17 @@ import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestState
|
||||
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
|
||||
import im.vector.matrix.android.internal.crypto.OutgoingSecretRequest
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
|
||||
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||
import im.vector.matrix.android.internal.crypto.model.toEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo
|
||||
import im.vector.matrix.android.internal.crypto.store.SavedKeyBackupKeyInfo
|
||||
import im.vector.matrix.android.internal.crypto.store.db.mapper.CrossSigningKeysMapper
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMapper
|
||||
@ -59,6 +60,7 @@ import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossiping
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity
|
||||
@ -91,6 +93,7 @@ import kotlin.collections.set
|
||||
@SessionScope
|
||||
internal class RealmCryptoStore @Inject constructor(
|
||||
@CryptoDatabase private val realmConfiguration: RealmConfiguration,
|
||||
private val crossSigningKeysMapper: CrossSigningKeysMapper,
|
||||
private val credentials: Credentials) : IMXCryptoStore {
|
||||
|
||||
/* ==========================================================================================
|
||||
@ -200,9 +203,9 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
}
|
||||
|
||||
override fun getDeviceId(): String {
|
||||
return doRealmQueryAndCopy(realmConfiguration) {
|
||||
it.where<CryptoMetadataEntity>().findFirst()
|
||||
}?.deviceId ?: ""
|
||||
return doWithRealm(realmConfiguration) {
|
||||
it.where<CryptoMetadataEntity>().findFirst()?.deviceId
|
||||
} ?: ""
|
||||
}
|
||||
|
||||
override fun saveOlmAccount() {
|
||||
@ -256,24 +259,25 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
}
|
||||
|
||||
override fun getUserDevice(userId: String, deviceId: String): CryptoDeviceInfo? {
|
||||
return doRealmQueryAndCopy(realmConfiguration) {
|
||||
return doWithRealm(realmConfiguration) {
|
||||
it.where<DeviceInfoEntity>()
|
||||
.equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId))
|
||||
.findFirst()
|
||||
}?.let {
|
||||
CryptoMapper.mapToModel(it)
|
||||
?.let { deviceInfo ->
|
||||
CryptoMapper.mapToModel(deviceInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun deviceWithIdentityKey(identityKey: String): CryptoDeviceInfo? {
|
||||
return doRealmQueryAndCopy(realmConfiguration) {
|
||||
return doWithRealm(realmConfiguration) {
|
||||
it.where<DeviceInfoEntity>()
|
||||
.equalTo(DeviceInfoEntityFields.IDENTITY_KEY, identityKey)
|
||||
.findFirst()
|
||||
?.let { deviceInfo ->
|
||||
CryptoMapper.mapToModel(deviceInfo)
|
||||
}
|
||||
}
|
||||
?.let {
|
||||
CryptoMapper.mapToModel(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun storeUserDevices(userId: String, devices: Map<String, CryptoDeviceInfo>?) {
|
||||
@ -285,10 +289,16 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
UserEntity.getOrCreate(realm, userId)
|
||||
.let { u ->
|
||||
// Add the devices
|
||||
val currentKnownDevices = u.devices.toList()
|
||||
val new = devices.map { entry -> entry.value.toEntity() }
|
||||
new.forEach { entity ->
|
||||
// Maintain first time seen
|
||||
val existing = currentKnownDevices.firstOrNull { it.deviceId == entity.deviceId && it.identityKey == entity.identityKey }
|
||||
entity.firstTimeSeenLocalTs = existing?.firstTimeSeenLocalTs ?: System.currentTimeMillis()
|
||||
realm.insertOrUpdate(entity)
|
||||
}
|
||||
// Ensure all other devices are deleted
|
||||
u.devices.deleteAllFromRealm()
|
||||
val new = devices.map { entry -> entry.value.toEntity() }
|
||||
new.forEach { realm.insertOrUpdate(it) }
|
||||
u.devices.addAll(new)
|
||||
}
|
||||
}
|
||||
@ -309,36 +319,19 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
} else {
|
||||
CrossSigningInfoEntity.getOrCreate(realm, userId).let { signingInfo ->
|
||||
// What should we do if we detect a change of the keys?
|
||||
|
||||
val existingMaster = signingInfo.getMasterKey()
|
||||
if (existingMaster != null && existingMaster.publicKeyBase64 == masterKey.unpaddedBase64PublicKey) {
|
||||
// update signatures?
|
||||
existingMaster.putSignatures(masterKey.signatures)
|
||||
existingMaster.usages = masterKey.usages?.toTypedArray()?.let { RealmList(*it) }
|
||||
?: RealmList()
|
||||
crossSigningKeysMapper.update(existingMaster, masterKey)
|
||||
} else {
|
||||
val keyEntity = realm.createObject(KeyInfoEntity::class.java).apply {
|
||||
this.publicKeyBase64 = masterKey.unpaddedBase64PublicKey
|
||||
this.usages = masterKey.usages?.toTypedArray()?.let { RealmList(*it) }
|
||||
?: RealmList()
|
||||
this.putSignatures(masterKey.signatures)
|
||||
}
|
||||
val keyEntity = crossSigningKeysMapper.map(masterKey)
|
||||
signingInfo.setMasterKey(keyEntity)
|
||||
}
|
||||
|
||||
val existingSelfSigned = signingInfo.getSelfSignedKey()
|
||||
if (existingSelfSigned != null && existingSelfSigned.publicKeyBase64 == selfSigningKey.unpaddedBase64PublicKey) {
|
||||
// update signatures?
|
||||
existingSelfSigned.putSignatures(selfSigningKey.signatures)
|
||||
existingSelfSigned.usages = selfSigningKey.usages?.toTypedArray()?.let { RealmList(*it) }
|
||||
?: RealmList()
|
||||
crossSigningKeysMapper.update(existingSelfSigned, selfSigningKey)
|
||||
} else {
|
||||
val keyEntity = realm.createObject(KeyInfoEntity::class.java).apply {
|
||||
this.publicKeyBase64 = selfSigningKey.unpaddedBase64PublicKey
|
||||
this.usages = selfSigningKey.usages?.toTypedArray()?.let { RealmList(*it) }
|
||||
?: RealmList()
|
||||
this.putSignatures(selfSigningKey.signatures)
|
||||
}
|
||||
val keyEntity = crossSigningKeysMapper.map(selfSigningKey)
|
||||
signingInfo.setSelfSignedKey(keyEntity)
|
||||
}
|
||||
|
||||
@ -346,21 +339,12 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
if (userSigningKey != null) {
|
||||
val existingUSK = signingInfo.getUserSigningKey()
|
||||
if (existingUSK != null && existingUSK.publicKeyBase64 == userSigningKey.unpaddedBase64PublicKey) {
|
||||
// update signatures?
|
||||
existingUSK.putSignatures(userSigningKey.signatures)
|
||||
existingUSK.usages = userSigningKey.usages?.toTypedArray()?.let { RealmList(*it) }
|
||||
?: RealmList()
|
||||
crossSigningKeysMapper.update(existingUSK, userSigningKey)
|
||||
} else {
|
||||
val keyEntity = realm.createObject(KeyInfoEntity::class.java).apply {
|
||||
this.publicKeyBase64 = userSigningKey.unpaddedBase64PublicKey
|
||||
this.usages = userSigningKey.usages?.toTypedArray()?.let { RealmList(*it) }
|
||||
?: RealmList()
|
||||
this.putSignatures(userSigningKey.signatures)
|
||||
}
|
||||
val keyEntity = crossSigningKeysMapper.map(userSigningKey)
|
||||
signingInfo.setUserSignedKey(keyEntity)
|
||||
}
|
||||
}
|
||||
|
||||
userEntity.crossSigningInfoEntity = signingInfo
|
||||
}
|
||||
}
|
||||
@ -369,14 +353,35 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
}
|
||||
|
||||
override fun getCrossSigningPrivateKeys(): PrivateKeysInfo? {
|
||||
return doRealmQueryAndCopy(realmConfiguration) { realm ->
|
||||
realm.where<CryptoMetadataEntity>().findFirst()
|
||||
}?.let {
|
||||
PrivateKeysInfo(
|
||||
master = it.xSignMasterPrivateKey,
|
||||
selfSigned = it.xSignSelfSignedPrivateKey,
|
||||
user = it.xSignUserPrivateKey
|
||||
)
|
||||
return doWithRealm(realmConfiguration) { realm ->
|
||||
realm.where<CryptoMetadataEntity>()
|
||||
.findFirst()
|
||||
?.let {
|
||||
PrivateKeysInfo(
|
||||
master = it.xSignMasterPrivateKey,
|
||||
selfSigned = it.xSignSelfSignedPrivateKey,
|
||||
user = it.xSignUserPrivateKey
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -400,16 +405,18 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
}
|
||||
|
||||
override fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? {
|
||||
return doRealmQueryAndCopy(realmConfiguration) { realm ->
|
||||
realm.where<CryptoMetadataEntity>().findFirst()
|
||||
}?.let {
|
||||
val key = it.keyBackupRecoveryKey
|
||||
val version = it.keyBackupRecoveryKeyVersion
|
||||
if (!key.isNullOrBlank() && !version.isNullOrBlank()) {
|
||||
SavedKeyBackupKeyInfo(recoveryKey = key, version = version)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return doWithRealm(realmConfiguration) { realm ->
|
||||
realm.where<CryptoMetadataEntity>()
|
||||
.findFirst()
|
||||
?.let {
|
||||
val key = it.keyBackupRecoveryKey
|
||||
val version = it.keyBackupRecoveryKeyVersion
|
||||
if (!key.isNullOrBlank() && !version.isNullOrBlank()) {
|
||||
SavedKeyBackupKeyInfo(recoveryKey = key, version = version)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -430,24 +437,30 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
}
|
||||
|
||||
override fun getUserDevices(userId: String): Map<String, CryptoDeviceInfo>? {
|
||||
return doRealmQueryAndCopy(realmConfiguration) {
|
||||
return doWithRealm(realmConfiguration) {
|
||||
it.where<UserEntity>()
|
||||
.equalTo(UserEntityFields.USER_ID, userId)
|
||||
.findFirst()
|
||||
?.devices
|
||||
?.map { deviceInfo ->
|
||||
CryptoMapper.mapToModel(deviceInfo)
|
||||
}
|
||||
?.associateBy { cryptoDevice ->
|
||||
cryptoDevice.deviceId
|
||||
}
|
||||
}
|
||||
?.devices
|
||||
?.map { CryptoMapper.mapToModel(it) }
|
||||
?.associateBy { it.deviceId }
|
||||
}
|
||||
|
||||
override fun getUserDeviceList(userId: String): List<CryptoDeviceInfo>? {
|
||||
return doRealmQueryAndCopy(realmConfiguration) {
|
||||
return doWithRealm(realmConfiguration) {
|
||||
it.where<UserEntity>()
|
||||
.equalTo(UserEntityFields.USER_ID, userId)
|
||||
.findFirst()
|
||||
?.devices
|
||||
?.map { deviceInfo ->
|
||||
CryptoMapper.mapToModel(deviceInfo)
|
||||
}
|
||||
}
|
||||
?.devices
|
||||
?.map { CryptoMapper.mapToModel(it) }
|
||||
}
|
||||
|
||||
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) {
|
||||
doRealmTransaction(realmConfiguration) {
|
||||
CryptoRoomEntity.getOrCreate(it, roomId).algorithm = algorithm
|
||||
@ -503,17 +562,16 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
}
|
||||
|
||||
override fun getRoomAlgorithm(roomId: String): String? {
|
||||
return doRealmQueryAndCopy(realmConfiguration) {
|
||||
CryptoRoomEntity.getById(it, roomId)
|
||||
return doWithRealm(realmConfiguration) {
|
||||
CryptoRoomEntity.getById(it, roomId)?.algorithm
|
||||
}
|
||||
?.algorithm
|
||||
}
|
||||
|
||||
override fun shouldEncryptForInvitedMembers(roomId: String): Boolean {
|
||||
return doRealmQueryAndCopy(realmConfiguration) {
|
||||
CryptoRoomEntity.getById(it, roomId)
|
||||
return doWithRealm(realmConfiguration) {
|
||||
CryptoRoomEntity.getById(it, roomId)?.shouldEncryptForInvitedMembers
|
||||
}
|
||||
?.shouldEncryptForInvitedMembers ?: false
|
||||
?: false
|
||||
}
|
||||
|
||||
override fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) {
|
||||
@ -577,24 +635,24 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
}
|
||||
|
||||
override fun getLastUsedSessionId(deviceKey: String): String? {
|
||||
return doRealmQueryAndCopy(realmConfiguration) {
|
||||
return doWithRealm(realmConfiguration) {
|
||||
it.where<OlmSessionEntity>()
|
||||
.equalTo(OlmSessionEntityFields.DEVICE_KEY, deviceKey)
|
||||
.sort(OlmSessionEntityFields.LAST_RECEIVED_MESSAGE_TS, Sort.DESCENDING)
|
||||
.findFirst()
|
||||
?.sessionId
|
||||
}
|
||||
?.sessionId
|
||||
}
|
||||
|
||||
override fun getDeviceSessionIds(deviceKey: String): MutableSet<String> {
|
||||
return doRealmQueryAndCopyList(realmConfiguration) {
|
||||
return doWithRealm(realmConfiguration) {
|
||||
it.where<OlmSessionEntity>()
|
||||
.equalTo(OlmSessionEntityFields.DEVICE_KEY, deviceKey)
|
||||
.findAll()
|
||||
.mapNotNull { sessionEntity ->
|
||||
sessionEntity.sessionId
|
||||
}
|
||||
}
|
||||
.mapNotNull {
|
||||
it.sessionId
|
||||
}
|
||||
.toMutableSet()
|
||||
}
|
||||
|
||||
@ -641,12 +699,12 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
|
||||
// If not in cache (or not found), try to read it from realm
|
||||
if (inboundGroupSessionToRelease[key] == null) {
|
||||
doRealmQueryAndCopy(realmConfiguration) {
|
||||
doWithRealm(realmConfiguration) {
|
||||
it.where<OlmInboundGroupSessionEntity>()
|
||||
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
|
||||
.findFirst()
|
||||
?.getInboundGroupSession()
|
||||
}
|
||||
?.getInboundGroupSession()
|
||||
?.let {
|
||||
inboundGroupSessionToRelease[key] = it
|
||||
}
|
||||
@ -660,13 +718,13 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
* so there is no need to use or update `inboundGroupSessionToRelease` for native memory management
|
||||
*/
|
||||
override fun getInboundGroupSessions(): MutableList<OlmInboundGroupSessionWrapper> {
|
||||
return doRealmQueryAndCopyList(realmConfiguration) {
|
||||
return doWithRealm(realmConfiguration) {
|
||||
it.where<OlmInboundGroupSessionEntity>()
|
||||
.findAll()
|
||||
.mapNotNull { inboundGroupSessionEntity ->
|
||||
inboundGroupSessionEntity.getInboundGroupSession()
|
||||
}
|
||||
}
|
||||
.mapNotNull {
|
||||
it.getInboundGroupSession()
|
||||
}
|
||||
.toMutableList()
|
||||
}
|
||||
|
||||
@ -755,13 +813,14 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
}
|
||||
|
||||
override fun inboundGroupSessionsToBackup(limit: Int): List<OlmInboundGroupSessionWrapper> {
|
||||
return doRealmQueryAndCopyList(realmConfiguration) {
|
||||
return doWithRealm(realmConfiguration) {
|
||||
it.where<OlmInboundGroupSessionEntity>()
|
||||
.equalTo(OlmInboundGroupSessionEntityFields.BACKED_UP, false)
|
||||
.limit(limit.toLong())
|
||||
.findAll()
|
||||
}.mapNotNull { inboundGroupSession ->
|
||||
inboundGroupSession.getInboundGroupSession()
|
||||
.mapNotNull { inboundGroupSession ->
|
||||
inboundGroupSession.getInboundGroupSession()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -785,10 +844,9 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
}
|
||||
|
||||
override fun getGlobalBlacklistUnverifiedDevices(): Boolean {
|
||||
return doRealmQueryAndCopy(realmConfiguration) {
|
||||
it.where<CryptoMetadataEntity>().findFirst()
|
||||
}?.globalBlacklistUnverifiedDevices
|
||||
?: false
|
||||
return doWithRealm(realmConfiguration) {
|
||||
it.where<CryptoMetadataEntity>().findFirst()?.globalBlacklistUnverifiedDevices
|
||||
} ?: false
|
||||
}
|
||||
|
||||
override fun setRoomsListBlacklistUnverifiedDevices(roomIds: List<String>) {
|
||||
@ -811,28 +869,28 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
}
|
||||
|
||||
override fun getRoomsListBlacklistUnverifiedDevices(): MutableList<String> {
|
||||
return doRealmQueryAndCopyList(realmConfiguration) {
|
||||
return doWithRealm(realmConfiguration) {
|
||||
it.where<CryptoRoomEntity>()
|
||||
.equalTo(CryptoRoomEntityFields.BLACKLIST_UNVERIFIED_DEVICES, true)
|
||||
.findAll()
|
||||
.mapNotNull { cryptoRoom ->
|
||||
cryptoRoom.roomId
|
||||
}
|
||||
}
|
||||
.mapNotNull {
|
||||
it.roomId
|
||||
}
|
||||
.toMutableList()
|
||||
}
|
||||
|
||||
override fun getDeviceTrackingStatuses(): MutableMap<String, Int> {
|
||||
return doRealmQueryAndCopyList(realmConfiguration) {
|
||||
return doWithRealm(realmConfiguration) {
|
||||
it.where<UserEntity>()
|
||||
.findAll()
|
||||
.associateBy { user ->
|
||||
user.userId!!
|
||||
}
|
||||
.mapValues { entry ->
|
||||
entry.value.deviceTrackingStatus
|
||||
}
|
||||
}
|
||||
.associateBy {
|
||||
it.userId!!
|
||||
}
|
||||
.mapValues {
|
||||
it.value.deviceTrackingStatus
|
||||
}
|
||||
.toMutableMap()
|
||||
}
|
||||
|
||||
@ -847,12 +905,12 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
}
|
||||
|
||||
override fun getDeviceTrackingStatus(userId: String, defaultValue: Int): Int {
|
||||
return doRealmQueryAndCopy(realmConfiguration) {
|
||||
return doWithRealm(realmConfiguration) {
|
||||
it.where<UserEntity>()
|
||||
.equalTo(UserEntityFields.USER_ID, userId)
|
||||
.findFirst()
|
||||
?.deviceTrackingStatus
|
||||
}
|
||||
?.deviceTrackingStatus
|
||||
?: defaultValue
|
||||
}
|
||||
|
||||
@ -1089,63 +1147,65 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
}
|
||||
|
||||
override fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest? {
|
||||
return doRealmQueryAndCopyList(realmConfiguration) { realm ->
|
||||
return doWithRealm(realmConfiguration) { realm ->
|
||||
realm.where<IncomingGossipingRequestEntity>()
|
||||
.equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
||||
.equalTo(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, deviceId)
|
||||
.equalTo(IncomingGossipingRequestEntityFields.OTHER_USER_ID, userId)
|
||||
.findAll()
|
||||
}.mapNotNull { entity ->
|
||||
entity.toIncomingGossipingRequest() as? IncomingRoomKeyRequest
|
||||
}.firstOrNull()
|
||||
.mapNotNull { entity ->
|
||||
entity.toIncomingGossipingRequest() as? IncomingRoomKeyRequest
|
||||
}
|
||||
.firstOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPendingIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> {
|
||||
return doRealmQueryAndCopyList(realmConfiguration) {
|
||||
return doWithRealm(realmConfiguration) {
|
||||
it.where<IncomingGossipingRequestEntity>()
|
||||
.equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
||||
.equalTo(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, GossipingRequestState.PENDING.name)
|
||||
.findAll()
|
||||
.map { entity ->
|
||||
IncomingRoomKeyRequest(
|
||||
userId = entity.otherUserId,
|
||||
deviceId = entity.otherDeviceId,
|
||||
requestId = entity.requestId,
|
||||
requestBody = entity.getRequestedKeyInfo(),
|
||||
localCreationTimestamp = entity.localCreationTimestamp
|
||||
)
|
||||
}
|
||||
}
|
||||
.map { entity ->
|
||||
IncomingRoomKeyRequest(
|
||||
userId = entity.otherUserId,
|
||||
deviceId = entity.otherDeviceId,
|
||||
requestId = entity.requestId,
|
||||
requestBody = entity.getRequestedKeyInfo(),
|
||||
localCreationTimestamp = entity.localCreationTimestamp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPendingIncomingGossipingRequests(): List<IncomingShareRequestCommon> {
|
||||
return doRealmQueryAndCopyList(realmConfiguration) {
|
||||
return doWithRealm(realmConfiguration) {
|
||||
it.where<IncomingGossipingRequestEntity>()
|
||||
.equalTo(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, GossipingRequestState.PENDING.name)
|
||||
.findAll()
|
||||
}
|
||||
.mapNotNull { entity ->
|
||||
when (entity.type) {
|
||||
GossipRequestType.KEY -> {
|
||||
IncomingRoomKeyRequest(
|
||||
userId = entity.otherUserId,
|
||||
deviceId = entity.otherDeviceId,
|
||||
requestId = entity.requestId,
|
||||
requestBody = entity.getRequestedKeyInfo(),
|
||||
localCreationTimestamp = entity.localCreationTimestamp
|
||||
)
|
||||
}
|
||||
GossipRequestType.SECRET -> {
|
||||
IncomingSecretShareRequest(
|
||||
userId = entity.otherUserId,
|
||||
deviceId = entity.otherDeviceId,
|
||||
requestId = entity.requestId,
|
||||
secretName = entity.getRequestedSecretName(),
|
||||
localCreationTimestamp = entity.localCreationTimestamp
|
||||
)
|
||||
.mapNotNull { entity ->
|
||||
when (entity.type) {
|
||||
GossipRequestType.KEY -> {
|
||||
IncomingRoomKeyRequest(
|
||||
userId = entity.otherUserId,
|
||||
deviceId = entity.otherDeviceId,
|
||||
requestId = entity.requestId,
|
||||
requestBody = entity.getRequestedKeyInfo(),
|
||||
localCreationTimestamp = entity.localCreationTimestamp
|
||||
)
|
||||
}
|
||||
GossipRequestType.SECRET -> {
|
||||
IncomingSecretShareRequest(
|
||||
userId = entity.otherUserId,
|
||||
deviceId = entity.otherDeviceId,
|
||||
requestId = entity.requestId,
|
||||
secretName = entity.getRequestedSecretName(),
|
||||
localCreationTimestamp = entity.localCreationTimestamp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?) {
|
||||
@ -1183,9 +1243,9 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
* Cross Signing
|
||||
* ========================================================================================== */
|
||||
override fun getMyCrossSigningInfo(): MXCrossSigningInfo? {
|
||||
return doRealmQueryAndCopy(realmConfiguration) {
|
||||
it.where<CryptoMetadataEntity>().findFirst()
|
||||
}?.userId?.let {
|
||||
return doWithRealm(realmConfiguration) {
|
||||
it.where<CryptoMetadataEntity>().findFirst()?.userId
|
||||
}?.let {
|
||||
getCrossSigningInfo(it)
|
||||
}
|
||||
}
|
||||
@ -1304,33 +1364,24 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
}
|
||||
|
||||
override fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? {
|
||||
return doRealmQueryAndCopy(realmConfiguration) { realm ->
|
||||
realm.where(CrossSigningInfoEntity::class.java)
|
||||
return doWithRealm(realmConfiguration) { realm ->
|
||||
val crossSigningInfo = realm.where(CrossSigningInfoEntity::class.java)
|
||||
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
|
||||
.findFirst()
|
||||
}?.let { xsignInfo ->
|
||||
mapCrossSigningInfoEntity(xsignInfo)
|
||||
if (crossSigningInfo == null) {
|
||||
null
|
||||
} else {
|
||||
mapCrossSigningInfoEntity(crossSigningInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapCrossSigningInfoEntity(xsignInfo: CrossSigningInfoEntity): MXCrossSigningInfo {
|
||||
val userId = xsignInfo.userId ?: ""
|
||||
return MXCrossSigningInfo(
|
||||
userId = xsignInfo.userId ?: "",
|
||||
userId = userId,
|
||||
crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull {
|
||||
val pubKey = it.publicKeyBase64 ?: return@mapNotNull null
|
||||
CryptoCrossSigningKey(
|
||||
userId = xsignInfo.userId ?: "",
|
||||
keys = mapOf("ed25519:$pubKey" to pubKey),
|
||||
usages = it.usages.map { it },
|
||||
signatures = it.getSignatures(),
|
||||
trustLevel = it.trustLevelEntity?.let {
|
||||
DeviceTrustLevel(
|
||||
crossSigningVerified = it.crossSignedVerified ?: false,
|
||||
locallyVerified = it.locallyVerified ?: false
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
crossSigningKeysMapper.map(userId, it)
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -1341,26 +1392,7 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
realm.where<CrossSigningInfoEntity>()
|
||||
.equalTo(UserEntityFields.USER_ID, userId)
|
||||
},
|
||||
{ entity ->
|
||||
MXCrossSigningInfo(
|
||||
userId = userId,
|
||||
crossSigningKeys = entity.crossSigningKeys.mapNotNull {
|
||||
val pubKey = it.publicKeyBase64 ?: return@mapNotNull null
|
||||
CryptoCrossSigningKey(
|
||||
userId = userId,
|
||||
keys = mapOf("ed25519:$pubKey" to pubKey),
|
||||
usages = it.usages.map { it },
|
||||
signatures = it.getSignatures(),
|
||||
trustLevel = it.trustLevelEntity?.let {
|
||||
DeviceTrustLevel(
|
||||
crossSigningVerified = it.crossSignedVerified ?: false,
|
||||
locallyVerified = it.locallyVerified ?: false
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
{ mapCrossSigningInfoEntity(it) }
|
||||
)
|
||||
return Transformations.map(liveData) {
|
||||
it.firstOrNull().toOptional()
|
||||
@ -1402,17 +1434,8 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
// existing.crossSigningKeys.forEach { it.deleteFromRealm() }
|
||||
val xkeys = RealmList<KeyInfoEntity>()
|
||||
info.crossSigningKeys.forEach { cryptoCrossSigningKey ->
|
||||
xkeys.add(
|
||||
realm.createObject(KeyInfoEntity::class.java).also { keyInfoEntity ->
|
||||
keyInfoEntity.publicKeyBase64 = cryptoCrossSigningKey.unpaddedBase64PublicKey
|
||||
keyInfoEntity.usages = cryptoCrossSigningKey.usages?.let { RealmList(*it.toTypedArray()) }
|
||||
?: RealmList()
|
||||
keyInfoEntity.putSignatures(cryptoCrossSigningKey.signatures)
|
||||
// TODO how to handle better, check if same keys?
|
||||
// reset trust
|
||||
keyInfoEntity.trustLevelEntity = null
|
||||
}
|
||||
)
|
||||
val keyEntity = crossSigningKeysMapper.map(cryptoCrossSigningKey)
|
||||
xkeys.add(keyEntity)
|
||||
}
|
||||
existing.crossSigningKeys = xkeys
|
||||
}
|
||||
|
@ -18,14 +18,17 @@ package im.vector.matrix.android.internal.crypto.store.db
|
||||
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import im.vector.matrix.android.api.extensions.tryThis
|
||||
import im.vector.matrix.android.api.util.JsonDict
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.store.db.mapper.CrossSigningKeysMapper
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMetadataEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.GossipingEventEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields
|
||||
@ -33,11 +36,14 @@ import im.vector.matrix.android.internal.di.SerializeNulls
|
||||
import io.realm.DynamicRealm
|
||||
import io.realm.RealmMigration
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal object RealmCryptoStoreMigration : RealmMigration {
|
||||
internal class RealmCryptoStoreMigration @Inject constructor(private val crossSigningKeysMapper: CrossSigningKeysMapper) : RealmMigration {
|
||||
|
||||
// Version 1L added Cross Signing info persistence
|
||||
const val CRYPTO_STORE_SCHEMA_VERSION = 3L
|
||||
companion object {
|
||||
const val CRYPTO_STORE_SCHEMA_VERSION = 5L
|
||||
}
|
||||
|
||||
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||
Timber.v("Migrating Realm Crypto from $oldVersion to $newVersion")
|
||||
@ -45,6 +51,8 @@ internal object RealmCryptoStoreMigration : RealmMigration {
|
||||
if (oldVersion <= 0) migrateTo1(realm)
|
||||
if (oldVersion <= 1) migrateTo2(realm)
|
||||
if (oldVersion <= 2) migrateTo3(realm)
|
||||
if (oldVersion <= 3) migrateTo4(realm)
|
||||
if (oldVersion <= 4) migrateTo5(realm)
|
||||
}
|
||||
|
||||
private fun migrateTo1(realm: DynamicRealm) {
|
||||
@ -193,4 +201,38 @@ internal object RealmCryptoStoreMigration : RealmMigration {
|
||||
?.addField(CryptoMetadataEntityFields.KEY_BACKUP_RECOVERY_KEY, String::class.java)
|
||||
?.addField(CryptoMetadataEntityFields.KEY_BACKUP_RECOVERY_KEY_VERSION, String::class.java)
|
||||
}
|
||||
|
||||
private fun migrateTo4(realm: DynamicRealm) {
|
||||
Timber.d("Updating KeyInfoEntity table")
|
||||
val keyInfoEntities = realm.where("KeyInfoEntity").findAll()
|
||||
try {
|
||||
keyInfoEntities.forEach {
|
||||
val stringSignatures = it.getString(KeyInfoEntityFields.SIGNATURES)
|
||||
val objectSignatures: Map<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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.crypto.store.db.model.GossipingEventEnt
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntity
|
||||
@ -48,6 +49,7 @@ import io.realm.annotations.RealmModule
|
||||
TrustLevelEntity::class,
|
||||
GossipingEventEntity::class,
|
||||
IncomingGossipingRequestEntity::class,
|
||||
OutgoingGossipingRequestEntity::class
|
||||
OutgoingGossipingRequestEntity::class,
|
||||
MyDeviceLastSeenInfoEntity::class
|
||||
])
|
||||
internal class RealmCryptoStoreModule
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -104,7 +104,8 @@ object CryptoMapper {
|
||||
Timber.e(failure)
|
||||
null
|
||||
}
|
||||
}
|
||||
},
|
||||
firstTimeSeenLocalTs = deviceInfoEntity.firstTimeSeenLocalTs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +34,12 @@ internal open class DeviceInfoEntity(@PrimaryKey var primaryKey: String = "",
|
||||
var keysMapJson: String? = null,
|
||||
var signatureMapJson: String? = null,
|
||||
var unsignedMapJson: String? = null,
|
||||
var trustLevelEntity: TrustLevelEntity? = null
|
||||
var trustLevelEntity: TrustLevelEntity? = null,
|
||||
/**
|
||||
* We use that to make distinction between old devices (there before mine)
|
||||
* and new ones. Used for example to detect new unverified login
|
||||
*/
|
||||
var firstTimeSeenLocalTs: Long? = null
|
||||
) : RealmObject() {
|
||||
|
||||
// // Deserialize data
|
||||
|
@ -16,8 +16,6 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.store.db.model
|
||||
|
||||
import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm
|
||||
import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm
|
||||
import io.realm.RealmList
|
||||
import io.realm.RealmObject
|
||||
|
||||
@ -31,15 +29,4 @@ internal open class KeyInfoEntity(
|
||||
*/
|
||||
var signatures: String? = null,
|
||||
var trustLevelEntity: TrustLevelEntity? = null
|
||||
) : RealmObject() {
|
||||
|
||||
// Deserialize data
|
||||
fun getSignatures(): Map<String, Map<String, String>>? {
|
||||
return deserializeFromRealm(signatures)
|
||||
}
|
||||
|
||||
// Serialize data
|
||||
fun putSignatures(deviceInfo: Map<String, Map<String, String>>?) {
|
||||
signatures = serializeForRealm(deviceInfo)
|
||||
}
|
||||
}
|
||||
) : RealmObject()
|
||||
|
@ -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
|
||||
}
|
@ -17,10 +17,9 @@
|
||||
package im.vector.matrix.android.internal.crypto.tasks
|
||||
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
|
||||
import im.vector.matrix.android.api.failure.toRegistrationFlowResponse
|
||||
import im.vector.matrix.android.internal.crypto.api.CryptoApi
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceParams
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
@ -43,25 +42,9 @@ internal class DefaultDeleteDeviceTask @Inject constructor(
|
||||
apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams())
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
if (throwable is Failure.OtherServerError && throwable.httpCode == 401) {
|
||||
// Parse to get a RegistrationFlowResponse
|
||||
val registrationFlowResponse = try {
|
||||
MoshiProvider.providesMoshi()
|
||||
.adapter(RegistrationFlowResponse::class.java)
|
||||
.fromJson(throwable.errorBody)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
// check if the server response can be casted
|
||||
if (registrationFlowResponse != null) {
|
||||
throw Failure.RegistrationFlowError(registrationFlowResponse)
|
||||
} else {
|
||||
throw throwable
|
||||
}
|
||||
} else {
|
||||
// Other error
|
||||
throw throwable
|
||||
}
|
||||
throw throwable.toRegistrationFlowResponse()
|
||||
?.let { Failure.RegistrationFlowError(it) }
|
||||
?: throwable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -52,6 +52,8 @@ internal class DefaultSendToDeviceTask @Inject constructor(
|
||||
params.transactionId ?: Random.nextInt(Integer.MAX_VALUE).toString(),
|
||||
sendToDeviceBody
|
||||
)
|
||||
isRetryable = true
|
||||
maxRetryCount = 3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,14 +17,13 @@
|
||||
package im.vector.matrix.android.internal.crypto.tasks
|
||||
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
|
||||
import im.vector.matrix.android.api.failure.toRegistrationFlowResponse
|
||||
import im.vector.matrix.android.internal.crypto.api.CryptoApi
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeysQueryResponse
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UploadSigningKeysBody
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||
import im.vector.matrix.android.internal.crypto.model.toRest
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
@ -65,37 +64,25 @@ internal class DefaultUploadSigningKeysTask @Inject constructor(
|
||||
}
|
||||
return
|
||||
} catch (throwable: Throwable) {
|
||||
if (throwable is Failure.OtherServerError
|
||||
&& throwable.httpCode == 401
|
||||
val registrationFlowResponse = throwable.toRegistrationFlowResponse()
|
||||
if (registrationFlowResponse != null
|
||||
&& params.userPasswordAuth != null
|
||||
/* Avoid infinite loop */
|
||||
&& params.userPasswordAuth.session.isNullOrEmpty()
|
||||
) {
|
||||
try {
|
||||
MoshiProvider.providesMoshi()
|
||||
.adapter(RegistrationFlowResponse::class.java)
|
||||
.fromJson(throwable.errorBody)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}?.let {
|
||||
// Retry with authentication
|
||||
try {
|
||||
val req = executeRequest<KeysQueryResponse>(eventBus) {
|
||||
apiCall = cryptoApi.uploadSigningKeys(
|
||||
uploadQuery.copy(auth = params.userPasswordAuth.copy(session = it.session))
|
||||
)
|
||||
}
|
||||
if (req.failures?.isNotEmpty() == true) {
|
||||
throw UploadSigningKeys(req.failures)
|
||||
}
|
||||
return
|
||||
} catch (failure: Throwable) {
|
||||
throw failure
|
||||
}
|
||||
// Retry with authentication
|
||||
val req = executeRequest<KeysQueryResponse>(eventBus) {
|
||||
apiCall = cryptoApi.uploadSigningKeys(
|
||||
uploadQuery.copy(auth = params.userPasswordAuth.copy(session = registrationFlowResponse.session))
|
||||
)
|
||||
}
|
||||
if (req.failures?.isNotEmpty() == true) {
|
||||
throw UploadSigningKeys(req.failures)
|
||||
}
|
||||
} else {
|
||||
// Other error
|
||||
throw throwable
|
||||
}
|
||||
// Other error
|
||||
throw throwable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -138,7 +138,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
|
||||
|
||||
override fun onVerificationAccept(accept: ValidVerificationInfoAccept) {
|
||||
Timber.v("## SAS O: onVerificationAccept id:$transactionId")
|
||||
if (state != VerificationTxState.Started) {
|
||||
if (state != VerificationTxState.Started && state != VerificationTxState.SendingStart) {
|
||||
Timber.e("## SAS O: received accept request from invalid state $state")
|
||||
cancel(CancelCode.UnexpectedMessage)
|
||||
return
|
||||
@ -148,7 +148,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
|
||||
|| !KNOWN_HASHES.contains(accept.hash)
|
||||
|| !KNOWN_MACS.contains(accept.messageAuthenticationCode)
|
||||
|| accept.shortAuthenticationStrings.intersect(KNOWN_SHORT_CODES).isEmpty()) {
|
||||
Timber.e("## SAS O: received accept request from invalid state")
|
||||
Timber.e("## SAS O: received invalid accept")
|
||||
cancel(CancelCode.UnknownMethod)
|
||||
return
|
||||
}
|
||||
|
@ -117,6 +117,7 @@ internal class VerificationTransportToDevice(
|
||||
onDone: (() -> Unit)?) {
|
||||
Timber.d("## SAS sending msg type $type")
|
||||
Timber.v("## SAS sending msg info $verificationInfo")
|
||||
val stateBeforeCall = tx?.state
|
||||
val tx = tx ?: return
|
||||
val contentMap = MXUsersDevicesMap<Any>()
|
||||
val toSendToDeviceObject = verificationInfo.toSendToDeviceObject()
|
||||
@ -132,7 +133,11 @@ internal class VerificationTransportToDevice(
|
||||
if (onDone != null) {
|
||||
onDone()
|
||||
} else {
|
||||
tx.state = nextState
|
||||
// we may have received next state (e.g received accept in sending_start)
|
||||
// We only put next state if the state was what is was before we started
|
||||
if (tx.state == stateBeforeCall) {
|
||||
tx.state = nextState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@
|
||||
package im.vector.matrix.android.internal.network
|
||||
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.api.failure.shouldBeRetried
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.delay
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
@ -46,7 +47,7 @@ internal class Request<DATA>(private val eventBus: EventBus?) {
|
||||
throw response.toFailure(eventBus)
|
||||
}
|
||||
} catch (exception: Throwable) {
|
||||
if (isRetryable && currentRetryCount++ < maxRetryCount && exception is IOException) {
|
||||
if (isRetryable && currentRetryCount++ < maxRetryCount && exception.shouldBeRetried()) {
|
||||
delay(currentDelay)
|
||||
currentDelay = (currentDelay * 2L).coerceAtMost(maxDelay)
|
||||
return execute()
|
||||
|
@ -16,9 +16,7 @@
|
||||
|
||||
package im.vector.matrix.android.internal.session.account
|
||||
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import im.vector.matrix.android.api.failure.toRegistrationFlowResponse
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
@ -45,31 +43,20 @@ internal class DefaultChangePasswordTask @Inject constructor(
|
||||
apiCall = accountAPI.changePassword(changePasswordParams)
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
if (throwable is Failure.OtherServerError
|
||||
&& throwable.httpCode == 401
|
||||
val registrationFlowResponse = throwable.toRegistrationFlowResponse()
|
||||
|
||||
if (registrationFlowResponse != null
|
||||
/* Avoid infinite loop */
|
||||
&& changePasswordParams.auth?.session == null) {
|
||||
try {
|
||||
MoshiProvider.providesMoshi()
|
||||
.adapter(RegistrationFlowResponse::class.java)
|
||||
.fromJson(throwable.errorBody)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}?.let {
|
||||
// Retry with authentication
|
||||
try {
|
||||
executeRequest<Unit>(eventBus) {
|
||||
apiCall = accountAPI.changePassword(
|
||||
changePasswordParams.copy(auth = changePasswordParams.auth?.copy(session = it.session))
|
||||
)
|
||||
}
|
||||
return
|
||||
} catch (failure: Throwable) {
|
||||
throw failure
|
||||
}
|
||||
// Retry with authentication
|
||||
executeRequest<Unit>(eventBus) {
|
||||
apiCall = accountAPI.changePassword(
|
||||
changePasswordParams.copy(auth = changePasswordParams.auth?.copy(session = registrationFlowResponse.session))
|
||||
)
|
||||
}
|
||||
} else {
|
||||
throw throwable
|
||||
}
|
||||
throw throwable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -153,7 +153,7 @@ internal class RoomSummaryUpdater @Inject constructor(
|
||||
|
||||
if (updateMembers) {
|
||||
val otherRoomMembers = RoomMemberHelper(realm, roomId)
|
||||
.queryRoomMembersEvent()
|
||||
.queryActiveRoomMembersEvent()
|
||||
.notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId)
|
||||
.findAll()
|
||||
.asSequence()
|
||||
@ -162,15 +162,7 @@ internal class RoomSummaryUpdater @Inject constructor(
|
||||
roomSummaryEntity.otherMemberIds.clear()
|
||||
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers)
|
||||
if (roomSummaryEntity.isEncrypted) {
|
||||
// The set of “all users” depends on the type of room:
|
||||
// For regular / topic rooms, all users including yourself, are considered when decorating a room
|
||||
// For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room
|
||||
val listToCheck = if (roomSummaryEntity.isDirect) {
|
||||
roomSummaryEntity.otherMemberIds.toList()
|
||||
} else {
|
||||
roomSummaryEntity.otherMemberIds.toList() + userId
|
||||
}
|
||||
eventBus.post(SessionToCryptoRoomMembersUpdate(roomId, listToCheck))
|
||||
eventBus.post(SessionToCryptoRoomMembersUpdate(roomId, roomSummaryEntity.isDirect, roomSummaryEntity.otherMemberIds.toList() + userId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -53,8 +53,6 @@ internal class DefaultSyncTask @Inject constructor(
|
||||
|
||||
private suspend fun doSync(params: SyncTask.Params) {
|
||||
Timber.v("Sync task started on Thread: ${Thread.currentThread().name}")
|
||||
// Maybe refresh the home server capabilities data we know
|
||||
getHomeServerCapabilitiesTask.execute(Unit)
|
||||
|
||||
val requestParams = HashMap<String, String>()
|
||||
var timeout = 0L
|
||||
@ -73,6 +71,9 @@ internal class DefaultSyncTask @Inject constructor(
|
||||
initialSyncProgressService.endAll()
|
||||
initialSyncProgressService.startTask(R.string.initial_sync_start_importing_account, 100)
|
||||
}
|
||||
// Maybe refresh the home server capabilities data we know
|
||||
getHomeServerCapabilitiesTask.execute(Unit)
|
||||
|
||||
val syncResponse = executeRequest<SyncResponse>(eventBus) {
|
||||
apiCall = syncAPI.sync(requestParams)
|
||||
}
|
||||
|
@ -4,72 +4,72 @@
|
||||
<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">%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_join">%1$s csatlakozott</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_kick">%1$s kidobta %2$s -t</string>
|
||||
<string name="notice_room_unban">%1$s feloldotta tiltását %2$s -nak/nek</string>
|
||||
<string name="notice_room_ban">%1$s kitiltotta %2$s -t</string>
|
||||
<string name="notice_room_withdraw">%1$s visszavonta %2$s\'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_display_name_set">%1$s megváltoztatták a megjelenő nevüket 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_removed">%1$s eltávolították a megjelenő nevüket (%2$s)</string>
|
||||
<string name="notice_room_kick">%1$s kidobta: %2$s</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</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áltoztatta a profilképét</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áltoztatta a megjelenítendő nevét erről: %2$s, erre: %3$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_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_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_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 szoba tag, onnantól, hogy meg lettek hívva.</string>
|
||||
<string name="notice_room_visibility_joined">az összes szoba tag, onnantól, hogy csatlakoztak.</string>
|
||||
<string name="notice_room_visibility_shared">az összes szoba tag.</string>
|
||||
<string name="notice_room_visibility_invited">az összes szobatag, onnantól, hogy meg lettek hívva.</string>
|
||||
<string name="notice_room_visibility_joined">az összes szobatag, onnantól, hogy csatlakoztak.</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_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_voip_started">Hanghívás konferencia elindult</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_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_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_registered_invite">%1$s elfogadta a meghívót a %2$s -hoz</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 számára, hogy csatlakozzon a szobá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_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="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="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="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="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="reply_to_an_image">kép elküldve.</string>
|
||||
<string name="reply_to_a_video">videó elküldve.</string>
|
||||
<string name="reply_to_an_audio_file">hangfájl elküldve.</string>
|
||||
<string name="reply_to_a_file">fájl elküldve.</string>
|
||||
<string name="reply_to_an_image">képet küldött.</string>
|
||||
<string name="reply_to_a_video">videót küldött.</string>
|
||||
<string name="reply_to_an_audio_file">hangfájlt küldött.</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_two_members">%1$s és %2$s</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="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_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>
|
||||
|
@ -170,4 +170,39 @@
|
||||
<string name="notice_room_third_party_revoked_invite">%1$s 撤回了对 %2$s 邀请</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>
|
||||
|
@ -18,7 +18,7 @@ package im.vector.matrix.android.test.shared
|
||||
|
||||
import net.lachlanmckee.timberjunit.TimberTestRule
|
||||
|
||||
fun createTimberTestRule(): TimberTestRule {
|
||||
internal fun createTimberTestRule(): TimberTestRule {
|
||||
return TimberTestRule.builder()
|
||||
.showThread(false)
|
||||
.showTimestamp(false)
|
||||
|
6
tools/templates/RiotXFeature/globals.xml.ftl
Normal file
6
tools/templates/RiotXFeature/globals.xml.ftl
Normal 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>
|
37
tools/templates/RiotXFeature/recipe.xml.ftl
Normal file
37
tools/templates/RiotXFeature/recipe.xml.ftl
Normal 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>
|
@ -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>
|
@ -0,0 +1,5 @@
|
||||
package ${escapeKotlinIdentifiers(packageName)}
|
||||
|
||||
import im.vector.riotx.core.platform.VectorViewModelAction
|
||||
|
||||
sealed class ${actionClass}: VectorViewModelAction
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package ${escapeKotlinIdentifiers(packageName)}
|
||||
|
||||
import im.vector.riotx.core.platform.VectorViewEvents
|
||||
|
||||
sealed class ${viewEventsClass} : VectorViewEvents
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package ${escapeKotlinIdentifiers(packageName)}
|
||||
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
|
||||
data class ${viewStateClass}() : MvRxState
|
121
tools/templates/RiotXFeature/template.xml
Normal file
121
tools/templates/RiotXFeature/template.xml
Normal 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
24
tools/templates/configure.sh
Executable 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."
|
||||
}
|
@ -235,6 +235,15 @@ android {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
androidTest {
|
||||
java.srcDirs += "src/sharedTest/java"
|
||||
}
|
||||
test {
|
||||
java.srcDirs += "src/sharedTest/java"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@ -250,6 +259,7 @@ dependencies {
|
||||
def daggerVersion = '2.25.4'
|
||||
def autofill_version = "1.0.0"
|
||||
def work_version = '2.3.3'
|
||||
def arch_version = '2.1.0'
|
||||
|
||||
implementation project(":matrix-sdk-android")
|
||||
implementation project(":matrix-sdk-android-rx")
|
||||
@ -378,10 +388,18 @@ dependencies {
|
||||
// TESTS
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.amshove.kluent:kluent-android:1.44'
|
||||
// Plant Timber tree for test
|
||||
testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
|
||||
|
||||
androidTestImplementation 'androidx.test:core:1.2.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
androidTestImplementation 'androidx.test:rules:1.2.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
androidTestImplementation 'org.amshove.kluent:kluent-android:1.44'
|
||||
androidTestImplementation "androidx.arch.core:core-testing:$arch_version"
|
||||
// Plant Timber tree for test
|
||||
androidTestImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
|
||||
}
|
||||
|
||||
if (getGradle().getStartParameter().getTaskRequests().toString().contains("Gplay")) {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -30,6 +30,10 @@ import io.reactivex.Single
|
||||
abstract class VectorViewModel<S : MvRxState, VA : VectorViewModelAction, VE : VectorViewEvents>(initialState: S)
|
||||
: BaseMvRxViewModel<S>(initialState, false) {
|
||||
|
||||
interface Factory<S: MvRxState> {
|
||||
fun create(state: S): BaseMvRxViewModel<S>
|
||||
}
|
||||
|
||||
// Used to post transient events to the View
|
||||
protected val _viewEvents = PublishDataSource<VE>()
|
||||
val viewEvents: DataSource<VE> = _viewEvents
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -34,8 +34,10 @@ import im.vector.riotx.core.utils.deleteAllFiles
|
||||
import im.vector.riotx.features.home.HomeActivity
|
||||
import im.vector.riotx.features.login.LoginActivity
|
||||
import im.vector.riotx.features.notifications.NotificationDrawerManager
|
||||
import im.vector.riotx.features.settings.VectorPreferences
|
||||
import im.vector.riotx.features.signout.hard.SignedOutActivity
|
||||
import im.vector.riotx.features.signout.soft.SoftLogoutActivity
|
||||
import im.vector.riotx.features.ui.UiStateRepository
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
@ -78,6 +80,8 @@ class MainActivity : VectorBaseActivity() {
|
||||
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
|
||||
@Inject lateinit var sessionHolder: ActiveSessionHolder
|
||||
@Inject lateinit var errorFormatter: ErrorFormatter
|
||||
@Inject lateinit var vectorPreferences: VectorPreferences
|
||||
@Inject lateinit var uiStateRepository: UiStateRepository
|
||||
|
||||
override fun injectWith(injector: ScreenComponent) {
|
||||
injector.inject(this)
|
||||
@ -127,7 +131,7 @@ class MainActivity : VectorBaseActivity() {
|
||||
// Just do the local cleanup
|
||||
Timber.w("Account deactivated, start app")
|
||||
sessionHolder.clearActiveSession()
|
||||
doLocalCleanup()
|
||||
doLocalCleanup(clearPreferences = true)
|
||||
startNextActivityAndFinish()
|
||||
}
|
||||
args.clearCredentials -> session.signOut(
|
||||
@ -136,7 +140,7 @@ class MainActivity : VectorBaseActivity() {
|
||||
override fun onSuccess(data: Unit) {
|
||||
Timber.w("SIGN_OUT: success, start app")
|
||||
sessionHolder.clearActiveSession()
|
||||
doLocalCleanup()
|
||||
doLocalCleanup(clearPreferences = true)
|
||||
startNextActivityAndFinish()
|
||||
}
|
||||
|
||||
@ -147,7 +151,7 @@ class MainActivity : VectorBaseActivity() {
|
||||
args.clearCache -> session.clearCache(
|
||||
object : MatrixCallback<Unit> {
|
||||
override fun onSuccess(data: Unit) {
|
||||
doLocalCleanup()
|
||||
doLocalCleanup(clearPreferences = false)
|
||||
session.startSyncing(applicationContext)
|
||||
startNextActivityAndFinish()
|
||||
}
|
||||
@ -164,10 +168,15 @@ class MainActivity : VectorBaseActivity() {
|
||||
Timber.w("Ignoring invalid token global error")
|
||||
}
|
||||
|
||||
private fun doLocalCleanup() {
|
||||
private fun doLocalCleanup(clearPreferences: Boolean) {
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
// On UI Thread
|
||||
Glide.get(this@MainActivity).clearMemory()
|
||||
|
||||
if (clearPreferences) {
|
||||
vectorPreferences.clearPreferences()
|
||||
uiStateRepository.reset()
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
// On BG thread
|
||||
Glide.get(this@MainActivity).clearDiskCache()
|
||||
|
@ -27,14 +27,13 @@ import im.vector.matrix.android.api.session.crypto.verification.SasVerificationT
|
||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationService
|
||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
|
||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
||||
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
||||
import im.vector.matrix.android.internal.crypto.IncomingRequestCancellation
|
||||
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
||||
import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.features.popup.DefaultVectorAlert
|
||||
import im.vector.riotx.features.popup.PopupAlertManager
|
||||
@ -75,7 +74,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context, privat
|
||||
session = null
|
||||
}
|
||||
|
||||
override fun onSecretShareRequest(request: IncomingSecretShareRequest) : Boolean {
|
||||
override fun onSecretShareRequest(request: IncomingSecretShareRequest): Boolean {
|
||||
// By default riotX will not prompt if the SDK has decided that the request should not be fulfilled
|
||||
Timber.v("## onSecretShareRequest() : Ignoring $request")
|
||||
request.ignore?.run()
|
||||
@ -124,19 +123,11 @@ class KeyRequestHandler @Inject constructor(private val context: Context, privat
|
||||
deviceInfo.trustLevel = DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false)
|
||||
|
||||
// can we get more info on this device?
|
||||
session?.cryptoService()?.getDevicesList(object : MatrixCallback<DevicesListResponse> {
|
||||
override fun onSuccess(data: DevicesListResponse) {
|
||||
data.devices?.find { it.deviceId == deviceId }?.let {
|
||||
postAlert(context, userId, deviceId, true, deviceInfo, it)
|
||||
} ?: run {
|
||||
postAlert(context, userId, deviceId, true, deviceInfo)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
postAlert(context, userId, deviceId, true, deviceInfo)
|
||||
}
|
||||
})
|
||||
session?.cryptoService()?.getMyDevicesInfo()?.firstOrNull { it.deviceId == deviceId }?.let {
|
||||
postAlert(context, userId, deviceId, true, deviceInfo, it)
|
||||
} ?: kotlin.run {
|
||||
postAlert(context, userId, deviceId, true, deviceInfo)
|
||||
}
|
||||
} else {
|
||||
postAlert(context, userId, deviceId, false, deviceInfo)
|
||||
}
|
||||
|
@ -95,6 +95,14 @@ class BootstrapConfirmPassphraseFragment @Inject constructor(
|
||||
sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility)
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
|
||||
bootstrapSubmit.clicks()
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
submit()
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
}
|
||||
|
||||
private fun submit() = withState(sharedViewModel) { state ->
|
||||
@ -113,8 +121,6 @@ class BootstrapConfirmPassphraseFragment @Inject constructor(
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(sharedViewModel) { state ->
|
||||
super.invalidate()
|
||||
|
||||
if (state.step is BootstrapStep.ConfirmPassphrase) {
|
||||
val isPasswordVisible = state.step.isPasswordVisible
|
||||
ssss_passphrase_enter_edittext.showPassword(isPasswordVisible, updateCursor = false)
|
||||
|
@ -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.MatrixError
|
||||
import im.vector.matrix.android.api.failure.toRegistrationFlowResponse
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
|
||||
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
|
||||
@ -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.SsssKeySpec
|
||||
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
|
||||
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import im.vector.matrix.android.internal.util.awaitCallback
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.platform.ViewModelTask
|
||||
@ -206,6 +208,16 @@ class BootstrapCrossSigningTask @Inject constructor(
|
||||
}
|
||||
// Save it for gossiping
|
||||
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) {
|
||||
Timber.e("## BootstrapCrossSigningTask: Failed to init keybackup")
|
||||
@ -217,15 +229,10 @@ class BootstrapCrossSigningTask @Inject constructor(
|
||||
private fun handleInitializeXSigningError(failure: Throwable): BootstrapResult {
|
||||
if (failure is Failure.ServerError && failure.error.code == MatrixError.M_FORBIDDEN) {
|
||||
return BootstrapResult.InvalidPasswordError(failure.error)
|
||||
} else if (failure is Failure.OtherServerError && failure.httpCode == 401) {
|
||||
try {
|
||||
MoshiProvider.providesMoshi()
|
||||
.adapter(RegistrationFlowResponse::class.java)
|
||||
.fromJson(failure.errorBody)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}?.let { flowResponse ->
|
||||
if (flowResponse.flows?.any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true } != true) {
|
||||
} else {
|
||||
val registrationFlowResponse = failure.toRegistrationFlowResponse()
|
||||
if (registrationFlowResponse != null) {
|
||||
if (registrationFlowResponse.flows?.any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true } != true) {
|
||||
// can't do this from here
|
||||
return BootstrapResult.UnsupportedAuthFlow()
|
||||
}
|
||||
|
@ -90,6 +90,14 @@ class BootstrapEnterPassphraseFragment @Inject constructor(
|
||||
sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility)
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
|
||||
bootstrapSubmit.clicks()
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
submit()
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
}
|
||||
|
||||
private fun submit() = withState(sharedViewModel) { state ->
|
||||
@ -108,8 +116,6 @@ class BootstrapEnterPassphraseFragment @Inject constructor(
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(sharedViewModel) { state ->
|
||||
super.invalidate()
|
||||
|
||||
if (state.step is BootstrapStep.SetupPassphrase) {
|
||||
val isPasswordVisible = state.step.isPasswordVisible
|
||||
ssss_passphrase_enter_edittext.showPassword(isPasswordVisible, updateCursor = false)
|
||||
|
@ -66,7 +66,7 @@ class IncomingVerificationRequestHandler @Inject constructor(
|
||||
uid,
|
||||
context.getString(R.string.sas_incoming_request_notif_title),
|
||||
context.getString(R.string.sas_incoming_request_notif_content, name),
|
||||
R.drawable.shield,
|
||||
R.drawable.ic_shield_black,
|
||||
shouldBeDisplayedIn = { activity ->
|
||||
if (activity is VectorBaseActivity) {
|
||||
// TODO a bit too hugly :/
|
||||
|
@ -165,7 +165,9 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
} else {
|
||||
otherUserShield.setImageResource(R.drawable.ic_shield_warning)
|
||||
}
|
||||
otherUserNameText.text = getString(R.string.complete_security)
|
||||
otherUserNameText.text = getString(
|
||||
if (state.selfVerificationMode) R.string.crosssigning_verify_this_session else R.string.crosssigning_verify_session
|
||||
)
|
||||
otherUserShield.isVisible = true
|
||||
} else {
|
||||
avatarRenderer.render(matrixItem, otherUserAvatarImageView)
|
||||
@ -241,7 +243,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
}
|
||||
|
||||
when (state.qrTransactionState) {
|
||||
is VerificationTxState.QrScannedByOther -> {
|
||||
is VerificationTxState.QrScannedByOther -> {
|
||||
showFragment(VerificationQrScannedByOtherFragment::class, Bundle())
|
||||
return@withState
|
||||
}
|
||||
@ -252,19 +254,19 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
})
|
||||
return@withState
|
||||
}
|
||||
is VerificationTxState.Verified -> {
|
||||
is VerificationTxState.Verified -> {
|
||||
showFragment(VerificationConclusionFragment::class, Bundle().apply {
|
||||
putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(true, null, state.isMe))
|
||||
})
|
||||
return@withState
|
||||
}
|
||||
is VerificationTxState.Cancelled -> {
|
||||
is VerificationTxState.Cancelled -> {
|
||||
showFragment(VerificationConclusionFragment::class, Bundle().apply {
|
||||
putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(false, state.qrTransactionState.cancelCode.value, state.isMe))
|
||||
})
|
||||
return@withState
|
||||
}
|
||||
else -> Unit
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
// At this point there is no SAS transaction for this request
|
||||
|
@ -423,7 +423,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
// Just ignore for now
|
||||
Timber.v("## Failed to restore backup after SSSS recovery")
|
||||
Timber.e(failure, "## Failed to restore backup after SSSS recovery")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -167,20 +167,26 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||
val crossSigningEnabledOnAccount = myCrossSigningKeys != null
|
||||
|
||||
if (!crossSigningEnabledOnAccount && !sharedActionViewModel.isAccountCreation) {
|
||||
// We need to ask
|
||||
promptSecurityEvent(
|
||||
session,
|
||||
R.string.upgrade_security,
|
||||
R.string.security_prompt_text
|
||||
) {
|
||||
it.navigator.upgradeSessionSecurity(it)
|
||||
// Do not propose for SSO accounts, because we do not support yet confirming account credentials using SSO
|
||||
if (session.getHomeServerCapabilities().canChangePassword) {
|
||||
// We need to ask
|
||||
promptSecurityEvent(
|
||||
session,
|
||||
R.string.upgrade_security,
|
||||
R.string.security_prompt_text
|
||||
) {
|
||||
it.navigator.upgradeSessionSecurity(it)
|
||||
}
|
||||
} else {
|
||||
// Do not do it again
|
||||
sharedActionViewModel.hasDisplayedCompleteSecurityPrompt = true
|
||||
}
|
||||
} else if (myCrossSigningKeys?.isTrusted() == false) {
|
||||
// We need to ask
|
||||
promptSecurityEvent(
|
||||
session,
|
||||
R.string.complete_security,
|
||||
R.string.crosssigning_verify_this_session
|
||||
R.string.crosssigning_verify_this_session,
|
||||
R.string.confirm_your_identity
|
||||
) {
|
||||
it.navigator.waitSessionVerification(it)
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
@ -31,6 +30,7 @@ import com.google.android.material.bottomnavigation.BottomNavigationMenuView
|
||||
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
import im.vector.matrix.android.api.util.toMatrixItem
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.extensions.commitTransactionNow
|
||||
import im.vector.riotx.core.glide.GlideApp
|
||||
@ -43,6 +43,7 @@ import im.vector.riotx.features.home.room.list.RoomListParams
|
||||
import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView
|
||||
import im.vector.riotx.features.popup.PopupAlertManager
|
||||
import im.vector.riotx.features.popup.VerificationVectorAlert
|
||||
import im.vector.riotx.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS
|
||||
import im.vector.riotx.features.workers.signout.SignOutViewModel
|
||||
import kotlinx.android.synthetic.main.fragment_home_detail.*
|
||||
import timber.log.Timber
|
||||
@ -61,7 +62,7 @@ class HomeDetailFragment @Inject constructor(
|
||||
private val unreadCounterBadgeViews = arrayListOf<UnreadCounterBadgeView>()
|
||||
|
||||
private val viewModel: HomeDetailViewModel by fragmentViewModel()
|
||||
private val unknownDeviceDetectorSharedViewModel : UnknownDeviceDetectorSharedViewModel by activityViewModel()
|
||||
private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel()
|
||||
|
||||
private lateinit var sharedActionViewModel: HomeSharedActionViewModel
|
||||
|
||||
@ -87,39 +88,82 @@ class HomeDetailFragment @Inject constructor(
|
||||
switchDisplayMode(displayMode)
|
||||
}
|
||||
|
||||
unknownDeviceDetectorSharedViewModel.subscribe {
|
||||
it.unknownSessions.invoke()?.let { unknownDevices ->
|
||||
Timber.v("## Detector - ${unknownDevices.size} Unknown sessions")
|
||||
unknownDevices.forEachIndexed { index, deviceInfo ->
|
||||
Timber.v("## Detector - #$index deviceId:${deviceInfo.second.deviceId} lastSeenTs:${deviceInfo.second.lastSeenTs}")
|
||||
}
|
||||
val uid = "Newest_Device"
|
||||
alertManager.cancelAlert(uid)
|
||||
if (it.canCrossSign && unknownDevices.isNotEmpty()) {
|
||||
val newest = unknownDevices.first().second
|
||||
val user = unknownDevices.first().first
|
||||
alertManager.postVectorAlert(
|
||||
VerificationVectorAlert(
|
||||
uid = uid,
|
||||
title = getString(R.string.new_session),
|
||||
description = getString(R.string.new_session_review_with_info, newest.displayName ?: "", newest.deviceId ?: ""),
|
||||
iconId = R.drawable.ic_shield_warning
|
||||
).apply {
|
||||
matrixItem = user
|
||||
colorInt = ContextCompat.getColor(requireActivity(), R.color.riotx_accent)
|
||||
contentAction = Runnable {
|
||||
(weakCurrentActivity?.get() as? VectorBaseActivity)
|
||||
?.navigator
|
||||
?.requestSessionVerification(requireContext(), newest.deviceId ?: "")
|
||||
}
|
||||
dismissedAction = Runnable {}
|
||||
}
|
||||
)
|
||||
unknownDeviceDetectorSharedViewModel.subscribe { state ->
|
||||
state.unknownSessions.invoke()?.let { unknownDevices ->
|
||||
// Timber.v("## Detector Triggerred in fragment - ${unknownDevices.firstOrNull()}")
|
||||
if (unknownDevices.firstOrNull()?.currentSessionTrust == true) {
|
||||
val uid = "review_login"
|
||||
alertManager.cancelAlert(uid)
|
||||
val olderUnverified = unknownDevices.filter { !it.isNew }
|
||||
val newest = unknownDevices.firstOrNull { it.isNew }?.deviceInfo
|
||||
if (newest != null) {
|
||||
promptForNewUnknownDevices(uid, state, newest)
|
||||
} else if (olderUnverified.isNotEmpty()) {
|
||||
// In this case we prompt to go to settings to review logins
|
||||
promptToReviewChanges(uid, state, olderUnverified.map { it.deviceInfo })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun promptForNewUnknownDevices(uid: String, state: UnknownDevicesState, newest: DeviceInfo) {
|
||||
val user = state.myMatrixItem
|
||||
alertManager.postVectorAlert(
|
||||
VerificationVectorAlert(
|
||||
uid = uid,
|
||||
title = getString(R.string.new_session),
|
||||
description = getString(R.string.new_session_review_with_info, newest.displayName ?: "", newest.deviceId ?: ""),
|
||||
iconId = R.drawable.ic_shield_warning
|
||||
).apply {
|
||||
matrixItem = user
|
||||
colorInt = ContextCompat.getColor(requireActivity(), R.color.riotx_accent)
|
||||
contentAction = Runnable {
|
||||
(weakCurrentActivity?.get() as? VectorBaseActivity)
|
||||
?.navigator
|
||||
?.requestSessionVerification(requireContext(), newest.deviceId ?: "")
|
||||
unknownDeviceDetectorSharedViewModel.handle(
|
||||
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) } ?: emptyList())
|
||||
)
|
||||
}
|
||||
dismissedAction = Runnable {
|
||||
unknownDeviceDetectorSharedViewModel.handle(
|
||||
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) } ?: emptyList())
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun promptToReviewChanges(uid: String, state: UnknownDevicesState, oldUnverified: List<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?) {
|
||||
groupSummary?.let {
|
||||
// Use GlideApp with activity context to avoid the glideRequests to be paused
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -957,8 +957,8 @@ class RoomDetailFragment @Inject constructor(
|
||||
.setMessage(
|
||||
getString(R.string.external_link_confirmation_message, title, url)
|
||||
.toSpannable()
|
||||
.colorizeMatchingText(url, colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
|
||||
.colorizeMatchingText(title, colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
|
||||
.colorizeMatchingText(url, colorProvider.getColorFromAttribute(R.attr.riotx_text_primary_body_contrast))
|
||||
.colorizeMatchingText(title, colorProvider.getColorFromAttribute(R.attr.riotx_text_primary_body_contrast))
|
||||
)
|
||||
.setPositiveButton(R.string._continue) { _, _ ->
|
||||
openUrlInExternalBrowser(requireContext(), url)
|
||||
|
@ -31,6 +31,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import im.vector.riotx.core.utils.isValidUrl
|
||||
|
||||
fun CharSequence.findPillsAndProcess(scope: CoroutineScope, processBlock: (PillImageSpan) -> Unit) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
@ -59,14 +60,16 @@ fun CharSequence.linkify(callback: TimelineEventController.UrlClickCallback?): C
|
||||
fun createLinkMovementMethod(urlClickCallback: TimelineEventController.UrlClickCallback?): EvenBetterLinkMovementMethod {
|
||||
return EvenBetterLinkMovementMethod(object : EvenBetterLinkMovementMethod.OnLinkClickListener {
|
||||
override fun onLinkClicked(textView: TextView, span: ClickableSpan, url: String, actualText: String): Boolean {
|
||||
return urlClickCallback?.onUrlClicked(url, actualText) == true
|
||||
// Always return false if the url is not valid, so the EvenBetterLinkMovementMethod can fallback to default click listener.
|
||||
return url.isValidUrl() && urlClickCallback?.onUrlClicked(url, actualText) == true
|
||||
}
|
||||
})
|
||||
.apply {
|
||||
// We need also to fix the case when long click on link will trigger long click on cell
|
||||
setOnLinkLongClickListener { tv, url ->
|
||||
// Long clicks are handled by parent, return true to block android to do something with url
|
||||
if (urlClickCallback?.onUrlLongClicked(url) == true) {
|
||||
// Always return false if the url is not valid, so the EvenBetterLinkMovementMethod can fallback to default click listener.
|
||||
if (url.isValidUrl() && urlClickCallback?.onUrlLongClicked(url) == true) {
|
||||
tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0))
|
||||
true
|
||||
} else {
|
||||
|
@ -31,9 +31,14 @@ import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.airbnb.mvrx.activityViewModel
|
||||
import im.vector.matrix.android.api.auth.LOGIN_FALLBACK_PATH
|
||||
import im.vector.matrix.android.api.auth.REGISTER_FALLBACK_PATH
|
||||
import im.vector.matrix.android.api.auth.SSO_FALLBACK_PATH
|
||||
import im.vector.matrix.android.api.auth.SSO_REDIRECT_URL_PARAM
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.extensions.appendParamToUrl
|
||||
import im.vector.riotx.core.utils.AssetReader
|
||||
import im.vector.riotx.features.signout.soft.SoftLogoutAction
|
||||
import im.vector.riotx.features.signout.soft.SoftLogoutViewModel
|
||||
@ -123,14 +128,24 @@ class LoginWebFragment @Inject constructor(
|
||||
val url = buildString {
|
||||
append(state.homeServerUrl?.trim { it == '/' })
|
||||
if (state.signMode == SignMode.SignIn) {
|
||||
append("/_matrix/static/client/login/")
|
||||
if (state.loginMode == LoginMode.Sso) {
|
||||
append(SSO_FALLBACK_PATH)
|
||||
// We do not want to deal with the result, so let the fallback login page to handle it for us
|
||||
appendParamToUrl(SSO_REDIRECT_URL_PARAM,
|
||||
buildString {
|
||||
append(state.homeServerUrl?.trim { it == '/' })
|
||||
append(LOGIN_FALLBACK_PATH)
|
||||
})
|
||||
} else {
|
||||
append(LOGIN_FALLBACK_PATH)
|
||||
}
|
||||
state.deviceId?.takeIf { it.isNotBlank() }?.let {
|
||||
// But https://github.com/matrix-org/synapse/issues/5755
|
||||
append("?device_id=$it")
|
||||
appendParamToUrl("device_id", it)
|
||||
}
|
||||
} else {
|
||||
// MODE_REGISTER
|
||||
append("/_matrix/static/client/register/")
|
||||
append(REGISTER_FALLBACK_PATH)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,7 +96,7 @@ class DefaultNavigator @Inject constructor(
|
||||
roomId = null,
|
||||
otherUserId = session.myUserId,
|
||||
transactionId = pr.transactionId
|
||||
).show(context.supportFragmentManager, "REQPOP")
|
||||
).show(context.supportFragmentManager, VerificationBottomSheet.WAITING_SELF_VERIF_TAG)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,6 +32,23 @@ class EmojiDataSource @Inject constructor(
|
||||
.adapter(EmojiData::class.java)
|
||||
.fromJson(input.bufferedReader().use { it.readText() })
|
||||
}
|
||||
?.let { parsedRawData ->
|
||||
// Add key as a keyword, it will solve the issue that ":tada" is not available in completion
|
||||
parsedRawData.copy(
|
||||
emojis = mutableMapOf<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())
|
||||
|
||||
private val quickReactions = mutableListOf<EmojiItem>()
|
||||
|
@ -101,7 +101,7 @@ class DeviceTrustInfoEpoxyController @Inject constructor(private val stringProvi
|
||||
|
||||
bottomSheetVerificationActionItem {
|
||||
id("verify")
|
||||
title(stringProvider.getString(R.string.verification_verify_device_manually))
|
||||
title(stringProvider.getString(R.string.cross_signing_verify_by_emoji))
|
||||
titleColor(colorProvider.getColor(R.color.riotx_accent))
|
||||
iconRes(R.drawable.ic_arrow_right)
|
||||
iconColor(colorProvider.getColor(R.color.riotx_accent))
|
||||
|
@ -24,6 +24,7 @@ import android.provider.MediaStore
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.squareup.seismic.ShakeDetector
|
||||
import im.vector.matrix.android.api.extensions.tryThis
|
||||
import im.vector.riotx.BuildConfig
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.features.homeserver.ServerUrlsRepository
|
||||
@ -166,6 +167,8 @@ class VectorPreferences @Inject constructor(private val context: Context) {
|
||||
private const val MEDIA_SAVING_1_MONTH = 2
|
||||
private const val MEDIA_SAVING_FOREVER = 3
|
||||
|
||||
private const val SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST = "SETTINGS_UNKNWON_DEVICE_DISMISSED_LIST"
|
||||
|
||||
// some preferences keys must be kept after a logout
|
||||
private val mKeysToKeepAfterLogout = listOf(
|
||||
SETTINGS_DEFAULT_MEDIA_COMPRESSION_KEY,
|
||||
@ -201,6 +204,11 @@ class VectorPreferences @Inject constructor(private val context: Context) {
|
||||
SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY,
|
||||
SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY,
|
||||
|
||||
SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY,
|
||||
SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY,
|
||||
SETTINGS_LABS_ALLOW_EXTENDED_LOGS,
|
||||
SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY,
|
||||
|
||||
SETTINGS_USE_RAGE_SHAKE_KEY,
|
||||
SETTINGS_SECURITY_USE_FLAG_SECURE
|
||||
)
|
||||
@ -364,6 +372,18 @@ class VectorPreferences @Inject constructor(private val context: Context) {
|
||||
return defaultPrefs.getBoolean(SETTINGS_PLAY_SHUTTER_SOUND_KEY, true)
|
||||
}
|
||||
|
||||
fun storeUnknownDeviceDismissedList(deviceIds: List<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
|
||||
*
|
||||
|
@ -26,6 +26,7 @@ import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
import im.vector.riotx.core.extensions.replaceFragment
|
||||
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||
import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment
|
||||
import kotlinx.android.synthetic.main.activity_vector_settings.*
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
@ -58,11 +59,16 @@ class VectorSettingsActivity : VectorBaseActivity(),
|
||||
if (isFirstCreation()) {
|
||||
// display the fragment
|
||||
when (intent.getIntExtra(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOT)) {
|
||||
EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS ->
|
||||
EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS ->
|
||||
replaceFragment(R.id.vector_settings_page, VectorSettingsAdvancedSettingsFragment::class.java, null, FRAGMENT_TAG)
|
||||
EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY ->
|
||||
EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY ->
|
||||
replaceFragment(R.id.vector_settings_page, VectorSettingsSecurityPrivacyFragment::class.java, null, FRAGMENT_TAG)
|
||||
else ->
|
||||
EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS ->
|
||||
replaceFragment(R.id.vector_settings_page,
|
||||
VectorSettingsDevicesFragment::class.java,
|
||||
null,
|
||||
FRAGMENT_TAG)
|
||||
else ->
|
||||
replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG)
|
||||
}
|
||||
}
|
||||
@ -130,6 +136,7 @@ class VectorSettingsActivity : VectorBaseActivity(),
|
||||
const val EXTRA_DIRECT_ACCESS_ROOT = 0
|
||||
const val EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS = 1
|
||||
const val EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY = 2
|
||||
const val EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS = 3
|
||||
|
||||
private const val FRAGMENT_TAG = "VectorSettingsPreferencesFragment"
|
||||
}
|
||||
|
@ -412,7 +412,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
||||
refreshCryptographyPreference(it)
|
||||
}
|
||||
// TODO Move to a ViewModel...
|
||||
session.cryptoService().getDevicesList(object : MatrixCallback<DevicesListResponse> {
|
||||
session.cryptoService().fetchDevicesList(object : MatrixCallback<DevicesListResponse> {
|
||||
override fun onSuccess(data: DevicesListResponse) {
|
||||
if (isAdded) {
|
||||
refreshCryptographyPreference(data.devices ?: emptyList())
|
||||
|
@ -37,7 +37,6 @@ class CrossSigningEpoxyController @Inject constructor(
|
||||
|
||||
interface InteractionListener {
|
||||
fun onInitializeCrossSigningKeys()
|
||||
fun onResetCrossSigningKeys()
|
||||
fun verifySession()
|
||||
}
|
||||
|
||||
@ -51,18 +50,6 @@ class CrossSigningEpoxyController @Inject constructor(
|
||||
titleIconResourceId(R.drawable.ic_shield_trusted)
|
||||
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_complete))
|
||||
}
|
||||
if (vectorPreferences.developerMode() && !data.isUploadingKeys) {
|
||||
bottomSheetVerificationActionItem {
|
||||
id("resetkeys")
|
||||
title("Reset keys")
|
||||
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
|
||||
iconRes(R.drawable.ic_arrow_right)
|
||||
iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
|
||||
listener {
|
||||
interactionListener?.onResetCrossSigningKeys()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (data.xSigningKeysAreTrusted) {
|
||||
genericItem {
|
||||
id("trusted")
|
||||
@ -70,22 +57,9 @@ class CrossSigningEpoxyController @Inject constructor(
|
||||
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_trusted))
|
||||
}
|
||||
if (!data.isUploadingKeys) {
|
||||
if (vectorPreferences.developerMode()) {
|
||||
bottomSheetVerificationActionItem {
|
||||
id("resetkeys")
|
||||
title("Reset keys")
|
||||
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
|
||||
iconRes(R.drawable.ic_arrow_right)
|
||||
iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
|
||||
listener {
|
||||
interactionListener?.onResetCrossSigningKeys()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bottomSheetVerificationActionItem {
|
||||
id("verify")
|
||||
title(stringProvider.getString(R.string.complete_security))
|
||||
title(stringProvider.getString(R.string.crosssigning_verify_this_session))
|
||||
titleColor(colorProvider.getColor(R.color.riotx_positive_accent))
|
||||
iconRes(R.drawable.ic_arrow_right)
|
||||
iconColor(colorProvider.getColor(R.color.riotx_positive_accent))
|
||||
@ -102,7 +76,7 @@ class CrossSigningEpoxyController @Inject constructor(
|
||||
}
|
||||
bottomSheetVerificationActionItem {
|
||||
id("verify")
|
||||
title(stringProvider.getString(R.string.complete_security))
|
||||
title(stringProvider.getString(R.string.crosssigning_verify_this_session))
|
||||
titleColor(colorProvider.getColor(R.color.riotx_positive_accent))
|
||||
iconRes(R.drawable.ic_arrow_right)
|
||||
iconColor(colorProvider.getColor(R.color.riotx_positive_accent))
|
||||
@ -110,18 +84,6 @@ class CrossSigningEpoxyController @Inject constructor(
|
||||
interactionListener?.verifySession()
|
||||
}
|
||||
}
|
||||
if (vectorPreferences.developerMode()) {
|
||||
bottomSheetVerificationActionItem {
|
||||
id("resetkeys")
|
||||
title("Reset keys")
|
||||
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
|
||||
iconRes(R.drawable.ic_arrow_right)
|
||||
iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
|
||||
listener {
|
||||
interactionListener?.onResetCrossSigningKeys()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
genericItem {
|
||||
id("not")
|
||||
@ -130,7 +92,7 @@ class CrossSigningEpoxyController @Inject constructor(
|
||||
if (vectorPreferences.developerMode() && !data.isUploadingKeys) {
|
||||
bottomSheetVerificationActionItem {
|
||||
id("initKeys")
|
||||
title("Initialize keys")
|
||||
title(stringProvider.getString(R.string.initialize_cross_signing))
|
||||
titleColor(colorProvider.getColor(R.color.riotx_positive_accent))
|
||||
iconRes(R.drawable.ic_arrow_right)
|
||||
iconColor(colorProvider.getColor(R.color.riotx_positive_accent))
|
||||
|
@ -101,14 +101,4 @@ class CrossSigningSettingsFragment @Inject constructor(
|
||||
override fun verifySession() {
|
||||
viewModel.handle(CrossSigningAction.VerifySession)
|
||||
}
|
||||
|
||||
override fun onResetCrossSigningKeys() {
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.dialog_title_confirmation)
|
||||
.setMessage(R.string.are_you_sure)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
viewModel.handle(CrossSigningAction.InitializeCrossSigning)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
@ -22,14 +22,12 @@ import com.airbnb.mvrx.ViewModelContext
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.api.failure.toRegistrationFlowResponse
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
|
||||
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.isVerified
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import im.vector.matrix.rx.rx
|
||||
import im.vector.riotx.core.extensions.exhaustive
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
@ -113,35 +111,26 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted privat
|
||||
override fun onFailure(failure: Throwable) {
|
||||
_pendingSession = null
|
||||
|
||||
if (failure is Failure.OtherServerError && failure.httpCode == 401) {
|
||||
try {
|
||||
MoshiProvider.providesMoshi()
|
||||
.adapter(RegistrationFlowResponse::class.java)
|
||||
.fromJson(failure.errorBody)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}?.let { flowResponse ->
|
||||
// Retry with authentication
|
||||
if (flowResponse.flows?.any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true } == true) {
|
||||
_pendingSession = flowResponse.session ?: ""
|
||||
_viewEvents.post(CrossSigningSettingsViewEvents.RequestPassword)
|
||||
return
|
||||
} else {
|
||||
// can't do this from here
|
||||
_viewEvents.post(CrossSigningSettingsViewEvents.Failure(Throwable("You cannot do that from mobile")))
|
||||
val registrationFlowResponse = failure.toRegistrationFlowResponse()
|
||||
if (registrationFlowResponse != null) {
|
||||
// Retry with authentication
|
||||
if (registrationFlowResponse.flows?.any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true } == true) {
|
||||
_pendingSession = registrationFlowResponse.session ?: ""
|
||||
_viewEvents.post(CrossSigningSettingsViewEvents.RequestPassword)
|
||||
} else {
|
||||
// can't do this from here
|
||||
_viewEvents.post(CrossSigningSettingsViewEvents.Failure(Throwable("You cannot do that from mobile")))
|
||||
|
||||
setState {
|
||||
copy(isUploadingKeys = false)
|
||||
}
|
||||
return
|
||||
setState {
|
||||
copy(isUploadingKeys = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_viewEvents.post(CrossSigningSettingsViewEvents.Failure(failure))
|
||||
|
||||
_viewEvents.post(CrossSigningSettingsViewEvents.Failure(failure))
|
||||
|
||||
setState {
|
||||
copy(isUploadingKeys = false)
|
||||
setState {
|
||||
copy(isUploadingKeys = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -20,15 +20,17 @@ import android.graphics.Typeface
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.riotx.core.resources.ColorProvider
|
||||
import im.vector.riotx.core.utils.DimensionConverter
|
||||
import me.gujun.android.span.span
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
@ -53,21 +55,37 @@ abstract class DeviceItem : VectorEpoxyModel<DeviceItem.Holder>() {
|
||||
var detailedMode = false
|
||||
|
||||
@EpoxyAttribute
|
||||
var trusted : Boolean? = null
|
||||
var trusted: DeviceTrustLevel? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var e2eCapable: Boolean = true
|
||||
|
||||
@EpoxyAttribute
|
||||
var legacyMode: Boolean = false
|
||||
|
||||
@EpoxyAttribute
|
||||
var trustedSession: Boolean = false
|
||||
|
||||
@EpoxyAttribute
|
||||
var colorProvider: ColorProvider? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var dimensionConverter: DimensionConverter? = null
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
holder.root.setOnClickListener { itemClickAction?.invoke() }
|
||||
|
||||
if (trusted != null) {
|
||||
holder.trustIcon.setImageDrawable(
|
||||
ContextCompat.getDrawable(
|
||||
holder.view.context,
|
||||
if (trusted!!) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning
|
||||
)
|
||||
)
|
||||
holder.trustIcon.isInvisible = false
|
||||
val shield = TrustUtils.shieldForTrust(
|
||||
currentDevice,
|
||||
trustedSession,
|
||||
legacyMode,
|
||||
trusted
|
||||
)
|
||||
|
||||
if (e2eCapable) {
|
||||
holder.trustIcon.setImageResource(shield)
|
||||
} else {
|
||||
holder.trustIcon.isInvisible = true
|
||||
holder.trustIcon.setImageDrawable(null)
|
||||
}
|
||||
|
||||
val detailedModeLabels = listOf(
|
||||
@ -103,7 +121,28 @@ abstract class DeviceItem : VectorEpoxyModel<DeviceItem.Holder>() {
|
||||
it.setTypeface(null, if (currentDevice) Typeface.BOLD else Typeface.NORMAL)
|
||||
}
|
||||
} else {
|
||||
holder.summaryLabelText.text = deviceInfo.displayName ?: deviceInfo.deviceId ?: ""
|
||||
holder.summaryLabelText.text =
|
||||
span {
|
||||
+(deviceInfo.displayName ?: deviceInfo.deviceId ?: "")
|
||||
apply {
|
||||
// Add additional info if current session is not trusted
|
||||
if (!trustedSession) {
|
||||
+"\n"
|
||||
span {
|
||||
text = "${deviceInfo.deviceId}"
|
||||
apply {
|
||||
colorProvider?.getColorFromAttribute(R.attr.riotx_text_secondary)?.let {
|
||||
textColor = it
|
||||
}
|
||||
dimensionConverter?.spToPx(12)?.let {
|
||||
textSize = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
holder.summaryLabelText.isVisible = true
|
||||
detailedModeLabels.map {
|
||||
it.isVisible = false
|
||||
|
@ -16,17 +16,14 @@
|
||||
package im.vector.riotx.features.settings.devices
|
||||
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.FragmentViewModelContext
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||
@ -37,7 +34,10 @@ import im.vector.riotx.core.platform.VectorViewModel
|
||||
|
||||
data class DeviceVerificationInfoBottomSheetViewState(
|
||||
val cryptoDeviceInfo: Async<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
|
||||
|
||||
class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: DeviceVerificationInfoBottomSheetViewState,
|
||||
@ -51,31 +51,43 @@ class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@As
|
||||
}
|
||||
|
||||
init {
|
||||
|
||||
setState {
|
||||
copy(
|
||||
hasAccountCrossSigning = session.cryptoService().crossSigningService().getMyCrossSigningKeys() != null,
|
||||
accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified()
|
||||
)
|
||||
}
|
||||
session.rx().liveCrossSigningInfo(session.myUserId)
|
||||
.execute {
|
||||
copy(
|
||||
hasAccountCrossSigning = it.invoke()?.getOrNull() != null,
|
||||
accountCrossSigningIsTrusted = it.invoke()?.getOrNull()?.isTrusted() == true
|
||||
)
|
||||
}
|
||||
|
||||
session.rx().liveUserCryptoDevices(session.myUserId)
|
||||
.map { list ->
|
||||
list.firstOrNull { it.deviceId == deviceId }
|
||||
}
|
||||
.execute {
|
||||
copy(
|
||||
cryptoDeviceInfo = it
|
||||
cryptoDeviceInfo = it,
|
||||
isMine = it.invoke()?.deviceId == session.sessionParams.credentials.deviceId
|
||||
)
|
||||
}
|
||||
|
||||
setState {
|
||||
copy(deviceInfo = Loading())
|
||||
}
|
||||
session.cryptoService().getDeviceInfo(deviceId, object : MatrixCallback<DeviceInfo> {
|
||||
override fun onSuccess(data: DeviceInfo) {
|
||||
setState {
|
||||
copy(deviceInfo = Success(data))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
setState {
|
||||
copy(deviceInfo = Fail(failure))
|
||||
session.rx().liveMyDeviceInfo()
|
||||
.map { devices ->
|
||||
devices.firstOrNull { it.deviceId == deviceId } ?: DeviceInfo(deviceId = deviceId)
|
||||
}
|
||||
.execute {
|
||||
copy(deviceInfo = it)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
companion object : MvRxViewModelFactory<DeviceVerificationInfoBottomSheetViewModel, DeviceVerificationInfoBottomSheetViewState> {
|
||||
|
@ -16,7 +16,9 @@
|
||||
package im.vector.riotx.features.settings.devices
|
||||
|
||||
import com.airbnb.epoxy.TypedEpoxyController
|
||||
import im.vector.matrix.android.api.extensions.orFalse
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.epoxy.dividerItem
|
||||
import im.vector.riotx.core.epoxy.loadingItem
|
||||
@ -26,6 +28,7 @@ import im.vector.riotx.core.ui.list.GenericItem
|
||||
import im.vector.riotx.core.ui.list.genericFooterItem
|
||||
import im.vector.riotx.core.ui.list.genericItem
|
||||
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class DeviceVerificationInfoEpoxyController @Inject constructor(private val stringProvider: StringProvider,
|
||||
@ -37,111 +40,251 @@ class DeviceVerificationInfoEpoxyController @Inject constructor(private val stri
|
||||
|
||||
override fun buildModels(data: DeviceVerificationInfoBottomSheetViewState?) {
|
||||
val cryptoDeviceInfo = data?.cryptoDeviceInfo?.invoke()
|
||||
if (cryptoDeviceInfo != null) {
|
||||
if (cryptoDeviceInfo.isVerified) {
|
||||
when {
|
||||
cryptoDeviceInfo != null -> {
|
||||
// It's a E2E capable device
|
||||
handleE2ECapableDevice(data, cryptoDeviceInfo)
|
||||
}
|
||||
data?.deviceInfo?.invoke() != null -> {
|
||||
// It's a non E2E capable device
|
||||
handleNonE2EDevice(data)
|
||||
}
|
||||
else -> {
|
||||
loadingItem {
|
||||
id("loading")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleE2ECapableDevice(data: DeviceVerificationInfoBottomSheetViewState, cryptoDeviceInfo: CryptoDeviceInfo) {
|
||||
val shield = TrustUtils.shieldForTrust(
|
||||
currentDevice = data.isMine,
|
||||
trustMSK = data.accountCrossSigningIsTrusted,
|
||||
legacyMode = !data.hasAccountCrossSigning,
|
||||
deviceTrustLevel = cryptoDeviceInfo.trustLevel
|
||||
)
|
||||
|
||||
if (data.hasAccountCrossSigning) {
|
||||
// Cross Signing is enabled
|
||||
handleE2EWithCrossSigning(data.isMine, data.accountCrossSigningIsTrusted, cryptoDeviceInfo, shield)
|
||||
} else {
|
||||
handleE2EInLegacy(data.isMine, cryptoDeviceInfo, shield)
|
||||
}
|
||||
|
||||
// COMMON ACTIONS (Rename / signout)
|
||||
addGenericDeviceManageActions(data, cryptoDeviceInfo.deviceId)
|
||||
}
|
||||
|
||||
private fun handleE2EWithCrossSigning(isMine: Boolean, currentSessionIsTrusted: Boolean, cryptoDeviceInfo: CryptoDeviceInfo, shield: Int) {
|
||||
Timber.v("handleE2EWithCrossSigning $isMine, $cryptoDeviceInfo, $shield")
|
||||
|
||||
if (isMine) {
|
||||
if (currentSessionIsTrusted) {
|
||||
genericItem {
|
||||
id("trust${cryptoDeviceInfo.deviceId}")
|
||||
style(GenericItem.STYLE.BIG_TEXT)
|
||||
titleIconResourceId(R.drawable.ic_shield_trusted)
|
||||
titleIconResourceId(shield)
|
||||
title(stringProvider.getString(R.string.encryption_information_verified))
|
||||
description(stringProvider.getString(R.string.settings_active_sessions_verified_device_desc))
|
||||
}
|
||||
} else {
|
||||
// You need tomcomplete security
|
||||
genericItem {
|
||||
id("trust${cryptoDeviceInfo.deviceId}")
|
||||
titleIconResourceId(R.drawable.ic_shield_warning)
|
||||
style(GenericItem.STYLE.BIG_TEXT)
|
||||
title(stringProvider.getString(R.string.encryption_information_not_verified))
|
||||
description(stringProvider.getString(R.string.settings_active_sessions_unverified_device_desc))
|
||||
}
|
||||
}
|
||||
|
||||
genericItem {
|
||||
id("info${cryptoDeviceInfo.deviceId}")
|
||||
title(cryptoDeviceInfo.displayName() ?: "")
|
||||
description("(${cryptoDeviceInfo.deviceId})")
|
||||
}
|
||||
|
||||
if (!cryptoDeviceInfo.isVerified) {
|
||||
dividerItem {
|
||||
id("d1")
|
||||
}
|
||||
bottomSheetVerificationActionItem {
|
||||
id("verify")
|
||||
title(stringProvider.getString(R.string.verification_verify_device))
|
||||
titleColor(colorProvider.getColor(R.color.riotx_accent))
|
||||
iconRes(R.drawable.ic_arrow_right)
|
||||
iconColor(colorProvider.getColor(R.color.riotx_accent))
|
||||
listener {
|
||||
callback?.onAction(DevicesAction.VerifyMyDevice(cryptoDeviceInfo.deviceId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cryptoDeviceInfo.deviceId != session.sessionParams.credentials.deviceId) {
|
||||
// Add the delete option
|
||||
dividerItem {
|
||||
id("d2")
|
||||
}
|
||||
bottomSheetVerificationActionItem {
|
||||
id("delete")
|
||||
title(stringProvider.getString(R.string.settings_active_sessions_signout_device))
|
||||
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
|
||||
iconRes(R.drawable.ic_arrow_right)
|
||||
iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
|
||||
listener {
|
||||
callback?.onAction(DevicesAction.Delete(cryptoDeviceInfo.deviceId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dividerItem {
|
||||
id("d3")
|
||||
}
|
||||
bottomSheetVerificationActionItem {
|
||||
id("rename")
|
||||
title(stringProvider.getString(R.string.rename))
|
||||
titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
|
||||
iconRes(R.drawable.ic_arrow_right)
|
||||
iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
|
||||
listener {
|
||||
callback?.onAction(DevicesAction.PromptRename(cryptoDeviceInfo.deviceId))
|
||||
}
|
||||
}
|
||||
} else if (data?.deviceInfo?.invoke() != null) {
|
||||
val info = data.deviceInfo.invoke()
|
||||
genericItem {
|
||||
id("info${info?.deviceId}")
|
||||
title(info?.displayName ?: "")
|
||||
description("(${info?.deviceId})")
|
||||
}
|
||||
|
||||
genericFooterItem {
|
||||
id("infoCrypto${info?.deviceId}")
|
||||
text(stringProvider.getString(R.string.settings_failed_to_get_crypto_device_info))
|
||||
}
|
||||
|
||||
if (info?.deviceId != session.sessionParams.credentials.deviceId) {
|
||||
// Add the delete option
|
||||
dividerItem {
|
||||
id("d2")
|
||||
}
|
||||
bottomSheetVerificationActionItem {
|
||||
id("delete")
|
||||
title(stringProvider.getString(R.string.settings_active_sessions_signout_device))
|
||||
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
|
||||
iconRes(R.drawable.ic_arrow_right)
|
||||
iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
|
||||
listener {
|
||||
callback?.onAction(DevicesAction.Delete(info?.deviceId ?: ""))
|
||||
}
|
||||
titleIconResourceId(shield)
|
||||
title(stringProvider.getString(R.string.crosssigning_verify_this_session))
|
||||
description(stringProvider.getString(R.string.confirm_your_identity))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
loadingItem {
|
||||
id("loading")
|
||||
if (!currentSessionIsTrusted) {
|
||||
// we don't know if this session is trusted...
|
||||
// for now we show nothing?
|
||||
} else {
|
||||
// we rely on cross signing status
|
||||
val trust = cryptoDeviceInfo.trustLevel?.isCrossSigningVerified() == true
|
||||
if (trust) {
|
||||
genericItem {
|
||||
id("trust${cryptoDeviceInfo.deviceId}")
|
||||
style(GenericItem.STYLE.BIG_TEXT)
|
||||
titleIconResourceId(shield)
|
||||
title(stringProvider.getString(R.string.encryption_information_verified))
|
||||
description(stringProvider.getString(R.string.settings_active_sessions_verified_device_desc))
|
||||
}
|
||||
} else {
|
||||
genericItem {
|
||||
id("trust${cryptoDeviceInfo.deviceId}")
|
||||
titleIconResourceId(shield)
|
||||
style(GenericItem.STYLE.BIG_TEXT)
|
||||
title(stringProvider.getString(R.string.encryption_information_not_verified))
|
||||
description(stringProvider.getString(R.string.settings_active_sessions_unverified_device_desc))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DEVICE INFO SECTION
|
||||
genericItem {
|
||||
id("info${cryptoDeviceInfo.deviceId}")
|
||||
title(cryptoDeviceInfo.displayName() ?: "")
|
||||
description("(${cryptoDeviceInfo.deviceId})")
|
||||
}
|
||||
|
||||
if (isMine && !currentSessionIsTrusted) {
|
||||
// Add complete security
|
||||
dividerItem {
|
||||
id("completeSecurityDiv")
|
||||
}
|
||||
bottomSheetVerificationActionItem {
|
||||
id("completeSecurity")
|
||||
title(stringProvider.getString(R.string.crosssigning_verify_this_session))
|
||||
titleColor(colorProvider.getColor(R.color.riotx_accent))
|
||||
iconRes(R.drawable.ic_arrow_right)
|
||||
iconColor(colorProvider.getColor(R.color.riotx_accent))
|
||||
listener {
|
||||
callback?.onAction(DevicesAction.CompleteSecurity)
|
||||
}
|
||||
}
|
||||
} else if (!isMine) {
|
||||
if (currentSessionIsTrusted) {
|
||||
// we can propose to verify it
|
||||
val isVerified = cryptoDeviceInfo.trustLevel?.crossSigningVerified.orFalse()
|
||||
if (!isVerified) {
|
||||
addVerifyActions(cryptoDeviceInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleE2EInLegacy(isMine: Boolean, cryptoDeviceInfo: CryptoDeviceInfo, shield: Int) {
|
||||
// ==== Legacy
|
||||
|
||||
// TRUST INFO SECTION
|
||||
if (cryptoDeviceInfo.trustLevel?.isLocallyVerified() == true) {
|
||||
genericItem {
|
||||
id("trust${cryptoDeviceInfo.deviceId}")
|
||||
style(GenericItem.STYLE.BIG_TEXT)
|
||||
titleIconResourceId(shield)
|
||||
title(stringProvider.getString(R.string.encryption_information_verified))
|
||||
description(stringProvider.getString(R.string.settings_active_sessions_verified_device_desc))
|
||||
}
|
||||
} else {
|
||||
genericItem {
|
||||
id("trust${cryptoDeviceInfo.deviceId}")
|
||||
titleIconResourceId(shield)
|
||||
style(GenericItem.STYLE.BIG_TEXT)
|
||||
title(stringProvider.getString(R.string.encryption_information_not_verified))
|
||||
description(stringProvider.getString(R.string.settings_active_sessions_unverified_device_desc))
|
||||
}
|
||||
}
|
||||
|
||||
// DEVICE INFO SECTION
|
||||
genericItem {
|
||||
id("info${cryptoDeviceInfo.deviceId}")
|
||||
title(cryptoDeviceInfo.displayName() ?: "")
|
||||
description("(${cryptoDeviceInfo.deviceId})")
|
||||
}
|
||||
|
||||
// ACTIONS
|
||||
|
||||
if (!isMine) {
|
||||
// if it's not the current device you can trigger a verification
|
||||
dividerItem {
|
||||
id("d1")
|
||||
}
|
||||
bottomSheetVerificationActionItem {
|
||||
id("verify${cryptoDeviceInfo.deviceId}")
|
||||
title(stringProvider.getString(R.string.verification_verify_device))
|
||||
titleColor(colorProvider.getColor(R.color.riotx_accent))
|
||||
iconRes(R.drawable.ic_arrow_right)
|
||||
iconColor(colorProvider.getColor(R.color.riotx_accent))
|
||||
listener {
|
||||
callback?.onAction(DevicesAction.VerifyMyDevice(cryptoDeviceInfo.deviceId))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addVerifyActions(cryptoDeviceInfo: CryptoDeviceInfo) {
|
||||
dividerItem {
|
||||
id("verifyDiv")
|
||||
}
|
||||
bottomSheetVerificationActionItem {
|
||||
id("verify_text")
|
||||
title(stringProvider.getString(R.string.cross_signing_verify_by_text))
|
||||
titleColor(colorProvider.getColor(R.color.riotx_accent))
|
||||
iconRes(R.drawable.ic_arrow_right)
|
||||
iconColor(colorProvider.getColor(R.color.riotx_accent))
|
||||
listener {
|
||||
callback?.onAction(DevicesAction.VerifyMyDeviceManually(cryptoDeviceInfo.deviceId))
|
||||
}
|
||||
}
|
||||
dividerItem {
|
||||
id("verifyDiv2")
|
||||
}
|
||||
bottomSheetVerificationActionItem {
|
||||
id("verify_emoji")
|
||||
title(stringProvider.getString(R.string.cross_signing_verify_by_emoji))
|
||||
titleColor(colorProvider.getColor(R.color.riotx_accent))
|
||||
iconRes(R.drawable.ic_arrow_right)
|
||||
iconColor(colorProvider.getColor(R.color.riotx_accent))
|
||||
listener {
|
||||
callback?.onAction(DevicesAction.VerifyMyDevice(cryptoDeviceInfo.deviceId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addGenericDeviceManageActions(data: DeviceVerificationInfoBottomSheetViewState, deviceId: String) {
|
||||
// Offer delete session if not me
|
||||
if (!data.isMine) {
|
||||
// Add the delete option
|
||||
dividerItem {
|
||||
id("manageD1")
|
||||
}
|
||||
bottomSheetVerificationActionItem {
|
||||
id("delete")
|
||||
title(stringProvider.getString(R.string.settings_active_sessions_signout_device))
|
||||
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
|
||||
iconRes(R.drawable.ic_arrow_right)
|
||||
iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
|
||||
listener {
|
||||
callback?.onAction(DevicesAction.Delete(deviceId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Always offer rename
|
||||
dividerItem {
|
||||
id("manageD2")
|
||||
}
|
||||
bottomSheetVerificationActionItem {
|
||||
id("rename")
|
||||
title(stringProvider.getString(R.string.rename))
|
||||
titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
|
||||
iconRes(R.drawable.ic_arrow_right)
|
||||
iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
|
||||
listener {
|
||||
callback?.onAction(DevicesAction.PromptRename(deviceId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNonE2EDevice(data: DeviceVerificationInfoBottomSheetViewState) {
|
||||
val info = data.deviceInfo.invoke() ?: return
|
||||
genericItem {
|
||||
id("info${info.deviceId}")
|
||||
title(info.displayName ?: "")
|
||||
description("(${info.deviceId})")
|
||||
}
|
||||
|
||||
genericFooterItem {
|
||||
id("infoCrypto${info.deviceId}")
|
||||
text(stringProvider.getString(R.string.settings_failed_to_get_crypto_device_info))
|
||||
}
|
||||
|
||||
info.deviceId?.let { addGenericDeviceManageActions(data, it) }
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
|
@ -16,14 +16,18 @@
|
||||
|
||||
package im.vector.riotx.features.settings.devices
|
||||
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.riotx.core.platform.VectorViewModelAction
|
||||
|
||||
sealed class DevicesAction : VectorViewModelAction {
|
||||
object Retry : DevicesAction()
|
||||
object Refresh : DevicesAction()
|
||||
data class Delete(val deviceId: String) : DevicesAction()
|
||||
data class Password(val password: String) : DevicesAction()
|
||||
data class Rename(val deviceId: String, val newName: String) : DevicesAction()
|
||||
|
||||
data class PromptRename(val deviceId: String) : DevicesAction()
|
||||
data class VerifyMyDevice(val deviceId: String) : DevicesAction()
|
||||
data class VerifyMyDeviceManually(val deviceId: String) : DevicesAction()
|
||||
object CompleteSecurity : DevicesAction()
|
||||
data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction()
|
||||
}
|
||||
|
@ -21,20 +21,23 @@ import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import im.vector.matrix.android.api.extensions.sortByLastSeen
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.epoxy.errorWithRetryItem
|
||||
import im.vector.riotx.core.epoxy.loadingItem
|
||||
import im.vector.riotx.core.error.ErrorFormatter
|
||||
import im.vector.riotx.core.resources.ColorProvider
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
import im.vector.riotx.core.ui.list.genericItemHeader
|
||||
import im.vector.riotx.core.utils.DimensionConverter
|
||||
import im.vector.riotx.features.settings.VectorPreferences
|
||||
import javax.inject.Inject
|
||||
|
||||
class DevicesController @Inject constructor(private val errorFormatter: ErrorFormatter,
|
||||
private val stringProvider: StringProvider,
|
||||
private val colorProvider: ColorProvider,
|
||||
private val dimensionConverter: DimensionConverter,
|
||||
private val vectorPreferences: VectorPreferences) : EpoxyController() {
|
||||
|
||||
var callback: Callback? = null
|
||||
@ -68,30 +71,51 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor
|
||||
listener { callback?.retry() }
|
||||
}
|
||||
is Success ->
|
||||
buildDevicesList(devices(), state.cryptoDevices(), state.myDeviceId)
|
||||
buildDevicesList(devices(), state.myDeviceId, !state.hasAccountCrossSigning, state.accountCrossSigningIsTrusted)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildDevicesList(devices: List<DeviceInfo>, cryptoDevices: List<CryptoDeviceInfo>?, myDeviceId: String) {
|
||||
// Current device
|
||||
genericItemHeader {
|
||||
id("current")
|
||||
text(stringProvider.getString(R.string.devices_current_device))
|
||||
}
|
||||
|
||||
private fun buildDevicesList(devices: List<DeviceFullInfo>,
|
||||
myDeviceId: String,
|
||||
legacyMode: Boolean,
|
||||
currentSessionCrossTrusted: Boolean) {
|
||||
devices
|
||||
.filter {
|
||||
it.deviceId == myDeviceId
|
||||
}
|
||||
.forEachIndexed { idx, deviceInfo ->
|
||||
.firstOrNull {
|
||||
it.deviceInfo.deviceId == myDeviceId
|
||||
}?.let { fullInfo ->
|
||||
val deviceInfo = fullInfo.deviceInfo
|
||||
// Current device
|
||||
genericItemHeader {
|
||||
id("current")
|
||||
text(stringProvider.getString(R.string.devices_current_device))
|
||||
}
|
||||
|
||||
deviceItem {
|
||||
id("myDevice$idx")
|
||||
id("myDevice${deviceInfo.deviceId}")
|
||||
legacyMode(legacyMode)
|
||||
trustedSession(currentSessionCrossTrusted)
|
||||
dimensionConverter(dimensionConverter)
|
||||
colorProvider(colorProvider)
|
||||
detailedMode(vectorPreferences.developerMode())
|
||||
deviceInfo(deviceInfo)
|
||||
currentDevice(true)
|
||||
e2eCapable(true)
|
||||
itemClickAction { callback?.onDeviceClicked(deviceInfo) }
|
||||
trusted(true)
|
||||
trusted(DeviceTrustLevel(currentSessionCrossTrusted, true))
|
||||
}
|
||||
|
||||
// // If cross signing enabled and this session not trusted, add short cut to complete security
|
||||
// NEED DESIGN
|
||||
// if (!legacyMode && !currentSessionCrossTrusted) {
|
||||
// genericButtonItem {
|
||||
// id("complete_security")
|
||||
// iconRes(R.drawable.ic_shield_warning)
|
||||
// text(stringProvider.getString(R.string.complete_security))
|
||||
// itemClickAction(DebouncedClickListener(View.OnClickListener { _ ->
|
||||
// callback?.completeSecurity()
|
||||
// }))
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
// Other devices
|
||||
@ -103,19 +127,23 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor
|
||||
|
||||
devices
|
||||
.filter {
|
||||
it.deviceId != myDeviceId
|
||||
it.deviceInfo.deviceId != myDeviceId
|
||||
}
|
||||
// sort before display: most recent first
|
||||
.sortByLastSeen()
|
||||
.forEachIndexed { idx, deviceInfo ->
|
||||
val isCurrentDevice = deviceInfo.deviceId == myDeviceId
|
||||
.forEachIndexed { idx, deviceInfoPair ->
|
||||
val deviceInfo = deviceInfoPair.deviceInfo
|
||||
val cryptoInfo = deviceInfoPair.cryptoDeviceInfo
|
||||
deviceItem {
|
||||
id("device$idx")
|
||||
legacyMode(legacyMode)
|
||||
trustedSession(currentSessionCrossTrusted)
|
||||
dimensionConverter(dimensionConverter)
|
||||
colorProvider(colorProvider)
|
||||
detailedMode(vectorPreferences.developerMode())
|
||||
deviceInfo(deviceInfo)
|
||||
currentDevice(isCurrentDevice)
|
||||
currentDevice(false)
|
||||
itemClickAction { callback?.onDeviceClicked(deviceInfo) }
|
||||
trusted(cryptoDevices?.firstOrNull { it.deviceId == deviceInfo.deviceId }?.isVerified)
|
||||
e2eCapable(cryptoInfo != null)
|
||||
trusted(cryptoInfo?.trustLevel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,8 @@
|
||||
|
||||
package im.vector.riotx.features.settings.devices
|
||||
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||
import im.vector.riotx.core.platform.VectorViewEvents
|
||||
|
||||
@ -35,4 +37,10 @@ sealed class DevicesViewEvents : VectorViewEvents {
|
||||
val userId: String,
|
||||
val transactionId: String?
|
||||
) : DevicesViewEvents()
|
||||
|
||||
data class SelfVerification(
|
||||
val session: Session
|
||||
) : DevicesViewEvents()
|
||||
|
||||
data class ShowManuallyVerify(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesViewEvents()
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
package im.vector.riotx.features.settings.devices
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.FragmentViewModelContext
|
||||
@ -28,33 +29,46 @@ import com.airbnb.mvrx.ViewModelContext
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.NoOpMatrixCallback
|
||||
import im.vector.matrix.android.api.extensions.tryThis
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod
|
||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationService
|
||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
|
||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
||||
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
||||
import im.vector.matrix.android.internal.util.awaitCallback
|
||||
import im.vector.matrix.rx.rx
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
import im.vector.riotx.features.crypto.verification.SupportedVerificationMethodsProvider
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.functions.BiFunction
|
||||
import io.reactivex.subjects.PublishSubject
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
data class DevicesViewState(
|
||||
val myDeviceId: String = "",
|
||||
val devices: Async<List<DeviceInfo>> = Uninitialized,
|
||||
val cryptoDevices: Async<List<CryptoDeviceInfo>> = Uninitialized,
|
||||
// val devices: Async<List<DeviceInfo>> = Uninitialized,
|
||||
// val cryptoDevices: Async<List<CryptoDeviceInfo>> = Uninitialized,
|
||||
val devices: Async<List<DeviceFullInfo>> = Uninitialized,
|
||||
// TODO Replace by isLoading boolean
|
||||
val request: Async<Unit> = Uninitialized
|
||||
val request: Async<Unit> = Uninitialized,
|
||||
val hasAccountCrossSigning: Boolean = false,
|
||||
val accountCrossSigningIsTrusted: Boolean = false
|
||||
) : MvRxState
|
||||
|
||||
data class DeviceFullInfo(
|
||||
val deviceInfo: DeviceInfo,
|
||||
val cryptoDeviceInfo: CryptoDeviceInfo?
|
||||
)
|
||||
class DevicesViewModel @AssistedInject constructor(
|
||||
@Assisted initialState: DevicesViewState,
|
||||
private val session: Session,
|
||||
private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider)
|
||||
: VectorViewModel<DevicesViewState, DevicesAction, DevicesViewEvents>(initialState), VerificationService.Listener {
|
||||
private val session: Session
|
||||
) : VectorViewModel<DevicesViewState, DevicesAction, DevicesViewEvents>(initialState), VerificationService.Listener {
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
@ -74,16 +88,76 @@ class DevicesViewModel @AssistedInject constructor(
|
||||
private var _currentDeviceId: String? = null
|
||||
private var _currentSession: String? = null
|
||||
|
||||
init {
|
||||
refreshDevicesList()
|
||||
session.cryptoService().verificationService().addListener(this)
|
||||
private val refreshPublisher: PublishSubject<Unit> = PublishSubject.create()
|
||||
|
||||
session.rx().liveUserCryptoDevices(session.myUserId)
|
||||
.execute {
|
||||
init {
|
||||
|
||||
setState {
|
||||
copy(
|
||||
hasAccountCrossSigning = session.cryptoService().crossSigningService().getMyCrossSigningKeys() != null,
|
||||
accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified(),
|
||||
myDeviceId = session.sessionParams.credentials.deviceId ?: ""
|
||||
)
|
||||
}
|
||||
|
||||
Observable.combineLatest<List<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(
|
||||
cryptoDevices = it
|
||||
devices = async
|
||||
)
|
||||
}
|
||||
|
||||
session.rx().liveCrossSigningInfo(session.myUserId)
|
||||
.execute {
|
||||
copy(
|
||||
hasAccountCrossSigning = it.invoke()?.getOrNull() != null,
|
||||
accountCrossSigningIsTrusted = it.invoke()?.getOrNull()?.isTrusted() == true
|
||||
)
|
||||
}
|
||||
session.cryptoService().verificationService().addListener(this)
|
||||
|
||||
// session.rx().liveMyDeviceInfo()
|
||||
// .execute {
|
||||
// copy(
|
||||
// devices = it
|
||||
// )
|
||||
// }
|
||||
|
||||
session.rx().liveUserCryptoDevices(session.myUserId)
|
||||
.distinct()
|
||||
.throttleLast(5_000, TimeUnit.MILLISECONDS)
|
||||
.subscribe {
|
||||
// If we have a new crypto device change, we might want to trigger refresh of device info
|
||||
session.cryptoService().fetchDevicesList(NoOpMatrixCallback())
|
||||
}.disposeOnClear()
|
||||
|
||||
// session.rx().liveUserCryptoDevices(session.myUserId)
|
||||
// .execute {
|
||||
// copy(
|
||||
// cryptoDevices = it
|
||||
// )
|
||||
// }
|
||||
|
||||
refreshPublisher.throttleFirst(4_000, TimeUnit.MILLISECONDS)
|
||||
.subscribe {
|
||||
session.cryptoService().fetchDevicesList(NoOpMatrixCallback())
|
||||
session.cryptoService().downloadKeys(listOf(session.myUserId), true, NoOpMatrixCallback())
|
||||
}
|
||||
.disposeOnClear()
|
||||
// then force download
|
||||
queryRefreshDevicesList()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
@ -93,7 +167,7 @@ class DevicesViewModel @AssistedInject constructor(
|
||||
|
||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||
if (tx.state == VerificationTxState.Verified) {
|
||||
refreshDevicesList()
|
||||
queryRefreshDevicesList()
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,91 +176,66 @@ class DevicesViewModel @AssistedInject constructor(
|
||||
* The devices list is the list of the devices where the user is logged in.
|
||||
* It can be any mobile devices, and any browsers.
|
||||
*/
|
||||
private fun refreshDevicesList() {
|
||||
if (!session.sessionParams.credentials.deviceId.isNullOrEmpty()) {
|
||||
// display something asap
|
||||
val localKnown = session.cryptoService().getUserDevices(session.myUserId).map {
|
||||
DeviceInfo(
|
||||
user_id = session.myUserId,
|
||||
deviceId = it.deviceId,
|
||||
displayName = it.displayName()
|
||||
)
|
||||
}
|
||||
|
||||
setState {
|
||||
copy(
|
||||
// Keep known list if we have it, and let refresh go in backgroung
|
||||
devices = this.devices.takeIf { it is Success } ?: Success(localKnown)
|
||||
)
|
||||
}
|
||||
|
||||
session.cryptoService().getDevicesList(object : MatrixCallback<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
|
||||
}
|
||||
private fun queryRefreshDevicesList() {
|
||||
refreshPublisher.onNext(Unit)
|
||||
}
|
||||
|
||||
override fun handle(action: DevicesAction) {
|
||||
return when (action) {
|
||||
is DevicesAction.Retry -> refreshDevicesList()
|
||||
is DevicesAction.Delete -> handleDelete(action)
|
||||
is DevicesAction.Password -> handlePassword(action)
|
||||
is DevicesAction.Rename -> handleRename(action)
|
||||
is DevicesAction.PromptRename -> handlePromptRename(action)
|
||||
is DevicesAction.VerifyMyDevice -> handleVerify(action)
|
||||
is DevicesAction.Refresh -> queryRefreshDevicesList()
|
||||
is DevicesAction.Delete -> handleDelete(action)
|
||||
is DevicesAction.Password -> handlePassword(action)
|
||||
is DevicesAction.Rename -> handleRename(action)
|
||||
is DevicesAction.PromptRename -> handlePromptRename(action)
|
||||
is DevicesAction.VerifyMyDevice -> handleInteractiveVerification(action)
|
||||
is DevicesAction.CompleteSecurity -> handleCompleteSecurity()
|
||||
is DevicesAction.MarkAsManuallyVerified -> handleVerifyManually(action)
|
||||
is DevicesAction.VerifyMyDeviceManually -> handleShowDeviceCryptoInfo(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleVerify(action: DevicesAction.VerifyMyDevice) {
|
||||
private fun handleInteractiveVerification(action: DevicesAction.VerifyMyDevice) {
|
||||
val txID = session.cryptoService()
|
||||
.verificationService()
|
||||
.requestKeyVerification(supportedVerificationMethodsProvider.provide(), session.myUserId, listOf(action.deviceId))
|
||||
.beginKeyVerification(VerificationMethod.SAS, session.myUserId, action.deviceId, null)
|
||||
_viewEvents.post(DevicesViewEvents.ShowVerifyDevice(
|
||||
session.myUserId,
|
||||
txID.transactionId
|
||||
txID
|
||||
))
|
||||
}
|
||||
|
||||
private fun handleShowDeviceCryptoInfo(action: DevicesAction.VerifyMyDeviceManually) = withState { state ->
|
||||
state.devices.invoke()
|
||||
?.firstOrNull { it.cryptoDeviceInfo?.deviceId == action.deviceId }
|
||||
?.let {
|
||||
_viewEvents.post(DevicesViewEvents.ShowManuallyVerify(it.cryptoDeviceInfo!!))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleVerifyManually(action: DevicesAction.MarkAsManuallyVerified) = withState { state ->
|
||||
viewModelScope.launch {
|
||||
if (state.hasAccountCrossSigning) {
|
||||
awaitCallback<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 ->
|
||||
val info = state.devices.invoke()?.firstOrNull { it.deviceId == action.deviceId }
|
||||
val info = state.devices.invoke()?.firstOrNull { it.deviceInfo.deviceId == action.deviceId }
|
||||
if (info != null) {
|
||||
_viewEvents.post(DevicesViewEvents.PromptRenameDevice(info))
|
||||
_viewEvents.post(DevicesViewEvents.PromptRenameDevice(info.deviceInfo))
|
||||
}
|
||||
}
|
||||
|
||||
@ -199,7 +248,7 @@ class DevicesViewModel @AssistedInject constructor(
|
||||
)
|
||||
}
|
||||
// force settings update
|
||||
refreshDevicesList()
|
||||
queryRefreshDevicesList()
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
@ -270,7 +319,7 @@ class DevicesViewModel @AssistedInject constructor(
|
||||
)
|
||||
}
|
||||
// force settings update
|
||||
refreshDevicesList()
|
||||
queryRefreshDevicesList()
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -299,7 +348,7 @@ class DevicesViewModel @AssistedInject constructor(
|
||||
)
|
||||
}
|
||||
// force settings update
|
||||
refreshDevicesList()
|
||||
queryRefreshDevicesList()
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -27,6 +27,7 @@ import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.dialogs.ManuallyVerifyDialog
|
||||
import im.vector.riotx.core.dialogs.PromptPasswordDialog
|
||||
import im.vector.riotx.core.extensions.cleanup
|
||||
import im.vector.riotx.core.extensions.configureWith
|
||||
@ -73,6 +74,15 @@ class VectorSettingsDevicesFragment @Inject constructor(
|
||||
transactionId = it.transactionId
|
||||
).show(childFragmentManager, "REQPOP")
|
||||
}
|
||||
is DevicesViewEvents.SelfVerification -> {
|
||||
VerificationBottomSheet.forSelfVerification(it.session)
|
||||
.show(childFragmentManager, "REQPOP")
|
||||
}
|
||||
is DevicesViewEvents.ShowManuallyVerify -> {
|
||||
ManuallyVerifyDialog.show(requireActivity(), it.cryptoDeviceInfo) {
|
||||
viewModel.handle(DevicesAction.MarkAsManuallyVerified(it.cryptoDeviceInfo))
|
||||
}
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
@ -92,8 +102,8 @@ class VectorSettingsDevicesFragment @Inject constructor(
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_active_sessions_manage)
|
||||
viewModel.handle(DevicesAction.Refresh)
|
||||
}
|
||||
|
||||
override fun onDeviceClicked(deviceInfo: DeviceInfo) {
|
||||
@ -112,7 +122,7 @@ class VectorSettingsDevicesFragment @Inject constructor(
|
||||
// }
|
||||
|
||||
override fun retry() {
|
||||
viewModel.handle(DevicesAction.Retry)
|
||||
viewModel.handle(DevicesAction.Refresh)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -26,6 +26,12 @@ import javax.inject.Inject
|
||||
*/
|
||||
class SharedPreferencesUiStateRepository @Inject constructor(private val sharedPreferences: SharedPreferences) : UiStateRepository {
|
||||
|
||||
override fun reset() {
|
||||
sharedPreferences.edit {
|
||||
remove(KEY_DISPLAY_MODE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDisplayMode(): RoomListDisplayMode {
|
||||
return when (sharedPreferences.getInt(KEY_DISPLAY_MODE, VALUE_DISPLAY_MODE_CATCHUP)) {
|
||||
VALUE_DISPLAY_MODE_PEOPLE -> RoomListDisplayMode.PEOPLE
|
||||
|
@ -23,6 +23,11 @@ import im.vector.riotx.features.home.RoomListDisplayMode
|
||||
*/
|
||||
interface UiStateRepository {
|
||||
|
||||
/**
|
||||
* Reset all the saved data
|
||||
*/
|
||||
fun reset()
|
||||
|
||||
fun getDisplayMode(): RoomListDisplayMode
|
||||
|
||||
fun storeDisplayMode(displayMode: RoomListDisplayMode)
|
||||
|
@ -33,7 +33,7 @@
|
||||
<LinearLayout
|
||||
android:id="@+id/alerter_texts"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
@ -68,6 +68,7 @@
|
||||
android:textAppearance="@style/AlertTextAppearance.Text"
|
||||
android:visibility="gone"
|
||||
tools:text="Text"
|
||||
android:maxLines="3"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
|
@ -1,5 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<androidx.core.widget.NestedScrollView 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"
|
||||
@ -12,10 +11,9 @@
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:layout_height="wrap_content">
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/bootstrapIcon"
|
||||
@ -28,23 +26,19 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bootstrapTitleText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/bootstrapIcon"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/bootstrapIcon"
|
||||
app:layout_constraintTop_toTopOf="@+id/bootstrapIcon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="@string/recovery_passphrase" />
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
|
@ -44,13 +44,11 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/verificationRequestAvatar"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/verificationRequestAvatar"
|
||||
app:layout_constraintTop_toTopOf="@+id/verificationRequestAvatar"
|
||||
|
@ -12,11 +12,11 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
tools:text="@string/enter_account_password"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="14sp"
|
||||
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
|
||||
android:id="@+id/bootstrapAccountPasswordTil"
|
||||
@ -59,9 +59,7 @@
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/bootstrapPasswordButton"
|
||||
style="@style/VectorButtonStyleText"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginTop="@dimen/layout_vertical_margin"
|
||||
android:padding="8dp"
|
||||
android:text="@string/_continue"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
@ -1,5 +1,4 @@
|
||||
<?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"
|
||||
@ -25,7 +24,6 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:errorEnabled="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/ssss_view_show_password"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/bootstrapDescriptionText">
|
||||
@ -34,11 +32,11 @@
|
||||
android:id="@+id/ssss_passphrase_enter_edittext"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:hint="@string/passphrase_enter_passphrase"
|
||||
android:imeOptions="actionDone"
|
||||
android:maxLines="3"
|
||||
android:singleLine="false"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
tools:hint="@string/passphrase_enter_passphrase"
|
||||
tools:inputType="textPassword" />
|
||||
|
||||
<!-- 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
|
||||
android:id="@+id/ssss_passphrase_security_progress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:layout_height="4dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_height="4dp" />
|
||||
android:layout_marginBottom="2dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bootstrapWarningInfo"
|
||||
@ -56,12 +54,12 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:textSize="12sp"
|
||||
android:gravity="center_vertical"
|
||||
android:drawableStart="@drawable/ic_alert_triangle"
|
||||
android:drawableTint="@color/riotx_destructive_accent"
|
||||
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>
|
||||
|
||||
@ -78,6 +76,13 @@
|
||||
app:layout_constraintStart_toEndOf="@+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-->
|
||||
<!-- android:id="@+id/bootstrapWarningInfo"-->
|
||||
|
@ -79,7 +79,6 @@
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/bootstrapMigrateContinueButton"
|
||||
style="@style/VectorButtonStyleText"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginTop="@dimen/layout_vertical_margin"
|
||||
android:text="@string/_continue"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
|
20
vector/src/main/res/layout/item_generic_button.xml
Normal file
20
vector/src/main/res/layout/item_generic_button.xml
Normal 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>
|
@ -58,7 +58,7 @@
|
||||
android:id="@+id/messageContentMergedCreationStub"
|
||||
style="@style/TimelineContentStubBaseParams"
|
||||
android:layout="@layout/item_timeline_event_merged_room_creation_stub"
|
||||
tools:layout_marginTop="160dp"
|
||||
tools:layout_marginTop="240dp"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</FrameLayout>
|
||||
|
@ -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_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="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_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 & 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í & 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 & 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 & 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 & 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 & 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 & %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 & 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 & 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 & 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 & 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>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user