Merge branch 'develop' into feature/invite_members_to_room

This commit is contained in:
Onuray Sahin 2020-04-30 16:11:52 +03:00 committed by GitHub
commit d2f0957eba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
123 changed files with 3697 additions and 1031 deletions

View File

@ -29,6 +29,9 @@ Improvements 🙌:
- Cross-Signing | Hide Use recovery key when 4S is not setup (#1007) - Cross-Signing | Hide Use recovery key when 4S is not setup (#1007)
- Cross-Signing | Trust account xSigning keys by entering Recovery Key (select file or copy) #1199 - Cross-Signing | Trust account xSigning keys by entering Recovery Key (select file or copy) #1199
- Invite member(s) to an existing room #1276 - Invite member(s) to an existing room #1276
- E2E timeline decoration (#1279)
- Manage Session Settings / Cross Signing update (#1295)
- Cross-Signing | Review sessions toast update old vs new (#1293, #1306)
Bugfix 🐛: Bugfix 🐛:
- Fix summary notification staying after "mark as read" - Fix summary notification staying after "mark as read"
@ -44,6 +47,9 @@ Bugfix 🐛:
- RiotX now uses as many threads as it needs to do work and send messages (#1221) - RiotX now uses as many threads as it needs to do work and send messages (#1221)
- Fix issue with media path (#1227) - Fix issue with media path (#1227)
- Add user to direct chat by user id (#1065) - Add user to direct chat by user id (#1065)
- Use correct URL for SSO connection (#1178)
- Emoji completion :tada: does not completes to 🎉 like on web (#1285)
- Fix bad Shield Logic for DM (#963)
Translations 🗣: Translations 🗣:
- -

View File

@ -13,6 +13,24 @@ Dedicated room for RiotX: [![RiotX Android Matrix room #riot-android:matrix.org]
Please set the "hard wrap" setting of Android Studio to 160 chars, this is the setting we use internally to format the source code (Menu `Settings/Editor/Code Style` then `Hard wrap at`). Please set the "hard wrap" setting of Android Studio to 160 chars, this is the setting we use internally to format the source code (Menu `Settings/Editor/Code Style` then `Hard wrap at`).
Please ensure that your using the project formatting rules (which are in the project at .idea/codeStyles/), and format the file before committing them. Please ensure that your using the project formatting rules (which are in the project at .idea/codeStyles/), and format the file before committing them.
### Template
An Android Studio template has been added to the project to help creating all files needed when adding a new screen to the application. Fragment, ViewModel, Activity, etc.
To install the template (to be done only once):
- Go to folder `./tools/template`.
- Run the script `./configure.sh`.
- Restart Android Studio.
To create a new screen:
- First create a new package in your code.
- Then right click on the package, and select `New/New Vector/RiotX Feature`.
- Follow the Wizard, especially replace `Main` by something more relevant to your feature.
- Click on `Finish`.
- Remainning steps are described as TODO in the generated files, or will be pointed out by the compilator, or at runtime :)
Note that if the templates are modified, the only things to do is to restart Android Studio for the change to take effect.
## Compilation ## Compilation
For now, the Matrix SDK and the RiotX application are in the same project. So there is no specific thing to do, this project should compile without any special action. For now, the Matrix SDK and the RiotX application are in the same project. So there is no specific thing to do, this project should compile without any special action.

View File

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

View File

@ -20,6 +20,8 @@ import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStore import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStore
import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreModule import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreModule
import im.vector.matrix.android.internal.crypto.store.db.mapper.CrossSigningKeysMapper
import im.vector.matrix.android.internal.di.MoshiProvider
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import kotlin.random.Random import kotlin.random.Random
@ -31,6 +33,7 @@ internal class CryptoStoreHelper {
.name("test.realm") .name("test.realm")
.modules(RealmCryptoStoreModule()) .modules(RealmCryptoStoreModule())
.build(), .build(),
crossSigningKeysMapper = CrossSigningKeysMapper(MoshiProvider.providesMoshi()),
credentials = createCredential()) credentials = createCredential())
} }

View File

@ -0,0 +1,37 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.auth
/**
* Path to use when the client does not supported any or all login flows
* Ref: https://matrix.org/docs/spec/client_server/latest#login-fallback
* */
const val LOGIN_FALLBACK_PATH = "/_matrix/static/client/login/"
/**
* Path to use when the client does not supported any or all registration flows
* Not documented
*/
const val REGISTER_FALLBACK_PATH = "/_matrix/static/client/register/"
/**
* Path to use when the client want to connect using SSO
* Ref: https://matrix.org/docs/spec/client_server/latest#sso-client-login
*/
const val SSO_FALLBACK_PATH = "/_matrix/client/r0/login/sso/redirect"
const val SSO_REDIRECT_URL_PARAM = "redirectUrl"

View File

@ -16,6 +16,10 @@
package im.vector.matrix.android.api.failure package im.vector.matrix.android.api.failure
import im.vector.matrix.android.api.extensions.tryThis
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
import im.vector.matrix.android.internal.di.MoshiProvider
import java.io.IOException
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
fun Throwable.is401() = fun Throwable.is401() =
@ -29,6 +33,7 @@ fun Throwable.isTokenError() =
fun Throwable.shouldBeRetried(): Boolean { fun Throwable.shouldBeRetried(): Boolean {
return this is Failure.NetworkConnection return this is Failure.NetworkConnection
|| this is IOException
|| (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED) || (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED)
} }
@ -37,3 +42,18 @@ fun Throwable.isInvalidPassword(): Boolean {
&& error.code == MatrixError.M_FORBIDDEN && error.code == MatrixError.M_FORBIDDEN
&& error.message == "Invalid password" && error.message == "Invalid password"
} }
/**
* Try to convert to a RegistrationFlowResponse. Return null in the cases it's not possible
*/
fun Throwable.toRegistrationFlowResponse(): RegistrationFlowResponse? {
return if (this is Failure.OtherServerError && this.httpCode == 401) {
tryThis {
MoshiProvider.providesMoshi()
.adapter(RegistrationFlowResponse::class.java)
.fromJson(this.errorBody)
}
} else {
null
}
}

View File

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

View File

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

View File

@ -46,10 +46,10 @@ data class RoomSummary constructor(
val readMarkerId: String? = null, val readMarkerId: String? = null,
val userDrafts: List<UserDraft> = emptyList(), val userDrafts: List<UserDraft> = emptyList(),
val isEncrypted: Boolean, val isEncrypted: Boolean,
val encryptionEventTs: Long?,
val inviterId: String? = null, val inviterId: String? = null,
val typingRoomMemberIds: List<String> = emptyList(), val typingRoomMemberIds: List<String> = emptyList(),
val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS, val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS,
// TODO Plug it
val roomEncryptionTrustLevel: RoomEncryptionTrustLevel? = null val roomEncryptionTrustLevel: RoomEncryptionTrustLevel? = null
) { ) {

View File

@ -18,8 +18,8 @@ package im.vector.matrix.android.internal.auth.registration
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.toRegistrationFlowResponse
import im.vector.matrix.android.internal.auth.AuthAPI import im.vector.matrix.android.internal.auth.AuthAPI
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
@ -39,25 +39,9 @@ internal class DefaultRegisterTask(
apiCall = authAPI.register(params.registrationParams) apiCall = authAPI.register(params.registrationParams)
} }
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
if (throwable is Failure.OtherServerError && throwable.httpCode == 401) { throw throwable.toRegistrationFlowResponse()
// Parse to get a RegistrationFlowResponse ?.let { Failure.RegistrationFlowError(it) }
val registrationFlowResponse = try { ?: throwable
MoshiProvider.providesMoshi()
.adapter(RegistrationFlowResponse::class.java)
.fromJson(throwable.errorBody)
} catch (e: Exception) {
null
}
// check if the server response can be cast
if (registrationFlowResponse != null) {
throw Failure.RegistrationFlowError(registrationFlowResponse)
} else {
throw throwable
}
} else {
// Other error
throw throwable
}
} }
} }
} }

View File

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

View File

@ -251,15 +251,33 @@ internal class DefaultCryptoService @Inject constructor(
return myDeviceInfoHolder.get().myDevice return myDeviceInfoHolder.get().myDevice
} }
override fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) { override fun fetchDevicesList(callback: MatrixCallback<DevicesListResponse>) {
getDevicesTask getDevicesTask
.configureWith { .configureWith {
// this.executionThread = TaskThread.CRYPTO // this.executionThread = TaskThread.CRYPTO
this.callback = callback this.callback = object : MatrixCallback<DevicesListResponse> {
override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
override fun onSuccess(data: DevicesListResponse) {
// Save in local DB
cryptoStore.saveMyDevicesInfo(data.devices ?: emptyList())
callback.onSuccess(data)
}
}
} }
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun getLiveMyDevicesInfo(): LiveData<List<DeviceInfo>> {
return cryptoStore.getLiveMyDevicesInfo()
}
override fun getMyDevicesInfo(): List<DeviceInfo> {
return cryptoStore.getMyDevicesInfo()
}
override fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>) { override fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>) {
getDeviceInfoTask getDeviceInfoTask
.configureWith(GetDeviceInfoTask.Params(deviceId)) { .configureWith(GetDeviceInfoTask.Params(deviceId)) {
@ -318,6 +336,8 @@ internal class DefaultCryptoService @Inject constructor(
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
internalStart(isInitialSync) internalStart(isInitialSync)
} }
// Just update
fetchDevicesList(NoOpMatrixCallback())
} }
private suspend fun internalStart(isInitialSync: Boolean) { private suspend fun internalStart(isInitialSync: Boolean) {

View File

@ -19,6 +19,7 @@ import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
import im.vector.matrix.android.api.extensions.orFalse import im.vector.matrix.android.api.extensions.orFalse
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -26,17 +27,28 @@ import javax.inject.Inject
internal interface ComputeTrustTask : Task<ComputeTrustTask.Params, RoomEncryptionTrustLevel> { internal interface ComputeTrustTask : Task<ComputeTrustTask.Params, RoomEncryptionTrustLevel> {
data class Params( data class Params(
val userIds: List<String> val activeMemberUserIds: List<String>,
val isDirectRoom: Boolean
) )
} }
internal class DefaultComputeTrustTask @Inject constructor( internal class DefaultComputeTrustTask @Inject constructor(
private val cryptoStore: IMXCryptoStore, private val cryptoStore: IMXCryptoStore,
@UserId private val userId: String,
private val coroutineDispatchers: MatrixCoroutineDispatchers private val coroutineDispatchers: MatrixCoroutineDispatchers
) : ComputeTrustTask { ) : ComputeTrustTask {
override suspend fun execute(params: ComputeTrustTask.Params): RoomEncryptionTrustLevel = withContext(coroutineDispatchers.crypto) { override suspend fun execute(params: ComputeTrustTask.Params): RoomEncryptionTrustLevel = withContext(coroutineDispatchers.crypto) {
val allTrustedUserIds = params.userIds // The set of “all users” depends on the type of room:
// For regular / topic rooms, all users including yourself, are considered when decorating a room
// For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room
val listToCheck = if (params.isDirectRoom) {
params.activeMemberUserIds.filter { it != userId }
} else {
params.activeMemberUserIds
}
val allTrustedUserIds = listToCheck
.filter { userId -> getUserCrossSigningKeys(userId)?.isTrusted() == true } .filter { userId -> getUserCrossSigningKeys(userId)?.isTrusted() == true }
if (allTrustedUserIds.isEmpty()) { if (allTrustedUserIds.isEmpty()) {
@ -60,7 +72,7 @@ internal class DefaultComputeTrustTask @Inject constructor(
if (hasWarning) { if (hasWarning) {
RoomEncryptionTrustLevel.Warning RoomEncryptionTrustLevel.Warning
} else { } else {
if (params.userIds.size == allTrustedUserIds.size) { if (listToCheck.size == allTrustedUserIds.size) {
// all users are trusted and all devices are verified // all users are trusted and all devices are verified
RoomEncryptionTrustLevel.Trusted RoomEncryptionTrustLevel.Trusted
} else { } else {

View File

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

View File

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

View File

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

View File

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

View File

@ -61,20 +61,4 @@ internal object CryptoInfoMapper {
signatures = keyInfo.signatures signatures = keyInfo.signatures
) )
} }
fun RestDeviceInfo.toCryptoModel(): CryptoDeviceInfo {
return map(this)
}
fun CryptoDeviceInfo.toRest(): RestDeviceInfo {
return map(this)
}
// fun RestKeyInfo.toCryptoModel(): CryptoCrossSigningKey {
// return map(this)
// }
fun CryptoCrossSigningKey.toRest(): RestKeyInfo {
return map(this)
}
} }

View File

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

View File

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

View File

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

View File

@ -18,14 +18,17 @@ package im.vector.matrix.android.internal.crypto.store.db
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.Types import com.squareup.moshi.Types
import im.vector.matrix.android.api.extensions.tryThis
import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.store.db.mapper.CrossSigningKeysMapper
import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMetadataEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMetadataEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.GossipingEventEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.GossipingEventEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields
@ -33,11 +36,14 @@ import im.vector.matrix.android.internal.di.SerializeNulls
import io.realm.DynamicRealm import io.realm.DynamicRealm
import io.realm.RealmMigration import io.realm.RealmMigration
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject
internal object RealmCryptoStoreMigration : RealmMigration { internal class RealmCryptoStoreMigration @Inject constructor(private val crossSigningKeysMapper: CrossSigningKeysMapper) : RealmMigration {
// Version 1L added Cross Signing info persistence // Version 1L added Cross Signing info persistence
const val CRYPTO_STORE_SCHEMA_VERSION = 3L companion object {
const val CRYPTO_STORE_SCHEMA_VERSION = 5L
}
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.v("Migrating Realm Crypto from $oldVersion to $newVersion") Timber.v("Migrating Realm Crypto from $oldVersion to $newVersion")
@ -45,6 +51,8 @@ internal object RealmCryptoStoreMigration : RealmMigration {
if (oldVersion <= 0) migrateTo1(realm) if (oldVersion <= 0) migrateTo1(realm)
if (oldVersion <= 1) migrateTo2(realm) if (oldVersion <= 1) migrateTo2(realm)
if (oldVersion <= 2) migrateTo3(realm) if (oldVersion <= 2) migrateTo3(realm)
if (oldVersion <= 3) migrateTo4(realm)
if (oldVersion <= 4) migrateTo5(realm)
} }
private fun migrateTo1(realm: DynamicRealm) { private fun migrateTo1(realm: DynamicRealm) {
@ -193,4 +201,38 @@ internal object RealmCryptoStoreMigration : RealmMigration {
?.addField(CryptoMetadataEntityFields.KEY_BACKUP_RECOVERY_KEY, String::class.java) ?.addField(CryptoMetadataEntityFields.KEY_BACKUP_RECOVERY_KEY, String::class.java)
?.addField(CryptoMetadataEntityFields.KEY_BACKUP_RECOVERY_KEY_VERSION, String::class.java) ?.addField(CryptoMetadataEntityFields.KEY_BACKUP_RECOVERY_KEY_VERSION, String::class.java)
} }
private fun migrateTo4(realm: DynamicRealm) {
Timber.d("Updating KeyInfoEntity table")
val keyInfoEntities = realm.where("KeyInfoEntity").findAll()
try {
keyInfoEntities.forEach {
val stringSignatures = it.getString(KeyInfoEntityFields.SIGNATURES)
val objectSignatures: Map<String, Map<String, String>>? = deserializeFromRealm(stringSignatures)
val jsonSignatures = crossSigningKeysMapper.serializeSignatures(objectSignatures)
it.setString(KeyInfoEntityFields.SIGNATURES, jsonSignatures)
}
} catch (failure: Throwable) {
}
}
private fun migrateTo5(realm: DynamicRealm) {
realm.schema.create("MyDeviceLastSeenInfoEntity")
.addField(MyDeviceLastSeenInfoEntityFields.DEVICE_ID, String::class.java)
.addPrimaryKey(MyDeviceLastSeenInfoEntityFields.DEVICE_ID)
.addField(MyDeviceLastSeenInfoEntityFields.DISPLAY_NAME, String::class.java)
.addField(MyDeviceLastSeenInfoEntityFields.LAST_SEEN_IP, String::class.java)
.addField(MyDeviceLastSeenInfoEntityFields.LAST_SEEN_TS, Long::class.java)
.setNullable(MyDeviceLastSeenInfoEntityFields.LAST_SEEN_TS, true)
val now = System.currentTimeMillis()
realm.schema.get("DeviceInfoEntity")
?.addField(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, Long::class.java)
?.setNullable(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, true)
?.transform { deviceInfoEntity ->
tryThis {
deviceInfoEntity.setLong(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, now)
}
}
}
} }

View File

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

View File

@ -0,0 +1,85 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.store.db.mapper
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity
import io.realm.RealmList
import timber.log.Timber
import javax.inject.Inject
internal class CrossSigningKeysMapper @Inject constructor(moshi: Moshi) {
private val signaturesAdapter = moshi.adapter<Map<String, Map<String, String>>>(Types.newParameterizedType(
Map::class.java,
String::class.java,
Any::class.java
))
fun update(keyInfo: KeyInfoEntity, cryptoCrossSigningKey: CryptoCrossSigningKey) {
// update signatures?
keyInfo.signatures = serializeSignatures(cryptoCrossSigningKey.signatures)
keyInfo.usages = cryptoCrossSigningKey.usages?.toTypedArray()?.let { RealmList(*it) }
?: RealmList()
}
fun map(userId: String?, keyInfo: KeyInfoEntity?): CryptoCrossSigningKey? {
val pubKey = keyInfo?.publicKeyBase64 ?: return null
return CryptoCrossSigningKey(
userId = userId ?: "",
keys = mapOf("ed25519:$pubKey" to pubKey),
usages = keyInfo.usages.map { it },
signatures = deserializeSignatures(keyInfo.signatures),
trustLevel = keyInfo.trustLevelEntity?.let {
DeviceTrustLevel(
crossSigningVerified = it.crossSignedVerified ?: false,
locallyVerified = it.locallyVerified ?: false
)
}
)
}
fun map(keyInfo: CryptoCrossSigningKey): KeyInfoEntity {
return KeyInfoEntity().apply {
publicKeyBase64 = keyInfo.unpaddedBase64PublicKey
usages = keyInfo.usages?.let { RealmList(*it.toTypedArray()) } ?: RealmList()
signatures = serializeSignatures(keyInfo.signatures)
// TODO how to handle better, check if same keys?
// reset trust
trustLevelEntity = null
}
}
fun serializeSignatures(signatures: Map<String, Map<String, String>>?): String {
return signaturesAdapter.toJson(signatures)
}
fun deserializeSignatures(signatures: String?): Map<String, Map<String, String>>? {
if (signatures == null) {
return null
}
return try {
signaturesAdapter.fromJson(signatures)
} catch (failure: Throwable) {
Timber.e(failure)
null
}
}
}

View File

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

View File

@ -34,7 +34,12 @@ internal open class DeviceInfoEntity(@PrimaryKey var primaryKey: String = "",
var keysMapJson: String? = null, var keysMapJson: String? = null,
var signatureMapJson: String? = null, var signatureMapJson: String? = null,
var unsignedMapJson: String? = null, var unsignedMapJson: String? = null,
var trustLevelEntity: TrustLevelEntity? = null var trustLevelEntity: TrustLevelEntity? = null,
/**
* We use that to make distinction between old devices (there before mine)
* and new ones. Used for example to detect new unverified login
*/
var firstTimeSeenLocalTs: Long? = null
) : RealmObject() { ) : RealmObject() {
// // Deserialize data // // Deserialize data

View File

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

View File

@ -0,0 +1,34 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.store.db.model
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
internal open class MyDeviceLastSeenInfoEntity(
/**The device id*/
@PrimaryKey var deviceId: String? = null,
/** The device display name*/
var displayName: String? = null,
/** The last time this device has been seen. */
var lastSeenTs: Long? = null,
/** The last ip address*/
var lastSeenIp: String? = null
) : RealmObject() {
companion object
}

View File

@ -16,6 +16,7 @@
package im.vector.matrix.android.internal.crypto.store.db.model package im.vector.matrix.android.internal.crypto.store.db.model
import im.vector.matrix.android.api.extensions.tryThis
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm
import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm
@ -36,7 +37,7 @@ internal open class OlmInboundGroupSessionEntity(
: RealmObject() { : RealmObject() {
fun getInboundGroupSession(): OlmInboundGroupSessionWrapper? { fun getInboundGroupSession(): OlmInboundGroupSessionWrapper? {
return deserializeFromRealm(olmInboundGroupSessionData) return tryThis { deserializeFromRealm<OlmInboundGroupSessionWrapper?>(olmInboundGroupSessionData) }
} }
fun putInboundGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper?) { fun putInboundGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper?) {

View File

@ -17,10 +17,9 @@
package im.vector.matrix.android.internal.crypto.tasks package im.vector.matrix.android.internal.crypto.tasks
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse import im.vector.matrix.android.api.failure.toRegistrationFlowResponse
import im.vector.matrix.android.internal.crypto.api.CryptoApi import im.vector.matrix.android.internal.crypto.api.CryptoApi
import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceParams import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceParams
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
@ -43,25 +42,9 @@ internal class DefaultDeleteDeviceTask @Inject constructor(
apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams()) apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams())
} }
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
if (throwable is Failure.OtherServerError && throwable.httpCode == 401) { throw throwable.toRegistrationFlowResponse()
// Parse to get a RegistrationFlowResponse ?.let { Failure.RegistrationFlowError(it) }
val registrationFlowResponse = try { ?: throwable
MoshiProvider.providesMoshi()
.adapter(RegistrationFlowResponse::class.java)
.fromJson(throwable.errorBody)
} catch (e: Exception) {
null
}
// check if the server response can be casted
if (registrationFlowResponse != null) {
throw Failure.RegistrationFlowError(registrationFlowResponse)
} else {
throw throwable
}
} else {
// Other error
throw throwable
}
} }
} }
} }

View File

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

View File

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

View File

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

View File

@ -117,6 +117,7 @@ internal class VerificationTransportToDevice(
onDone: (() -> Unit)?) { onDone: (() -> Unit)?) {
Timber.d("## SAS sending msg type $type") Timber.d("## SAS sending msg type $type")
Timber.v("## SAS sending msg info $verificationInfo") Timber.v("## SAS sending msg info $verificationInfo")
val stateBeforeCall = tx?.state
val tx = tx ?: return val tx = tx ?: return
val contentMap = MXUsersDevicesMap<Any>() val contentMap = MXUsersDevicesMap<Any>()
val toSendToDeviceObject = verificationInfo.toSendToDeviceObject() val toSendToDeviceObject = verificationInfo.toSendToDeviceObject()
@ -132,9 +133,13 @@ internal class VerificationTransportToDevice(
if (onDone != null) { if (onDone != null) {
onDone() onDone()
} else { } else {
// we may have received next state (e.g received accept in sending_start)
// We only put next state if the state was what is was before we started
if (tx.state == stateBeforeCall) {
tx.state = nextState tx.state = nextState
} }
} }
}
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
Timber.e("## SAS verification [$tx.transactionId] failed to send toDevice in state : $tx.state") Timber.e("## SAS verification [$tx.transactionId] failed to send toDevice in state : $tx.state")

View File

@ -53,6 +53,7 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
canonicalAlias = roomSummaryEntity.canonicalAlias, canonicalAlias = roomSummaryEntity.canonicalAlias,
aliases = roomSummaryEntity.aliases.toList(), aliases = roomSummaryEntity.aliases.toList(),
isEncrypted = roomSummaryEntity.isEncrypted, isEncrypted = roomSummaryEntity.isEncrypted,
encryptionEventTs = roomSummaryEntity.encryptionEventTs,
typingRoomMemberIds = roomSummaryEntity.typingUserIds.toList(), typingRoomMemberIds = roomSummaryEntity.typingUserIds.toList(),
breadcrumbsIndex = roomSummaryEntity.breadcrumbsIndex, breadcrumbsIndex = roomSummaryEntity.breadcrumbsIndex,
roomEncryptionTrustLevel = roomSummaryEntity.roomEncryptionTrustLevel, roomEncryptionTrustLevel = roomSummaryEntity.roomEncryptionTrustLevel,

View File

@ -48,6 +48,7 @@ internal open class RoomSummaryEntity(
// this is required for querying // this is required for querying
var flatAliases: String = "", var flatAliases: String = "",
var isEncrypted: Boolean = false, var isEncrypted: Boolean = false,
var encryptionEventTs: Long? = 0,
var typingUserIds: RealmList<String> = RealmList(), var typingUserIds: RealmList<String> = RealmList(),
var roomEncryptionTrustLevelStr: String? = null, var roomEncryptionTrustLevelStr: String? = null,
var inviterId: String? = null var inviterId: String? = null

View File

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

View File

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

View File

@ -136,6 +136,7 @@ internal class RoomSummaryUpdater @Inject constructor(
roomSummaryEntity.aliases.addAll(roomAliases) roomSummaryEntity.aliases.addAll(roomAliases)
roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|") roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|")
roomSummaryEntity.isEncrypted = encryptionEvent != null roomSummaryEntity.isEncrypted = encryptionEvent != null
roomSummaryEntity.encryptionEventTs = encryptionEvent?.originServerTs
roomSummaryEntity.typingUserIds.clear() roomSummaryEntity.typingUserIds.clear()
roomSummaryEntity.typingUserIds.addAll(ephemeralResult?.typingUserIds.orEmpty()) roomSummaryEntity.typingUserIds.addAll(ephemeralResult?.typingUserIds.orEmpty())
@ -152,7 +153,7 @@ internal class RoomSummaryUpdater @Inject constructor(
if (updateMembers) { if (updateMembers) {
val otherRoomMembers = RoomMemberHelper(realm, roomId) val otherRoomMembers = RoomMemberHelper(realm, roomId)
.queryRoomMembersEvent() .queryActiveRoomMembersEvent()
.notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId) .notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId)
.findAll() .findAll()
.asSequence() .asSequence()
@ -161,15 +162,7 @@ internal class RoomSummaryUpdater @Inject constructor(
roomSummaryEntity.otherMemberIds.clear() roomSummaryEntity.otherMemberIds.clear()
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers) roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers)
if (roomSummaryEntity.isEncrypted) { if (roomSummaryEntity.isEncrypted) {
// The set of “all users” depends on the type of room: eventBus.post(SessionToCryptoRoomMembersUpdate(roomId, roomSummaryEntity.isDirect, roomSummaryEntity.otherMemberIds.toList() + userId))
// For regular / topic rooms, all users including yourself, are considered when decorating a room
// For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room
val listToCheck = if (roomSummaryEntity.isDirect) {
roomSummaryEntity.otherMemberIds.toList()
} else {
roomSummaryEntity.otherMemberIds.toList() + userId
}
eventBus.post(SessionToCryptoRoomMembersUpdate(roomId, listToCheck))
} }
} }
} }

View File

@ -53,8 +53,6 @@ internal class DefaultSyncTask @Inject constructor(
private suspend fun doSync(params: SyncTask.Params) { private suspend fun doSync(params: SyncTask.Params) {
Timber.v("Sync task started on Thread: ${Thread.currentThread().name}") Timber.v("Sync task started on Thread: ${Thread.currentThread().name}")
// Maybe refresh the home server capabilities data we know
getHomeServerCapabilitiesTask.execute(Unit)
val requestParams = HashMap<String, String>() val requestParams = HashMap<String, String>()
var timeout = 0L var timeout = 0L
@ -73,6 +71,9 @@ internal class DefaultSyncTask @Inject constructor(
initialSyncProgressService.endAll() initialSyncProgressService.endAll()
initialSyncProgressService.startTask(R.string.initial_sync_start_importing_account, 100) initialSyncProgressService.startTask(R.string.initial_sync_start_importing_account, 100)
} }
// Maybe refresh the home server capabilities data we know
getHomeServerCapabilitiesTask.execute(Unit)
val syncResponse = executeRequest<SyncResponse>(eventBus) { val syncResponse = executeRequest<SyncResponse>(eventBus) {
apiCall = syncAPI.sync(requestParams) apiCall = syncAPI.sync(requestParams)
} }

View File

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

View File

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

View File

@ -1,4 +1,32 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<!--
PLEASE DO NOT ADD NEW STRINGS HERE, THE FILE WILL BE DELETED, ONCE ALL PR WILL BE MERGED
-->
</resources> </resources>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,49 @@
package ${escapeKotlinIdentifiers(packageName)}
import android.content.Context
import android.content.Intent
import androidx.appcompat.widget.Toolbar
import im.vector.riotx.R
import im.vector.riotx.core.extensions.addFragment
import im.vector.riotx.core.platform.ToolbarConfigurable
import im.vector.riotx.core.platform.VectorBaseActivity
//TODO: add this activity to manifest
class ${activityClass} : VectorBaseActivity(), ToolbarConfigurable {
companion object {
<#if createFragmentArgs>
private const val EXTRA_FRAGMENT_ARGS = "EXTRA_FRAGMENT_ARGS"
fun newIntent(context: Context, args: ${fragmentArgsClass}): Intent {
return Intent(context, ${activityClass}::class.java).apply {
putExtra(EXTRA_FRAGMENT_ARGS, args)
}
}
<#else>
fun newIntent(context: Context): Intent {
return Intent(context, ${activityClass}::class.java)
}
</#if>
}
override fun getLayoutRes() = R.layout.activity_simple
override fun initUiAndData() {
if (isFirstCreation()) {
<#if createFragmentArgs>
val fragmentArgs: ${fragmentArgsClass} = intent?.extras?.getParcelable(EXTRA_FRAGMENT_ARGS)
?: return
addFragment(R.id.simpleFragmentContainer, ${fragmentClass}::class.java, fragmentArgs)
<#else>
addFragment(R.id.simpleFragmentContainer, ${fragmentClass}::class.java)
</#if>
}
}
override fun configure(toolbar: Toolbar) {
configureToolbar(toolbar)
}
}

View File

@ -0,0 +1,47 @@
package ${escapeKotlinIdentifiers(packageName)}
import android.os.Bundle
<#if createFragmentArgs>
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
import com.airbnb.mvrx.args
</#if>
import android.view.View
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseFragment
import javax.inject.Inject
<#if createFragmentArgs>
@Parcelize
data class ${fragmentArgsClass}() : Parcelable
</#if>
//TODO: add this fragment into FragmentModule
class ${fragmentClass} @Inject constructor(
private val viewModelFactory: ${viewModelClass}.Factory
) : VectorBaseFragment(), ${viewModelClass}.Factory by viewModelFactory {
<#if createFragmentArgs>
private val fragmentArgs: ${fragmentArgsClass} by args()
</#if>
private val viewModel: ${viewModelClass} by fragmentViewModel()
override fun getLayoutResId() = R.layout.${fragmentLayout}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Initialize your view, subscribe to viewModel...
}
override fun onDestroyView() {
super.onDestroyView()
// Clear your view, unsubscribe...
}
override fun invalidate() = withState(viewModel) { state ->
//TODO
}
}

View File

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

View File

@ -0,0 +1,44 @@
package ${escapeKotlinIdentifiers(packageName)}
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.riotx.core.platform.VectorViewModel
<#if createViewEvents>
<#else>
import im.vector.riotx.core.platform.EmptyViewEvents
</#if>
class ${viewModelClass} @AssistedInject constructor(@Assisted initialState: ${viewStateClass})
<#if createViewEvents>
: VectorViewModel<${viewStateClass}, ${actionClass}, ${viewEventsClass}>(initialState) {
<#else>
: VectorViewModel<${viewStateClass}, ${actionClass}, EmptyViewEvents>(initialState) {
</#if>
@AssistedInject.Factory
interface Factory {
fun create(initialState: ${viewStateClass}): ${viewModelClass}
}
companion object : MvRxViewModelFactory<${viewModelClass}, ${viewStateClass}> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: ${viewStateClass}): ${viewModelClass}? {
val factory = when (viewModelContext) {
is FragmentViewModelContext -> viewModelContext.fragment as? Factory
is ActivityViewModelContext -> viewModelContext.activity as? Factory
}
return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface")
}
}
override fun handle(action: ${actionClass}) {
//TODO
}
}

View File

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

View File

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

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

@ -0,0 +1,24 @@
#!/usr/bin/env bash
#
# Copyright 2020 New Vector Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
echo "Configure RiotX Template..."
{
ln -s $(pwd)/RiotXFeature /Applications/Android\ Studio.app/Contents/plugins/android/lib/templates/other
} && {
echo "Please restart Android Studio."
}

View File

@ -235,6 +235,15 @@ android {
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = "1.8"
} }
sourceSets {
androidTest {
java.srcDirs += "src/sharedTest/java"
}
test {
java.srcDirs += "src/sharedTest/java"
}
}
} }
dependencies { dependencies {
@ -250,6 +259,7 @@ dependencies {
def daggerVersion = '2.25.4' def daggerVersion = '2.25.4'
def autofill_version = "1.0.0" def autofill_version = "1.0.0"
def work_version = '2.3.3' def work_version = '2.3.3'
def arch_version = '2.1.0'
implementation project(":matrix-sdk-android") implementation project(":matrix-sdk-android")
implementation project(":matrix-sdk-android-rx") implementation project(":matrix-sdk-android-rx")
@ -378,10 +388,18 @@ dependencies {
// TESTS // TESTS
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
testImplementation 'org.amshove.kluent:kluent-android:1.44' testImplementation 'org.amshove.kluent:kluent-android:1.44'
// Plant Timber tree for test
testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
androidTestImplementation 'androidx.test:core:1.2.0'
androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test:rules:1.2.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
androidTestImplementation 'org.amshove.kluent:kluent-android:1.44' androidTestImplementation 'org.amshove.kluent:kluent-android:1.44'
androidTestImplementation "androidx.arch.core:core-testing:$arch_version"
// Plant Timber tree for test
androidTestImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
} }
if (getGradle().getStartParameter().getTaskRequests().toString().contains("Gplay")) { if (getGradle().getStartParameter().getTaskRequests().toString().contains("Gplay")) {

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import im.vector.riotx.test.shared.createTimberTestRule
import org.junit.Rule
interface InstrumentedTest {
@Rule
fun timberTestRule() = createTimberTestRule()
fun context(): Context {
return ApplicationProvider.getApplicationContext()
}
}

View File

@ -0,0 +1,95 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.reactions.data
import im.vector.riotx.InstrumentedTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.junit.runners.MethodSorters
import kotlin.system.measureTimeMillis
@RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
class EmojiDataSourceTest : InstrumentedTest {
@Test
fun checkParsingTime() {
val time = measureTimeMillis {
EmojiDataSource(context().resources)
}
assertTrue("Too long to parse", time < 100)
}
@Test
fun checkNumberOfResult() {
val emojiDataSource = EmojiDataSource(context().resources)
assertEquals("Wrong number of emojis", 1545, emojiDataSource.rawData.emojis.size)
assertEquals("Wrong number of categories", 8, emojiDataSource.rawData.categories.size)
assertEquals("Wrong number of aliases", 57, emojiDataSource.rawData.aliases.size)
}
@Test
fun searchTestEmptySearch() {
val emojiDataSource = EmojiDataSource(context().resources)
assertEquals("Empty search should return 1545 results", 1545, emojiDataSource.filterWith("").size)
}
@Test
fun searchTestNoResult() {
val emojiDataSource = EmojiDataSource(context().resources)
assertTrue("Should not have result", emojiDataSource.filterWith("noresult").isEmpty())
}
@Test
fun searchTestOneResult() {
val emojiDataSource = EmojiDataSource(context().resources)
assertEquals("Should have 1 result", 1, emojiDataSource.filterWith("france").size)
}
@Test
fun searchTestManyResult() {
val emojiDataSource = EmojiDataSource(context().resources)
assertTrue("Should have many result", emojiDataSource.filterWith("fra").size > 1)
}
@Test
fun testTada() {
val emojiDataSource = EmojiDataSource(context().resources)
val result = emojiDataSource.filterWith("tada")
assertEquals("Should find tada emoji", 1, result.size)
assertEquals("Should find tada emoji", "🎉", result[0].emoji)
}
@Test
fun testQuickReactions() {
val emojiDataSource = EmojiDataSource(context().resources)
assertEquals("Should have 8 quick reactions", 8, emojiDataSource.getQuickReactions().size)
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.core.dialogs
import android.app.Activity
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import im.vector.matrix.android.api.extensions.getFingerprintHumanReadable
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.riotx.R
object ManuallyVerifyDialog {
fun show(activity: Activity, cryptoDeviceInfo: CryptoDeviceInfo, onVerified: (() -> Unit)) {
val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_device_verify, null)
val builder = AlertDialog.Builder(activity)
.setTitle(R.string.cross_signing_verify_by_text)
.setView(dialogLayout)
.setPositiveButton(R.string.encryption_information_verify) { _, _ ->
onVerified()
}
.setNegativeButton(R.string.cancel, null)
dialogLayout.findViewById<TextView>(R.id.encrypted_device_info_device_name)?.let {
it.text = cryptoDeviceInfo.displayName()
}
dialogLayout.findViewById<TextView>(R.id.encrypted_device_info_device_id)?.let {
it.text = cryptoDeviceInfo.deviceId
}
dialogLayout.findViewById<TextView>(R.id.encrypted_device_info_device_key)?.let {
it.text = cryptoDeviceInfo.getFingerprintHumanReadable()
}
builder.show()
}
}

View File

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

View File

@ -0,0 +1,65 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.core.ui.list
import android.view.View
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.google.android.material.button.MaterialButton
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.features.themes.ThemeUtils
/**
* A generic button list item.
*/
@EpoxyModelClass(layout = R.layout.item_generic_button)
abstract class GenericButtonItem : VectorEpoxyModel<GenericButtonItem.Holder>() {
@EpoxyAttribute
var text: String? = null
@EpoxyAttribute
var itemClickAction: View.OnClickListener? = null
@EpoxyAttribute
@ColorInt
var textColor: Int? = null
@EpoxyAttribute
@DrawableRes
var iconRes: Int? = null
override fun bind(holder: Holder) {
holder.button.text = text
val textColor = textColor ?: ThemeUtils.getColor(holder.view.context, R.attr.riotx_text_primary)
holder.button.setTextColor(textColor)
if (iconRes != null) {
holder.button.setIconResource(iconRes!!)
} else {
holder.button.icon = null
}
itemClickAction?.let { holder.view.setOnClickListener(it) }
}
class Holder : VectorEpoxyHolder() {
val button by bind<MaterialButton>(R.id.itemGenericItemButton)
}
}

View File

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

View File

@ -27,14 +27,13 @@ import im.vector.matrix.android.api.session.crypto.verification.SasVerificationT
import im.vector.matrix.android.api.session.crypto.verification.VerificationService import im.vector.matrix.android.api.session.crypto.verification.VerificationService
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.IncomingRequestCancellation import im.vector.matrix.android.internal.crypto.IncomingRequestCancellation
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.features.popup.DefaultVectorAlert import im.vector.riotx.features.popup.DefaultVectorAlert
import im.vector.riotx.features.popup.PopupAlertManager import im.vector.riotx.features.popup.PopupAlertManager
@ -75,7 +74,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context, privat
session = null 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 // By default riotX will not prompt if the SDK has decided that the request should not be fulfilled
Timber.v("## onSecretShareRequest() : Ignoring $request") Timber.v("## onSecretShareRequest() : Ignoring $request")
request.ignore?.run() request.ignore?.run()
@ -124,19 +123,11 @@ class KeyRequestHandler @Inject constructor(private val context: Context, privat
deviceInfo.trustLevel = DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false) deviceInfo.trustLevel = DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false)
// can we get more info on this device? // can we get more info on this device?
session?.cryptoService()?.getDevicesList(object : MatrixCallback<DevicesListResponse> { session?.cryptoService()?.getMyDevicesInfo()?.firstOrNull { it.deviceId == deviceId }?.let {
override fun onSuccess(data: DevicesListResponse) {
data.devices?.find { it.deviceId == deviceId }?.let {
postAlert(context, userId, deviceId, true, deviceInfo, it) postAlert(context, userId, deviceId, true, deviceInfo, it)
} ?: run { } ?: kotlin.run {
postAlert(context, userId, deviceId, true, deviceInfo) postAlert(context, userId, deviceId, true, deviceInfo)
} }
}
override fun onFailure(failure: Throwable) {
postAlert(context, userId, deviceId, true, deviceInfo)
}
})
} else { } else {
postAlert(context, userId, deviceId, false, deviceInfo) postAlert(context, userId, deviceId, false, deviceInfo)
} }

View File

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

View File

@ -18,6 +18,7 @@ package im.vector.riotx.features.crypto.recover
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.api.failure.toRegistrationFlowResponse
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
@ -28,13 +29,11 @@ import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageSer
import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo
import im.vector.matrix.android.api.session.securestorage.SsssKeySpec import im.vector.matrix.android.api.session.securestorage.SsssKeySpec
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion
import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.util.awaitCallback import im.vector.matrix.android.internal.util.awaitCallback
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.platform.ViewModelTask import im.vector.riotx.core.platform.ViewModelTask
@ -230,15 +229,10 @@ class BootstrapCrossSigningTask @Inject constructor(
private fun handleInitializeXSigningError(failure: Throwable): BootstrapResult { private fun handleInitializeXSigningError(failure: Throwable): BootstrapResult {
if (failure is Failure.ServerError && failure.error.code == MatrixError.M_FORBIDDEN) { if (failure is Failure.ServerError && failure.error.code == MatrixError.M_FORBIDDEN) {
return BootstrapResult.InvalidPasswordError(failure.error) return BootstrapResult.InvalidPasswordError(failure.error)
} else if (failure is Failure.OtherServerError && failure.httpCode == 401) { } else {
try { val registrationFlowResponse = failure.toRegistrationFlowResponse()
MoshiProvider.providesMoshi() if (registrationFlowResponse != null) {
.adapter(RegistrationFlowResponse::class.java) if (registrationFlowResponse.flows?.any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true } != true) {
.fromJson(failure.errorBody)
} catch (e: Exception) {
null
}?.let { flowResponse ->
if (flowResponse.flows?.any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true } != true) {
// can't do this from here // can't do this from here
return BootstrapResult.UnsupportedAuthFlow() return BootstrapResult.UnsupportedAuthFlow()
} }

View File

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

View File

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

View File

@ -165,7 +165,9 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
} else { } else {
otherUserShield.setImageResource(R.drawable.ic_shield_warning) otherUserShield.setImageResource(R.drawable.ic_shield_warning)
} }
otherUserNameText.text = getString(R.string.complete_security) otherUserNameText.text = getString(
if (state.selfVerificationMode) R.string.crosssigning_verify_this_session else R.string.crosssigning_verify_session
)
otherUserShield.isVisible = true otherUserShield.isVisible = true
} else { } else {
avatarRenderer.render(matrixItem, otherUserAvatarImageView) avatarRenderer.render(matrixItem, otherUserAvatarImageView)

View File

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

View File

@ -167,6 +167,8 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
val crossSigningEnabledOnAccount = myCrossSigningKeys != null val crossSigningEnabledOnAccount = myCrossSigningKeys != null
if (!crossSigningEnabledOnAccount && !sharedActionViewModel.isAccountCreation) { if (!crossSigningEnabledOnAccount && !sharedActionViewModel.isAccountCreation) {
// Do not propose for SSO accounts, because we do not support yet confirming account credentials using SSO
if (session.getHomeServerCapabilities().canChangePassword) {
// We need to ask // We need to ask
promptSecurityEvent( promptSecurityEvent(
session, session,
@ -175,12 +177,16 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
) { ) {
it.navigator.upgradeSessionSecurity(it) it.navigator.upgradeSessionSecurity(it)
} }
} else {
// Do not do it again
sharedActionViewModel.hasDisplayedCompleteSecurityPrompt = true
}
} else if (myCrossSigningKeys?.isTrusted() == false) { } else if (myCrossSigningKeys?.isTrusted() == false) {
// We need to ask // We need to ask
promptSecurityEvent( promptSecurityEvent(
session, session,
R.string.complete_security, R.string.crosssigning_verify_this_session,
R.string.crosssigning_verify_this_session R.string.confirm_your_identity
) { ) {
it.navigator.waitSessionVerification(it) it.navigator.waitSessionVerification(it)
} }

View File

@ -1,4 +1,3 @@
/* /*
* Copyright 2019 New Vector Ltd * Copyright 2019 New Vector Ltd
* *
@ -31,6 +30,7 @@ import com.google.android.material.bottomnavigation.BottomNavigationMenuView
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.commitTransactionNow import im.vector.riotx.core.extensions.commitTransactionNow
import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.glide.GlideApp
@ -43,6 +43,7 @@ import im.vector.riotx.features.home.room.list.RoomListParams
import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView
import im.vector.riotx.features.popup.PopupAlertManager import im.vector.riotx.features.popup.PopupAlertManager
import im.vector.riotx.features.popup.VerificationVectorAlert import im.vector.riotx.features.popup.VerificationVectorAlert
import im.vector.riotx.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS
import im.vector.riotx.features.workers.signout.SignOutViewModel import im.vector.riotx.features.workers.signout.SignOutViewModel
import kotlinx.android.synthetic.main.fragment_home_detail.* import kotlinx.android.synthetic.main.fragment_home_detail.*
import timber.log.Timber import timber.log.Timber
@ -61,7 +62,7 @@ class HomeDetailFragment @Inject constructor(
private val unreadCounterBadgeViews = arrayListOf<UnreadCounterBadgeView>() private val unreadCounterBadgeViews = arrayListOf<UnreadCounterBadgeView>()
private val viewModel: HomeDetailViewModel by fragmentViewModel() private val viewModel: HomeDetailViewModel by fragmentViewModel()
private val unknownDeviceDetectorSharedViewModel : UnknownDeviceDetectorSharedViewModel by activityViewModel() private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel()
private lateinit var sharedActionViewModel: HomeSharedActionViewModel private lateinit var sharedActionViewModel: HomeSharedActionViewModel
@ -87,17 +88,27 @@ class HomeDetailFragment @Inject constructor(
switchDisplayMode(displayMode) switchDisplayMode(displayMode)
} }
unknownDeviceDetectorSharedViewModel.subscribe { unknownDeviceDetectorSharedViewModel.subscribe { state ->
it.unknownSessions.invoke()?.let { unknownDevices -> state.unknownSessions.invoke()?.let { unknownDevices ->
Timber.v("## Detector - ${unknownDevices.size} Unknown sessions") // Timber.v("## Detector Triggerred in fragment - ${unknownDevices.firstOrNull()}")
unknownDevices.forEachIndexed { index, deviceInfo -> if (unknownDevices.firstOrNull()?.currentSessionTrust == true) {
Timber.v("## Detector - #$index deviceId:${deviceInfo.second.deviceId} lastSeenTs:${deviceInfo.second.lastSeenTs}") val uid = "review_login"
}
val uid = "Newest_Device"
alertManager.cancelAlert(uid) alertManager.cancelAlert(uid)
if (it.canCrossSign && unknownDevices.isNotEmpty()) { val olderUnverified = unknownDevices.filter { !it.isNew }
val newest = unknownDevices.first().second val newest = unknownDevices.firstOrNull { it.isNew }?.deviceInfo
val user = unknownDevices.first().first if (newest != null) {
promptForNewUnknownDevices(uid, state, newest)
} else if (olderUnverified.isNotEmpty()) {
// In this case we prompt to go to settings to review logins
promptToReviewChanges(uid, state, olderUnverified.map { it.deviceInfo })
}
}
}
}
}
private fun promptForNewUnknownDevices(uid: String, state: UnknownDevicesState, newest: DeviceInfo) {
val user = state.myMatrixItem
alertManager.postVectorAlert( alertManager.postVectorAlert(
VerificationVectorAlert( VerificationVectorAlert(
uid = uid, uid = uid,
@ -111,13 +122,46 @@ class HomeDetailFragment @Inject constructor(
(weakCurrentActivity?.get() as? VectorBaseActivity) (weakCurrentActivity?.get() as? VectorBaseActivity)
?.navigator ?.navigator
?.requestSessionVerification(requireContext(), newest.deviceId ?: "") ?.requestSessionVerification(requireContext(), newest.deviceId ?: "")
unknownDeviceDetectorSharedViewModel.handle(
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) } ?: emptyList())
)
} }
dismissedAction = Runnable {} dismissedAction = Runnable {
} unknownDeviceDetectorSharedViewModel.handle(
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) } ?: emptyList())
) )
} }
} }
)
} }
private fun promptToReviewChanges(uid: String, state: UnknownDevicesState, oldUnverified: List<DeviceInfo>) {
val user = state.myMatrixItem
alertManager.postVectorAlert(
VerificationVectorAlert(
uid = uid,
title = getString(R.string.review_logins),
description = getString(R.string.verify_other_sessions),
iconId = R.drawable.ic_shield_warning
).apply {
matrixItem = user
colorInt = ContextCompat.getColor(requireActivity(), R.color.riotx_accent)
contentAction = Runnable {
(weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
// mark as ignored to avoid showing it again
unknownDeviceDetectorSharedViewModel.handle(
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(oldUnverified.mapNotNull { it.deviceId })
)
it.navigator.openSettings(it, EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS)
}
}
dismissedAction = Runnable {
unknownDeviceDetectorSharedViewModel.handle(
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(oldUnverified.mapNotNull { it.deviceId })
)
}
}
)
} }
private fun onGroupChange(groupSummary: GroupSummary?) { private fun onGroupChange(groupSummary: GroupSummary?) {

View File

@ -0,0 +1,156 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import im.vector.matrix.android.api.NoOpMatrixCallback
import im.vector.matrix.android.api.extensions.orFalse
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo
import im.vector.matrix.rx.rx
import im.vector.riotx.core.di.HasScreenInjector
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.platform.VectorViewModelAction
import im.vector.riotx.features.settings.VectorPreferences
import io.reactivex.Observable
import io.reactivex.functions.Function3
import timber.log.Timber
import java.util.concurrent.TimeUnit
data class UnknownDevicesState(
val myMatrixItem: MatrixItem.UserItem? = null,
val unknownSessions: Async<List<DeviceDetectionInfo>> = Uninitialized
) : MvRxState
data class DeviceDetectionInfo(
val deviceInfo: DeviceInfo,
val isNew: Boolean,
val currentSessionTrust: Boolean
)
class UnknownDeviceDetectorSharedViewModel(
session: Session,
private val vectorPreferences: VectorPreferences,
initialState: UnknownDevicesState)
: VectorViewModel<UnknownDevicesState, UnknownDeviceDetectorSharedViewModel.Action, EmptyViewEvents>(initialState) {
sealed class Action : VectorViewModelAction {
data class IgnoreDevice(val deviceIds: List<String>) : Action()
}
private val ignoredDeviceList = ArrayList<String>()
init {
val currentSessionTs = session.cryptoService().getCryptoDeviceInfo(session.myUserId).firstOrNull {
it.deviceId == session.sessionParams.credentials.deviceId
}?.firstTimeSeenLocalTs ?: System.currentTimeMillis()
Timber.v("## Detector - Current Session first time seen $currentSessionTs")
ignoredDeviceList.addAll(
vectorPreferences.getUnknownDeviceDismissedList().also {
Timber.v("## Detector - Remembered ignored list $it")
}
)
Observable.combineLatest<List<CryptoDeviceInfo>, List<DeviceInfo>, Optional<PrivateKeysInfo>, List<DeviceDetectionInfo>>(
session.rx().liveUserCryptoDevices(session.myUserId),
session.rx().liveMyDeviceInfo(),
session.rx().liveCrossSigningPrivateKeys(),
Function3 { cryptoList, infoList, pInfo ->
// Timber.v("## Detector trigger ${cryptoList.map { "${it.deviceId} ${it.trustLevel}" }}")
// Timber.v("## Detector trigger canCrossSign ${pInfo.get().selfSigned != null}")
infoList
.filter { info ->
// filter verified session, by checking the crypto device info
cryptoList.firstOrNull { info.deviceId == it.deviceId }?.isVerified?.not().orFalse()
}
// filter out ignored devices
.filter { !ignoredDeviceList.contains(it.deviceId) }
.sortedByDescending { it.lastSeenTs }
.map { deviceInfo ->
val deviceKnownSince = cryptoList.firstOrNull { it.deviceId == deviceInfo.deviceId }?.firstTimeSeenLocalTs ?: 0
DeviceDetectionInfo(
deviceInfo,
deviceKnownSince > currentSessionTs + 60_000, // short window to avoid false positive,
pInfo.getOrNull()?.selfSigned != null // adding this to pass distinct when cross sign change
)
}
}
)
.distinctUntilChanged()
.execute { async ->
// Timber.v("## Detector trigger passed distinct")
copy(
myMatrixItem = session.getUser(session.myUserId)?.toMatrixItem(),
unknownSessions = async
)
}
session.rx().liveUserCryptoDevices(session.myUserId)
.distinct()
.throttleLast(5_000, TimeUnit.MILLISECONDS)
.subscribe {
// If we have a new crypto device change, we might want to trigger refresh of device info
session.cryptoService().fetchDevicesList(NoOpMatrixCallback())
}.disposeOnClear()
// trigger a refresh of lastSeen / last Ip
session.cryptoService().fetchDevicesList(NoOpMatrixCallback())
}
override fun handle(action: Action) {
when (action) {
is Action.IgnoreDevice -> {
ignoredDeviceList.addAll(action.deviceIds)
// local echo
withState { state ->
state.unknownSessions.invoke()?.let { detectedSessions ->
val updated = detectedSessions.filter { !action.deviceIds.contains(it.deviceInfo.deviceId) }
setState {
copy(unknownSessions = Success(updated))
}
}
}
}
}
}
override fun onCleared() {
vectorPreferences.storeUnknownDeviceDismissedList(ignoredDeviceList)
super.onCleared()
}
companion object : MvRxViewModelFactory<UnknownDeviceDetectorSharedViewModel, UnknownDevicesState> {
override fun create(viewModelContext: ViewModelContext, state: UnknownDevicesState): UnknownDeviceDetectorSharedViewModel? {
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
return UnknownDeviceDetectorSharedViewModel(session, VectorPreferences(viewModelContext.activity()), state)
}
}
}

View File

@ -1,87 +0,0 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.NoOpCancellable
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.matrix.rx.rx
import im.vector.matrix.rx.singleBuilder
import im.vector.riotx.core.di.HasScreenInjector
import im.vector.riotx.core.platform.EmptyAction
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel
import java.util.concurrent.TimeUnit
data class UnknownDevicesState(
val unknownSessions: Async<List<Pair<MatrixItem?, DeviceInfo>>> = Uninitialized,
val canCrossSign: Boolean = false
) : MvRxState
class UnknownDeviceDetectorSharedViewModel(session: Session, initialState: UnknownDevicesState)
: VectorViewModel<UnknownDevicesState, EmptyAction, EmptyViewEvents>(initialState) {
init {
session.rx().liveUserCryptoDevices(session.myUserId)
.debounce(600, TimeUnit.MILLISECONDS)
.distinct()
.switchMap { deviceList ->
// Timber.v("## Detector - ============================")
// Timber.v("## Detector - Crypto device update ${deviceList.map { "${it.deviceId} : ${it.isVerified}" }}")
singleBuilder<DevicesListResponse> {
session.cryptoService().getDevicesList(it)
NoOpCancellable
}.map { resp ->
// Timber.v("## Detector - Device Infos ${resp.devices?.map { "${it.deviceId} : lastSeen:${it.lastSeenTs}" }}")
resp.devices?.filter { info ->
deviceList.firstOrNull { info.deviceId == it.deviceId }?.let {
!it.isVerified
} ?: false
}?.sortedByDescending { it.lastSeenTs }
?.map {
session.getUser(it.user_id ?: "")?.toMatrixItem() to it
} ?: emptyList()
}
.toObservable()
}
.execute { async ->
copy(unknownSessions = async)
}
session.rx().liveCrossSigningInfo(session.myUserId)
.execute {
copy(canCrossSign = session.cryptoService().crossSigningService().canCrossSign())
}
}
override fun handle(action: EmptyAction) {}
companion object : MvRxViewModelFactory<UnknownDeviceDetectorSharedViewModel, UnknownDevicesState> {
override fun create(viewModelContext: ViewModelContext, state: UnknownDevicesState): UnknownDeviceDetectorSharedViewModel? {
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
return UnknownDeviceDetectorSharedViewModel(session, state)
}
}
}

View File

@ -26,7 +26,6 @@ import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.content.ContextCompat
import androidx.core.text.toSpannable import androidx.core.text.toSpannable
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.transition.AutoTransition import androidx.transition.AutoTransition
@ -172,7 +171,7 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib
RoomEncryptionTrustLevel.Warning -> R.drawable.ic_shield_warning RoomEncryptionTrustLevel.Warning -> R.drawable.ic_shield_warning
else -> R.drawable.ic_shield_black else -> R.drawable.ic_shield_black
} }
composerShieldImageView.setImageDrawable(ContextCompat.getDrawable(context, shieldRes)) composerShieldImageView.setImageResource(shieldRes)
} else { } else {
composerEditText.setHint(R.string.room_message_placeholder) composerEditText.setHint(R.string.room_message_placeholder)
composerShieldImageView.isVisible = false composerShieldImageView.isVisible = false

View File

@ -29,6 +29,7 @@ import im.vector.riotx.core.epoxy.dividerItem
import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.item.E2EDecoration
import im.vector.riotx.features.home.room.detail.timeline.tools.createLinkMovementMethod import im.vector.riotx.features.home.room.detail.timeline.tools.createLinkMovementMethod
import im.vector.riotx.features.home.room.detail.timeline.tools.linkify import im.vector.riotx.features.home.room.detail.timeline.tools.linkify
import javax.inject.Inject import javax.inject.Inject
@ -72,6 +73,29 @@ class MessageActionsEpoxyController @Inject constructor(
} }
} }
when (state.informationData.e2eDecoration) {
E2EDecoration.WARN_IN_CLEAR -> {
bottomSheetSendStateItem {
id("e2e_clear")
showProgress(false)
text(stringProvider.getString(R.string.unencrypted))
drawableStart(R.drawable.ic_shield_warning_small)
}
}
E2EDecoration.WARN_SENT_BY_UNVERIFIED,
E2EDecoration.WARN_SENT_BY_UNKNOWN -> {
bottomSheetSendStateItem {
id("e2e_unverified")
showProgress(false)
text(stringProvider.getString(R.string.encrypted_unverified))
drawableStart(R.drawable.ic_shield_warning_small)
}
}
else -> {
// nothing
}
}
// Quick reactions // Quick reactions
if (state.canReact() && state.quickStates is Success) { if (state.canReact() && state.quickStates is Success) {
// Separator // Separator

View File

@ -18,19 +18,23 @@
package im.vector.riotx.features.home.room.detail.timeline.helper package im.vector.riotx.features.home.room.detail.timeline.helper
import im.vector.matrix.android.api.extensions.orFalse
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.model.ReferencesAggregatedContent import im.vector.matrix.android.api.session.room.model.ReferencesAggregatedContent
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.matrix.android.internal.session.room.VerificationState import im.vector.matrix.android.internal.session.room.VerificationState
import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.core.extensions.localDateTime
import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.utils.getColorFromUserId import im.vector.riotx.core.utils.getColorFromUserId
import im.vector.riotx.features.home.room.detail.timeline.item.E2EDecoration
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.home.room.detail.timeline.item.PollResponseData import im.vector.riotx.features.home.room.detail.timeline.item.PollResponseData
import im.vector.riotx.features.home.room.detail.timeline.item.ReactionInfoData import im.vector.riotx.features.home.room.detail.timeline.item.ReactionInfoData
@ -72,6 +76,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId)) textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId))
} }
val room = event.root.roomId?.let { session.getRoom(it) }
val e2eDecoration = getE2EDecoration(room, event)
return MessageInformationData( return MessageInformationData(
eventId = eventId, eventId = eventId,
senderId = event.root.senderId ?: "", senderId = event.root.senderId ?: "",
@ -111,10 +117,59 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
?: VerificationState.REQUEST ?: VerificationState.REQUEST
ReferencesInfoData(verificationState) ReferencesInfoData(verificationState)
}, },
sentByMe = event.root.senderId == session.myUserId sentByMe = event.root.senderId == session.myUserId,
e2eDecoration = e2eDecoration
) )
} }
private fun getE2EDecoration(room: Room?, event: TimelineEvent): E2EDecoration {
return if (room?.isEncrypted() == true
// is user verified
&& session.cryptoService().crossSigningService().getUserCrossSigningKeys(event.root.senderId ?: "")?.isTrusted() == true) {
val ts = room.roomSummary()?.encryptionEventTs ?: 0
val eventTs = event.root.originServerTs ?: 0
if (event.isEncrypted()) {
// Do not decorate failed to decrypt, or redaction (we lost sender device info)
if (event.root.getClearType() == EventType.ENCRYPTED || event.root.isRedacted()) {
E2EDecoration.NONE
} else {
val sendingDevice = event.root.content
.toModel<EncryptedEventContent>()
?.deviceId
?.let { deviceId ->
session.cryptoService().getDeviceInfo(event.root.senderId ?: "", deviceId)
}
when {
sendingDevice == null -> {
// For now do not decorate this with warning
// maybe it's a deleted session
E2EDecoration.NONE
}
sendingDevice.trustLevel == null -> {
E2EDecoration.WARN_SENT_BY_UNKNOWN
}
sendingDevice.trustLevel?.isVerified().orFalse() -> {
E2EDecoration.NONE
}
else -> {
E2EDecoration.WARN_SENT_BY_UNVERIFIED
}
}
}
} else {
if (EventType.isStateEvent(event.root.type)) {
// Do not warn for state event, they are always in clear
E2EDecoration.NONE
} else {
// If event is in clear after the room enabled encryption we should warn
if (eventTs > ts) E2EDecoration.WARN_IN_CLEAR else E2EDecoration.NONE
}
}
} else {
E2EDecoration.NONE
}
}
/** /**
* Tiles type message never show the sender information (like verification request), so we should repeat it for next message * Tiles type message never show the sender information (like verification request), so we should repeat it for next message
* even if same sender * even if same sender

View File

@ -92,6 +92,18 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
holder.reactionsContainer.setOnLongClickListener(baseAttributes.itemLongClickListener) holder.reactionsContainer.setOnLongClickListener(baseAttributes.itemLongClickListener)
} }
when (baseAttributes.informationData.e2eDecoration) {
E2EDecoration.NONE -> {
holder.e2EDecorationView.isVisible = false
}
E2EDecoration.WARN_IN_CLEAR,
E2EDecoration.WARN_SENT_BY_UNVERIFIED,
E2EDecoration.WARN_SENT_BY_UNKNOWN -> {
holder.e2EDecorationView.setImageResource(R.drawable.ic_shield_warning)
holder.e2EDecorationView.isVisible = true
}
}
holder.view.setOnClickListener(baseAttributes.itemClickListener) holder.view.setOnClickListener(baseAttributes.itemClickListener)
holder.view.setOnLongClickListener(baseAttributes.itemLongClickListener) holder.view.setOnLongClickListener(baseAttributes.itemLongClickListener)
} }
@ -110,6 +122,7 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
abstract class Holder(@IdRes stubId: Int) : BaseEventItem.BaseHolder(stubId) { abstract class Holder(@IdRes stubId: Int) : BaseEventItem.BaseHolder(stubId) {
val reactionsContainer by bind<ViewGroup>(R.id.reactionsContainer) val reactionsContainer by bind<ViewGroup>(R.id.reactionsContainer)
val e2EDecorationView by bind<ImageView>(R.id.messageE2EDecoration)
} }
/** /**

View File

@ -40,7 +40,8 @@ data class MessageInformationData(
val hasPendingEdits: Boolean = false, val hasPendingEdits: Boolean = false,
val readReceipts: List<ReadReceiptData> = emptyList(), val readReceipts: List<ReadReceiptData> = emptyList(),
val referencesInfoData: ReferencesInfoData? = null, val referencesInfoData: ReferencesInfoData? = null,
val sentByMe : Boolean val sentByMe : Boolean,
val e2eDecoration: E2EDecoration = E2EDecoration.NONE
) : Parcelable { ) : Parcelable {
val matrixItem: MatrixItem val matrixItem: MatrixItem
@ -75,4 +76,11 @@ data class PollResponseData(
val isClosed: Boolean = false val isClosed: Boolean = false
) : Parcelable ) : Parcelable
enum class E2EDecoration {
NONE,
WARN_IN_CLEAR,
WARN_SENT_BY_UNVERIFIED,
WARN_SENT_BY_UNKNOWN
}
fun ReadReceiptData.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) fun ReadReceiptData.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)

View File

@ -19,6 +19,7 @@ package im.vector.riotx.features.home.room.detail.timeline.item
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R import im.vector.riotx.R
@ -45,6 +46,18 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
holder.view.setOnLongClickListener(attributes.itemLongClickListener) holder.view.setOnLongClickListener(attributes.itemLongClickListener)
holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener) holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener)
holder.avatarImageView.onClick(attributes.avatarClickListener) holder.avatarImageView.onClick(attributes.avatarClickListener)
when (attributes.informationData.e2eDecoration) {
E2EDecoration.NONE -> {
holder.e2EDecorationView.isVisible = false
}
E2EDecoration.WARN_IN_CLEAR,
E2EDecoration.WARN_SENT_BY_UNVERIFIED,
E2EDecoration.WARN_SENT_BY_UNKNOWN -> {
holder.e2EDecorationView.setImageResource(R.drawable.ic_shield_warning)
holder.e2EDecorationView.isVisible = true
}
}
} }
override fun getEventIds(): List<String> { override fun getEventIds(): List<String> {
@ -56,6 +69,7 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
class Holder : BaseHolder(STUB_ID) { class Holder : BaseHolder(STUB_ID) {
val avatarImageView by bind<ImageView>(R.id.itemNoticeAvatarView) val avatarImageView by bind<ImageView>(R.id.itemNoticeAvatarView)
val noticeTextView by bind<TextView>(R.id.itemNoticeTextView) val noticeTextView by bind<TextView>(R.id.itemNoticeTextView)
val e2EDecorationView by bind<ImageView>(R.id.messageE2EDecoration)
} }
data class Attributes( data class Attributes(

View File

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

View File

@ -31,9 +31,14 @@ import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.activityViewModel
import im.vector.matrix.android.api.auth.LOGIN_FALLBACK_PATH
import im.vector.matrix.android.api.auth.REGISTER_FALLBACK_PATH
import im.vector.matrix.android.api.auth.SSO_FALLBACK_PATH
import im.vector.matrix.android.api.auth.SSO_REDIRECT_URL_PARAM
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.appendParamToUrl
import im.vector.riotx.core.utils.AssetReader import im.vector.riotx.core.utils.AssetReader
import im.vector.riotx.features.signout.soft.SoftLogoutAction import im.vector.riotx.features.signout.soft.SoftLogoutAction
import im.vector.riotx.features.signout.soft.SoftLogoutViewModel import im.vector.riotx.features.signout.soft.SoftLogoutViewModel
@ -123,14 +128,24 @@ class LoginWebFragment @Inject constructor(
val url = buildString { val url = buildString {
append(state.homeServerUrl?.trim { it == '/' }) append(state.homeServerUrl?.trim { it == '/' })
if (state.signMode == SignMode.SignIn) { if (state.signMode == SignMode.SignIn) {
append("/_matrix/static/client/login/") if (state.loginMode == LoginMode.Sso) {
append(SSO_FALLBACK_PATH)
// We do not want to deal with the result, so let the fallback login page to handle it for us
appendParamToUrl(SSO_REDIRECT_URL_PARAM,
buildString {
append(state.homeServerUrl?.trim { it == '/' })
append(LOGIN_FALLBACK_PATH)
})
} else {
append(LOGIN_FALLBACK_PATH)
}
state.deviceId?.takeIf { it.isNotBlank() }?.let { state.deviceId?.takeIf { it.isNotBlank() }?.let {
// But https://github.com/matrix-org/synapse/issues/5755 // But https://github.com/matrix-org/synapse/issues/5755
append("?device_id=$it") appendParamToUrl("device_id", it)
} }
} else { } else {
// MODE_REGISTER // MODE_REGISTER
append("/_matrix/static/client/register/") append(REGISTER_FALLBACK_PATH)
} }
} }

View File

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

View File

@ -32,6 +32,23 @@ class EmojiDataSource @Inject constructor(
.adapter(EmojiData::class.java) .adapter(EmojiData::class.java)
.fromJson(input.bufferedReader().use { it.readText() }) .fromJson(input.bufferedReader().use { it.readText() })
} }
?.let { parsedRawData ->
// Add key as a keyword, it will solve the issue that ":tada" is not available in completion
parsedRawData.copy(
emojis = mutableMapOf<String, EmojiItem>().apply {
parsedRawData.emojis.keys.forEach { key ->
val origin = parsedRawData.emojis[key] ?: return@forEach
// Do not add keys containing '_'
if (origin.keywords.contains(key) || key.contains("_")) {
put(key, origin)
} else {
put(key, origin.copy(keywords = origin.keywords + key))
}
}
}
)
}
?: EmojiData(emptyList(), emptyMap(), emptyMap()) ?: EmojiData(emptyList(), emptyMap(), emptyMap())
private val quickReactions = mutableListOf<EmojiItem>() private val quickReactions = mutableListOf<EmojiItem>()

View File

@ -101,7 +101,7 @@ class DeviceTrustInfoEpoxyController @Inject constructor(private val stringProvi
bottomSheetVerificationActionItem { bottomSheetVerificationActionItem {
id("verify") id("verify")
title(stringProvider.getString(R.string.verification_verify_device_manually)) title(stringProvider.getString(R.string.cross_signing_verify_by_emoji))
titleColor(colorProvider.getColor(R.color.riotx_accent)) titleColor(colorProvider.getColor(R.color.riotx_accent))
iconRes(R.drawable.ic_arrow_right) iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_accent)) iconColor(colorProvider.getColor(R.color.riotx_accent))

View File

@ -24,6 +24,7 @@ import android.provider.MediaStore
import androidx.core.content.edit import androidx.core.content.edit
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.squareup.seismic.ShakeDetector import com.squareup.seismic.ShakeDetector
import im.vector.matrix.android.api.extensions.tryThis
import im.vector.riotx.BuildConfig import im.vector.riotx.BuildConfig
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.features.homeserver.ServerUrlsRepository import im.vector.riotx.features.homeserver.ServerUrlsRepository
@ -166,6 +167,8 @@ class VectorPreferences @Inject constructor(private val context: Context) {
private const val MEDIA_SAVING_1_MONTH = 2 private const val MEDIA_SAVING_1_MONTH = 2
private const val MEDIA_SAVING_FOREVER = 3 private const val MEDIA_SAVING_FOREVER = 3
private const val SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST = "SETTINGS_UNKNWON_DEVICE_DISMISSED_LIST"
// some preferences keys must be kept after a logout // some preferences keys must be kept after a logout
private val mKeysToKeepAfterLogout = listOf( private val mKeysToKeepAfterLogout = listOf(
SETTINGS_DEFAULT_MEDIA_COMPRESSION_KEY, SETTINGS_DEFAULT_MEDIA_COMPRESSION_KEY,
@ -201,6 +204,11 @@ class VectorPreferences @Inject constructor(private val context: Context) {
SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY, SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY,
SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY, SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY,
SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY,
SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY,
SETTINGS_LABS_ALLOW_EXTENDED_LOGS,
SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY,
SETTINGS_USE_RAGE_SHAKE_KEY, SETTINGS_USE_RAGE_SHAKE_KEY,
SETTINGS_SECURITY_USE_FLAG_SECURE SETTINGS_SECURITY_USE_FLAG_SECURE
) )
@ -364,6 +372,18 @@ class VectorPreferences @Inject constructor(private val context: Context) {
return defaultPrefs.getBoolean(SETTINGS_PLAY_SHUTTER_SOUND_KEY, true) return defaultPrefs.getBoolean(SETTINGS_PLAY_SHUTTER_SOUND_KEY, true)
} }
fun storeUnknownDeviceDismissedList(deviceIds: List<String>) {
defaultPrefs.edit(true) {
putStringSet(SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST, deviceIds.toSet())
}
}
fun getUnknownDeviceDismissedList(): List<String> {
return tryThis {
defaultPrefs.getStringSet(SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST, null)?.toList()
} ?: emptyList()
}
/** /**
* Update the notification ringtone * Update the notification ringtone
* *

View File

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

View File

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

View File

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

View File

@ -101,14 +101,4 @@ class CrossSigningSettingsFragment @Inject constructor(
override fun verifySession() { override fun verifySession() {
viewModel.handle(CrossSigningAction.VerifySession) viewModel.handle(CrossSigningAction.VerifySession)
} }
override fun onResetCrossSigningKeys() {
AlertDialog.Builder(requireContext())
.setTitle(R.string.dialog_title_confirmation)
.setMessage(R.string.are_you_sure)
.setPositiveButton(R.string.ok) { _, _ ->
viewModel.handle(CrossSigningAction.InitializeCrossSigning)
}
.show()
}
} }

View File

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

View File

@ -20,15 +20,17 @@ import android.graphics.Typeface
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.utils.DimensionConverter
import me.gujun.android.span.span
import java.text.DateFormat import java.text.DateFormat
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
@ -53,21 +55,37 @@ abstract class DeviceItem : VectorEpoxyModel<DeviceItem.Holder>() {
var detailedMode = false var detailedMode = false
@EpoxyAttribute @EpoxyAttribute
var trusted : Boolean? = null var trusted: DeviceTrustLevel? = null
@EpoxyAttribute
var e2eCapable: Boolean = true
@EpoxyAttribute
var legacyMode: Boolean = false
@EpoxyAttribute
var trustedSession: Boolean = false
@EpoxyAttribute
var colorProvider: ColorProvider? = null
@EpoxyAttribute
var dimensionConverter: DimensionConverter? = null
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
holder.root.setOnClickListener { itemClickAction?.invoke() } holder.root.setOnClickListener { itemClickAction?.invoke() }
if (trusted != null) { val shield = TrustUtils.shieldForTrust(
holder.trustIcon.setImageDrawable( currentDevice,
ContextCompat.getDrawable( trustedSession,
holder.view.context, legacyMode,
if (trusted!!) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning trusted
) )
)
holder.trustIcon.isInvisible = false if (e2eCapable) {
holder.trustIcon.setImageResource(shield)
} else { } else {
holder.trustIcon.isInvisible = true holder.trustIcon.setImageDrawable(null)
} }
val detailedModeLabels = listOf( val detailedModeLabels = listOf(
@ -103,7 +121,28 @@ abstract class DeviceItem : VectorEpoxyModel<DeviceItem.Holder>() {
it.setTypeface(null, if (currentDevice) Typeface.BOLD else Typeface.NORMAL) it.setTypeface(null, if (currentDevice) Typeface.BOLD else Typeface.NORMAL)
} }
} else { } else {
holder.summaryLabelText.text = deviceInfo.displayName ?: deviceInfo.deviceId ?: "" holder.summaryLabelText.text =
span {
+(deviceInfo.displayName ?: deviceInfo.deviceId ?: "")
apply {
// Add additional info if current session is not trusted
if (!trustedSession) {
+"\n"
span {
text = "${deviceInfo.deviceId}"
apply {
colorProvider?.getColorFromAttribute(R.attr.riotx_text_secondary)?.let {
textColor = it
}
dimensionConverter?.spToPx(12)?.let {
textSize = it
}
}
}
}
}
}
holder.summaryLabelText.isVisible = true holder.summaryLabelText.isVisible = true
detailedModeLabels.map { detailedModeLabels.map {
it.isVisible = false it.isVisible = false

View File

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

View File

@ -16,7 +16,9 @@
package im.vector.riotx.features.settings.devices package im.vector.riotx.features.settings.devices
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.extensions.orFalse
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.dividerItem import im.vector.riotx.core.epoxy.dividerItem
import im.vector.riotx.core.epoxy.loadingItem import im.vector.riotx.core.epoxy.loadingItem
@ -26,6 +28,7 @@ import im.vector.riotx.core.ui.list.GenericItem
import im.vector.riotx.core.ui.list.genericFooterItem import im.vector.riotx.core.ui.list.genericFooterItem
import im.vector.riotx.core.ui.list.genericItem import im.vector.riotx.core.ui.list.genericItem
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class DeviceVerificationInfoEpoxyController @Inject constructor(private val stringProvider: StringProvider, class DeviceVerificationInfoEpoxyController @Inject constructor(private val stringProvider: StringProvider,
@ -37,37 +40,162 @@ class DeviceVerificationInfoEpoxyController @Inject constructor(private val stri
override fun buildModels(data: DeviceVerificationInfoBottomSheetViewState?) { override fun buildModels(data: DeviceVerificationInfoBottomSheetViewState?) {
val cryptoDeviceInfo = data?.cryptoDeviceInfo?.invoke() val cryptoDeviceInfo = data?.cryptoDeviceInfo?.invoke()
if (cryptoDeviceInfo != null) { when {
if (cryptoDeviceInfo.isVerified) { cryptoDeviceInfo != null -> {
// It's a E2E capable device
handleE2ECapableDevice(data, cryptoDeviceInfo)
}
data?.deviceInfo?.invoke() != null -> {
// It's a non E2E capable device
handleNonE2EDevice(data)
}
else -> {
loadingItem {
id("loading")
}
}
}
}
private fun handleE2ECapableDevice(data: DeviceVerificationInfoBottomSheetViewState, cryptoDeviceInfo: CryptoDeviceInfo) {
val shield = TrustUtils.shieldForTrust(
currentDevice = data.isMine,
trustMSK = data.accountCrossSigningIsTrusted,
legacyMode = !data.hasAccountCrossSigning,
deviceTrustLevel = cryptoDeviceInfo.trustLevel
)
if (data.hasAccountCrossSigning) {
// Cross Signing is enabled
handleE2EWithCrossSigning(data.isMine, data.accountCrossSigningIsTrusted, cryptoDeviceInfo, shield)
} else {
handleE2EInLegacy(data.isMine, cryptoDeviceInfo, shield)
}
// COMMON ACTIONS (Rename / signout)
addGenericDeviceManageActions(data, cryptoDeviceInfo.deviceId)
}
private fun handleE2EWithCrossSigning(isMine: Boolean, currentSessionIsTrusted: Boolean, cryptoDeviceInfo: CryptoDeviceInfo, shield: Int) {
Timber.v("handleE2EWithCrossSigning $isMine, $cryptoDeviceInfo, $shield")
if (isMine) {
if (currentSessionIsTrusted) {
genericItem { genericItem {
id("trust${cryptoDeviceInfo.deviceId}") id("trust${cryptoDeviceInfo.deviceId}")
style(GenericItem.STYLE.BIG_TEXT) style(GenericItem.STYLE.BIG_TEXT)
titleIconResourceId(R.drawable.ic_shield_trusted) titleIconResourceId(shield)
title(stringProvider.getString(R.string.encryption_information_verified))
description(stringProvider.getString(R.string.settings_active_sessions_verified_device_desc))
}
} else {
// You need tomcomplete security
genericItem {
id("trust${cryptoDeviceInfo.deviceId}")
style(GenericItem.STYLE.BIG_TEXT)
titleIconResourceId(shield)
title(stringProvider.getString(R.string.crosssigning_verify_this_session))
description(stringProvider.getString(R.string.confirm_your_identity))
}
}
} else {
if (!currentSessionIsTrusted) {
// we don't know if this session is trusted...
// for now we show nothing?
} else {
// we rely on cross signing status
val trust = cryptoDeviceInfo.trustLevel?.isCrossSigningVerified() == true
if (trust) {
genericItem {
id("trust${cryptoDeviceInfo.deviceId}")
style(GenericItem.STYLE.BIG_TEXT)
titleIconResourceId(shield)
title(stringProvider.getString(R.string.encryption_information_verified)) title(stringProvider.getString(R.string.encryption_information_verified))
description(stringProvider.getString(R.string.settings_active_sessions_verified_device_desc)) description(stringProvider.getString(R.string.settings_active_sessions_verified_device_desc))
} }
} else { } else {
genericItem { genericItem {
id("trust${cryptoDeviceInfo.deviceId}") id("trust${cryptoDeviceInfo.deviceId}")
titleIconResourceId(R.drawable.ic_shield_warning) titleIconResourceId(shield)
style(GenericItem.STYLE.BIG_TEXT) style(GenericItem.STYLE.BIG_TEXT)
title(stringProvider.getString(R.string.encryption_information_not_verified)) title(stringProvider.getString(R.string.encryption_information_not_verified))
description(stringProvider.getString(R.string.settings_active_sessions_unverified_device_desc)) description(stringProvider.getString(R.string.settings_active_sessions_unverified_device_desc))
} }
} }
}
}
// DEVICE INFO SECTION
genericItem { genericItem {
id("info${cryptoDeviceInfo.deviceId}") id("info${cryptoDeviceInfo.deviceId}")
title(cryptoDeviceInfo.displayName() ?: "") title(cryptoDeviceInfo.displayName() ?: "")
description("(${cryptoDeviceInfo.deviceId})") description("(${cryptoDeviceInfo.deviceId})")
} }
if (!cryptoDeviceInfo.isVerified) { if (isMine && !currentSessionIsTrusted) {
// Add complete security
dividerItem {
id("completeSecurityDiv")
}
bottomSheetVerificationActionItem {
id("completeSecurity")
title(stringProvider.getString(R.string.crosssigning_verify_this_session))
titleColor(colorProvider.getColor(R.color.riotx_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_accent))
listener {
callback?.onAction(DevicesAction.CompleteSecurity)
}
}
} else if (!isMine) {
if (currentSessionIsTrusted) {
// we can propose to verify it
val isVerified = cryptoDeviceInfo.trustLevel?.crossSigningVerified.orFalse()
if (!isVerified) {
addVerifyActions(cryptoDeviceInfo)
}
}
}
}
private fun handleE2EInLegacy(isMine: Boolean, cryptoDeviceInfo: CryptoDeviceInfo, shield: Int) {
// ==== Legacy
// TRUST INFO SECTION
if (cryptoDeviceInfo.trustLevel?.isLocallyVerified() == true) {
genericItem {
id("trust${cryptoDeviceInfo.deviceId}")
style(GenericItem.STYLE.BIG_TEXT)
titleIconResourceId(shield)
title(stringProvider.getString(R.string.encryption_information_verified))
description(stringProvider.getString(R.string.settings_active_sessions_verified_device_desc))
}
} else {
genericItem {
id("trust${cryptoDeviceInfo.deviceId}")
titleIconResourceId(shield)
style(GenericItem.STYLE.BIG_TEXT)
title(stringProvider.getString(R.string.encryption_information_not_verified))
description(stringProvider.getString(R.string.settings_active_sessions_unverified_device_desc))
}
}
// DEVICE INFO SECTION
genericItem {
id("info${cryptoDeviceInfo.deviceId}")
title(cryptoDeviceInfo.displayName() ?: "")
description("(${cryptoDeviceInfo.deviceId})")
}
// ACTIONS
if (!isMine) {
// if it's not the current device you can trigger a verification
dividerItem { dividerItem {
id("d1") id("d1")
} }
bottomSheetVerificationActionItem { bottomSheetVerificationActionItem {
id("verify") id("verify${cryptoDeviceInfo.deviceId}")
title(stringProvider.getString(R.string.verification_verify_device)) title(stringProvider.getString(R.string.verification_verify_device))
titleColor(colorProvider.getColor(R.color.riotx_accent)) titleColor(colorProvider.getColor(R.color.riotx_accent))
iconRes(R.drawable.ic_arrow_right) iconRes(R.drawable.ic_arrow_right)
@ -77,11 +205,43 @@ class DeviceVerificationInfoEpoxyController @Inject constructor(private val stri
} }
} }
} }
}
if (cryptoDeviceInfo.deviceId != session.sessionParams.credentials.deviceId) { private fun addVerifyActions(cryptoDeviceInfo: CryptoDeviceInfo) {
dividerItem {
id("verifyDiv")
}
bottomSheetVerificationActionItem {
id("verify_text")
title(stringProvider.getString(R.string.cross_signing_verify_by_text))
titleColor(colorProvider.getColor(R.color.riotx_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_accent))
listener {
callback?.onAction(DevicesAction.VerifyMyDeviceManually(cryptoDeviceInfo.deviceId))
}
}
dividerItem {
id("verifyDiv2")
}
bottomSheetVerificationActionItem {
id("verify_emoji")
title(stringProvider.getString(R.string.cross_signing_verify_by_emoji))
titleColor(colorProvider.getColor(R.color.riotx_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_accent))
listener {
callback?.onAction(DevicesAction.VerifyMyDevice(cryptoDeviceInfo.deviceId))
}
}
}
private fun addGenericDeviceManageActions(data: DeviceVerificationInfoBottomSheetViewState, deviceId: String) {
// Offer delete session if not me
if (!data.isMine) {
// Add the delete option // Add the delete option
dividerItem { dividerItem {
id("d2") id("manageD1")
} }
bottomSheetVerificationActionItem { bottomSheetVerificationActionItem {
id("delete") id("delete")
@ -90,13 +250,14 @@ class DeviceVerificationInfoEpoxyController @Inject constructor(private val stri
iconRes(R.drawable.ic_arrow_right) iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_destructive_accent)) iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
listener { listener {
callback?.onAction(DevicesAction.Delete(cryptoDeviceInfo.deviceId)) callback?.onAction(DevicesAction.Delete(deviceId))
} }
} }
} }
// Always offer rename
dividerItem { dividerItem {
id("d3") id("manageD2")
} }
bottomSheetVerificationActionItem { bottomSheetVerificationActionItem {
id("rename") id("rename")
@ -105,43 +266,25 @@ class DeviceVerificationInfoEpoxyController @Inject constructor(private val stri
iconRes(R.drawable.ic_arrow_right) iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
listener { listener {
callback?.onAction(DevicesAction.PromptRename(cryptoDeviceInfo.deviceId)) callback?.onAction(DevicesAction.PromptRename(deviceId))
} }
} }
} else if (data?.deviceInfo?.invoke() != null) { }
val info = data.deviceInfo.invoke()
private fun handleNonE2EDevice(data: DeviceVerificationInfoBottomSheetViewState) {
val info = data.deviceInfo.invoke() ?: return
genericItem { genericItem {
id("info${info?.deviceId}") id("info${info.deviceId}")
title(info?.displayName ?: "") title(info.displayName ?: "")
description("(${info?.deviceId})") description("(${info.deviceId})")
} }
genericFooterItem { genericFooterItem {
id("infoCrypto${info?.deviceId}") id("infoCrypto${info.deviceId}")
text(stringProvider.getString(R.string.settings_failed_to_get_crypto_device_info)) text(stringProvider.getString(R.string.settings_failed_to_get_crypto_device_info))
} }
if (info?.deviceId != session.sessionParams.credentials.deviceId) { info.deviceId?.let { addGenericDeviceManageActions(data, it) }
// Add the delete option
dividerItem {
id("d2")
}
bottomSheetVerificationActionItem {
id("delete")
title(stringProvider.getString(R.string.settings_active_sessions_signout_device))
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
listener {
callback?.onAction(DevicesAction.Delete(info?.deviceId ?: ""))
}
}
}
} else {
loadingItem {
id("loading")
}
}
} }
interface Callback { interface Callback {

View File

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

View File

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

View File

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

View File

@ -16,6 +16,7 @@
package im.vector.riotx.features.settings.devices package im.vector.riotx.features.settings.devices
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.FragmentViewModelContext
@ -28,33 +29,46 @@ import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.NoOpMatrixCallback
import im.vector.matrix.android.api.extensions.tryThis
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod
import im.vector.matrix.android.api.session.crypto.verification.VerificationService import im.vector.matrix.android.api.session.crypto.verification.VerificationService
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse import im.vector.matrix.android.internal.util.awaitCallback
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.crypto.verification.SupportedVerificationMethodsProvider import io.reactivex.Observable
import io.reactivex.functions.BiFunction
import io.reactivex.subjects.PublishSubject
import kotlinx.coroutines.launch
import java.util.concurrent.TimeUnit
data class DevicesViewState( data class DevicesViewState(
val myDeviceId: String = "", val myDeviceId: String = "",
val devices: Async<List<DeviceInfo>> = Uninitialized, // val devices: Async<List<DeviceInfo>> = Uninitialized,
val cryptoDevices: Async<List<CryptoDeviceInfo>> = Uninitialized, // val cryptoDevices: Async<List<CryptoDeviceInfo>> = Uninitialized,
val devices: Async<List<DeviceFullInfo>> = Uninitialized,
// TODO Replace by isLoading boolean // TODO Replace by isLoading boolean
val request: Async<Unit> = Uninitialized val request: Async<Unit> = Uninitialized,
val hasAccountCrossSigning: Boolean = false,
val accountCrossSigningIsTrusted: Boolean = false
) : MvRxState ) : MvRxState
data class DeviceFullInfo(
val deviceInfo: DeviceInfo,
val cryptoDeviceInfo: CryptoDeviceInfo?
)
class DevicesViewModel @AssistedInject constructor( class DevicesViewModel @AssistedInject constructor(
@Assisted initialState: DevicesViewState, @Assisted initialState: DevicesViewState,
private val session: Session, private val session: Session
private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider) ) : VectorViewModel<DevicesViewState, DevicesAction, DevicesViewEvents>(initialState), VerificationService.Listener {
: VectorViewModel<DevicesViewState, DevicesAction, DevicesViewEvents>(initialState), VerificationService.Listener {
@AssistedInject.Factory @AssistedInject.Factory
interface Factory { interface Factory {
@ -74,16 +88,76 @@ class DevicesViewModel @AssistedInject constructor(
private var _currentDeviceId: String? = null private var _currentDeviceId: String? = null
private var _currentSession: String? = null private var _currentSession: String? = null
init { private val refreshPublisher: PublishSubject<Unit> = PublishSubject.create()
refreshDevicesList()
session.cryptoService().verificationService().addListener(this)
session.rx().liveUserCryptoDevices(session.myUserId) init {
.execute {
setState {
copy( copy(
cryptoDevices = it hasAccountCrossSigning = session.cryptoService().crossSigningService().getMyCrossSigningKeys() != null,
accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified(),
myDeviceId = session.sessionParams.credentials.deviceId ?: ""
) )
} }
Observable.combineLatest<List<CryptoDeviceInfo>, List<DeviceInfo>, List<DeviceFullInfo>>(
session.rx().liveUserCryptoDevices(session.myUserId),
session.rx().liveMyDeviceInfo(),
BiFunction { cryptoList, infoList ->
infoList
.sortedByDescending { it.lastSeenTs }
.map { deviceInfo ->
val cryptoDeviceInfo = cryptoList.firstOrNull { it.deviceId == deviceInfo.deviceId }
DeviceFullInfo(deviceInfo, cryptoDeviceInfo)
}
}
)
.distinct()
.execute { async ->
copy(
devices = async
)
}
session.rx().liveCrossSigningInfo(session.myUserId)
.execute {
copy(
hasAccountCrossSigning = it.invoke()?.getOrNull() != null,
accountCrossSigningIsTrusted = it.invoke()?.getOrNull()?.isTrusted() == true
)
}
session.cryptoService().verificationService().addListener(this)
// session.rx().liveMyDeviceInfo()
// .execute {
// copy(
// devices = it
// )
// }
session.rx().liveUserCryptoDevices(session.myUserId)
.distinct()
.throttleLast(5_000, TimeUnit.MILLISECONDS)
.subscribe {
// If we have a new crypto device change, we might want to trigger refresh of device info
session.cryptoService().fetchDevicesList(NoOpMatrixCallback())
}.disposeOnClear()
// session.rx().liveUserCryptoDevices(session.myUserId)
// .execute {
// copy(
// cryptoDevices = it
// )
// }
refreshPublisher.throttleFirst(4_000, TimeUnit.MILLISECONDS)
.subscribe {
session.cryptoService().fetchDevicesList(NoOpMatrixCallback())
session.cryptoService().downloadKeys(listOf(session.myUserId), true, NoOpMatrixCallback())
}
.disposeOnClear()
// then force download
queryRefreshDevicesList()
} }
override fun onCleared() { override fun onCleared() {
@ -93,7 +167,7 @@ class DevicesViewModel @AssistedInject constructor(
override fun transactionUpdated(tx: VerificationTransaction) { override fun transactionUpdated(tx: VerificationTransaction) {
if (tx.state == VerificationTxState.Verified) { if (tx.state == VerificationTxState.Verified) {
refreshDevicesList() queryRefreshDevicesList()
} }
} }
@ -102,91 +176,66 @@ class DevicesViewModel @AssistedInject constructor(
* The devices list is the list of the devices where the user is logged in. * The devices list is the list of the devices where the user is logged in.
* It can be any mobile devices, and any browsers. * It can be any mobile devices, and any browsers.
*/ */
private fun refreshDevicesList() { private fun queryRefreshDevicesList() {
if (!session.sessionParams.credentials.deviceId.isNullOrEmpty()) { refreshPublisher.onNext(Unit)
// display something asap
val localKnown = session.cryptoService().getUserDevices(session.myUserId).map {
DeviceInfo(
user_id = session.myUserId,
deviceId = it.deviceId,
displayName = it.displayName()
)
}
setState {
copy(
// Keep known list if we have it, and let refresh go in backgroung
devices = this.devices.takeIf { it is Success } ?: Success(localKnown)
)
}
session.cryptoService().getDevicesList(object : MatrixCallback<DevicesListResponse> {
override fun onSuccess(data: DevicesListResponse) {
setState {
copy(
myDeviceId = session.sessionParams.credentials.deviceId ?: "",
devices = Success(data.devices.orEmpty())
)
}
}
override fun onFailure(failure: Throwable) {
setState {
copy(
devices = Fail(failure)
)
}
}
})
// Put cached state
setState {
copy(
myDeviceId = session.sessionParams.credentials.deviceId ?: "",
cryptoDevices = Success(session.cryptoService().getUserDevices(session.myUserId))
)
}
// then force download
session.cryptoService().downloadKeys(listOf(session.myUserId), true, object : MatrixCallback<MXUsersDevicesMap<CryptoDeviceInfo>> {
override fun onSuccess(data: MXUsersDevicesMap<CryptoDeviceInfo>) {
setState {
copy(
cryptoDevices = Success(session.cryptoService().getUserDevices(session.myUserId))
)
}
}
})
} else {
// Should not happen
}
} }
override fun handle(action: DevicesAction) { override fun handle(action: DevicesAction) {
return when (action) { return when (action) {
is DevicesAction.Retry -> refreshDevicesList() is DevicesAction.Refresh -> queryRefreshDevicesList()
is DevicesAction.Delete -> handleDelete(action) is DevicesAction.Delete -> handleDelete(action)
is DevicesAction.Password -> handlePassword(action) is DevicesAction.Password -> handlePassword(action)
is DevicesAction.Rename -> handleRename(action) is DevicesAction.Rename -> handleRename(action)
is DevicesAction.PromptRename -> handlePromptRename(action) is DevicesAction.PromptRename -> handlePromptRename(action)
is DevicesAction.VerifyMyDevice -> handleVerify(action) is DevicesAction.VerifyMyDevice -> handleInteractiveVerification(action)
is DevicesAction.CompleteSecurity -> handleCompleteSecurity()
is DevicesAction.MarkAsManuallyVerified -> handleVerifyManually(action)
is DevicesAction.VerifyMyDeviceManually -> handleShowDeviceCryptoInfo(action)
} }
} }
private fun handleVerify(action: DevicesAction.VerifyMyDevice) { private fun handleInteractiveVerification(action: DevicesAction.VerifyMyDevice) {
val txID = session.cryptoService() val txID = session.cryptoService()
.verificationService() .verificationService()
.requestKeyVerification(supportedVerificationMethodsProvider.provide(), session.myUserId, listOf(action.deviceId)) .beginKeyVerification(VerificationMethod.SAS, session.myUserId, action.deviceId, null)
_viewEvents.post(DevicesViewEvents.ShowVerifyDevice( _viewEvents.post(DevicesViewEvents.ShowVerifyDevice(
session.myUserId, session.myUserId,
txID.transactionId txID
)) ))
} }
private fun handleShowDeviceCryptoInfo(action: DevicesAction.VerifyMyDeviceManually) = withState { state ->
state.devices.invoke()
?.firstOrNull { it.cryptoDeviceInfo?.deviceId == action.deviceId }
?.let {
_viewEvents.post(DevicesViewEvents.ShowManuallyVerify(it.cryptoDeviceInfo!!))
}
}
private fun handleVerifyManually(action: DevicesAction.MarkAsManuallyVerified) = withState { state ->
viewModelScope.launch {
if (state.hasAccountCrossSigning) {
awaitCallback<Unit> {
tryThis { session.cryptoService().crossSigningService().trustDevice(action.cryptoDeviceInfo.deviceId, it) }
}
} else {
// legacy
session.cryptoService().setDeviceVerification(
DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true),
action.cryptoDeviceInfo.userId,
action.cryptoDeviceInfo.deviceId)
}
}
}
private fun handleCompleteSecurity() {
_viewEvents.post(DevicesViewEvents.SelfVerification(session))
}
private fun handlePromptRename(action: DevicesAction.PromptRename) = withState { state -> private fun handlePromptRename(action: DevicesAction.PromptRename) = withState { state ->
val info = state.devices.invoke()?.firstOrNull { it.deviceId == action.deviceId } val info = state.devices.invoke()?.firstOrNull { it.deviceInfo.deviceId == action.deviceId }
if (info != null) { if (info != null) {
_viewEvents.post(DevicesViewEvents.PromptRenameDevice(info)) _viewEvents.post(DevicesViewEvents.PromptRenameDevice(info.deviceInfo))
} }
} }
@ -199,7 +248,7 @@ class DevicesViewModel @AssistedInject constructor(
) )
} }
// force settings update // force settings update
refreshDevicesList() queryRefreshDevicesList()
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
@ -270,7 +319,7 @@ class DevicesViewModel @AssistedInject constructor(
) )
} }
// force settings update // force settings update
refreshDevicesList() queryRefreshDevicesList()
} }
}) })
} }
@ -299,7 +348,7 @@ class DevicesViewModel @AssistedInject constructor(
) )
} }
// force settings update // force settings update
refreshDevicesList() queryRefreshDevicesList()
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {

View File

@ -0,0 +1,56 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.settings.devices
import androidx.annotation.DrawableRes
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import im.vector.riotx.R
object TrustUtils {
@DrawableRes
fun shieldForTrust(currentDevice: Boolean, trustMSK: Boolean, legacyMode: Boolean, deviceTrustLevel: DeviceTrustLevel?): Int {
return when {
currentDevice -> {
if (legacyMode) {
// In legacy, current session is always trusted
R.drawable.ic_shield_trusted
} else {
// If current session doesn't trust MSK, show red shield for current device
R.drawable.ic_shield_trusted.takeIf { trustMSK } ?: R.drawable.ic_shield_warning
}
}
else -> {
if (legacyMode) {
// use local trust
R.drawable.ic_shield_trusted.takeIf { deviceTrustLevel?.locallyVerified == true } ?: R.drawable.ic_shield_warning
} else {
if (trustMSK) {
// use cross sign trust, put locally trusted in black
R.drawable.ic_shield_trusted.takeIf { deviceTrustLevel?.crossSigningVerified == true }
?: R.drawable.ic_shield_black.takeIf { deviceTrustLevel?.locallyVerified == true }
?: R.drawable.ic_shield_warning
} else {
// The current session is untrusted, so displays others in black
// as we can't know the cross-signing state
R.drawable.ic_shield_black
}
}
}
}
}
}

View File

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

View File

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

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