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

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 ensure that your using the project formatting rules (which are in the project at .idea/codeStyles/), and format the file before committing them.
### Template
An Android Studio template has been added to the project to help creating all files needed when adding a new screen to the application. Fragment, ViewModel, Activity, etc.
To install the template (to be done only once):
- Go to folder `./tools/template`.
- Run the script `./configure.sh`.
- Restart Android Studio.
To create a new screen:
- First create a new package in your code.
- Then right click on the package, and select `New/New Vector/RiotX Feature`.
- Follow the Wizard, especially replace `Main` by something more relevant to your feature.
- Click on `Finish`.
- Remainning steps are described as TODO in the generated files, or will be pointed out by the compilator, or at runtime :)
Note that if the templates are modified, the only things to do is to restart Android Studio for the change to take effect.
## Compilation
For now, the Matrix SDK and the RiotX application are in the same project. So there is no specific thing to do, this project should compile without any special action.

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

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

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

View File

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

View File

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

View File

@ -46,10 +46,10 @@ data class RoomSummary constructor(
val readMarkerId: String? = null,
val userDrafts: List<UserDraft> = emptyList(),
val isEncrypted: Boolean,
val encryptionEventTs: Long?,
val inviterId: String? = null,
val typingRoomMemberIds: List<String> = emptyList(),
val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS,
// TODO Plug it
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.failure.Failure
import im.vector.matrix.android.api.failure.toRegistrationFlowResponse
import im.vector.matrix.android.internal.auth.AuthAPI
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
@ -39,25 +39,9 @@ internal class DefaultRegisterTask(
apiCall = authAPI.register(params.registrationParams)
}
} catch (throwable: Throwable) {
if (throwable is Failure.OtherServerError && throwable.httpCode == 401) {
// Parse to get a RegistrationFlowResponse
val registrationFlowResponse = try {
MoshiProvider.providesMoshi()
.adapter(RegistrationFlowResponse::class.java)
.fromJson(throwable.errorBody)
} catch (e: Exception) {
null
}
// check if the server response can be cast
if (registrationFlowResponse != null) {
throw Failure.RegistrationFlowError(registrationFlowResponse)
} else {
throw throwable
}
} else {
// Other error
throw throwable
}
throw throwable.toRegistrationFlowResponse()
?.let { Failure.RegistrationFlowError(it) }
?: throwable
}
}
}

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

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

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)
null
}
}
},
firstTimeSeenLocalTs = deviceInfoEntity.firstTimeSeenLocalTs
)
}
}

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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)
: BaseMvRxViewModel<S>(initialState, false) {
interface Factory<S: MvRxState> {
fun create(state: S): BaseMvRxViewModel<S>
}
// Used to post transient events to the View
protected val _viewEvents = PublishDataSource<VE>()
val viewEvents: DataSource<VE> = _viewEvents

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

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

View File

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

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

View File

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

View File

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

View File

@ -165,7 +165,9 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
} else {
otherUserShield.setImageResource(R.drawable.ic_shield_warning)
}
otherUserNameText.text = getString(R.string.complete_security)
otherUserNameText.text = getString(
if (state.selfVerificationMode) R.string.crosssigning_verify_this_session else R.string.crosssigning_verify_session
)
otherUserShield.isVisible = true
} else {
avatarRenderer.render(matrixItem, otherUserAvatarImageView)
@ -241,7 +243,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
}
when (state.qrTransactionState) {
is VerificationTxState.QrScannedByOther -> {
is VerificationTxState.QrScannedByOther -> {
showFragment(VerificationQrScannedByOtherFragment::class, Bundle())
return@withState
}
@ -252,19 +254,19 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
})
return@withState
}
is VerificationTxState.Verified -> {
is VerificationTxState.Verified -> {
showFragment(VerificationConclusionFragment::class, Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(true, null, state.isMe))
})
return@withState
}
is VerificationTxState.Cancelled -> {
is VerificationTxState.Cancelled -> {
showFragment(VerificationConclusionFragment::class, Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(false, state.qrTransactionState.cancelCode.value, state.isMe))
})
return@withState
}
else -> Unit
else -> Unit
}
// At this point there is no SAS transaction for this request

View File

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

View File

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

View File

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

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

View File

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

View File

@ -18,19 +18,23 @@
package im.vector.riotx.features.home.room.detail.timeline.helper
import im.vector.matrix.android.api.extensions.orFalse
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.model.ReferencesAggregatedContent
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.matrix.android.internal.session.room.VerificationState
import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.core.extensions.localDateTime
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.utils.getColorFromUserId
import im.vector.riotx.features.home.room.detail.timeline.item.E2EDecoration
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.home.room.detail.timeline.item.PollResponseData
import im.vector.riotx.features.home.room.detail.timeline.item.ReactionInfoData
@ -72,6 +76,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId))
}
val room = event.root.roomId?.let { session.getRoom(it) }
val e2eDecoration = getE2EDecoration(room, event)
return MessageInformationData(
eventId = eventId,
senderId = event.root.senderId ?: "",
@ -111,10 +117,59 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
?: VerificationState.REQUEST
ReferencesInfoData(verificationState)
},
sentByMe = event.root.senderId == session.myUserId
sentByMe = event.root.senderId == session.myUserId,
e2eDecoration = e2eDecoration
)
}
private fun getE2EDecoration(room: Room?, event: TimelineEvent): E2EDecoration {
return if (room?.isEncrypted() == true
// is user verified
&& session.cryptoService().crossSigningService().getUserCrossSigningKeys(event.root.senderId ?: "")?.isTrusted() == true) {
val ts = room.roomSummary()?.encryptionEventTs ?: 0
val eventTs = event.root.originServerTs ?: 0
if (event.isEncrypted()) {
// Do not decorate failed to decrypt, or redaction (we lost sender device info)
if (event.root.getClearType() == EventType.ENCRYPTED || event.root.isRedacted()) {
E2EDecoration.NONE
} else {
val sendingDevice = event.root.content
.toModel<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
* even if same sender

View File

@ -92,6 +92,18 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
holder.reactionsContainer.setOnLongClickListener(baseAttributes.itemLongClickListener)
}
when (baseAttributes.informationData.e2eDecoration) {
E2EDecoration.NONE -> {
holder.e2EDecorationView.isVisible = false
}
E2EDecoration.WARN_IN_CLEAR,
E2EDecoration.WARN_SENT_BY_UNVERIFIED,
E2EDecoration.WARN_SENT_BY_UNKNOWN -> {
holder.e2EDecorationView.setImageResource(R.drawable.ic_shield_warning)
holder.e2EDecorationView.isVisible = true
}
}
holder.view.setOnClickListener(baseAttributes.itemClickListener)
holder.view.setOnLongClickListener(baseAttributes.itemLongClickListener)
}
@ -110,6 +122,7 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
abstract class Holder(@IdRes stubId: Int) : BaseEventItem.BaseHolder(stubId) {
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 readReceipts: List<ReadReceiptData> = emptyList(),
val referencesInfoData: ReferencesInfoData? = null,
val sentByMe : Boolean
val sentByMe : Boolean,
val e2eDecoration: E2EDecoration = E2EDecoration.NONE
) : Parcelable {
val matrixItem: MatrixItem
@ -75,4 +76,11 @@ data class PollResponseData(
val isClosed: Boolean = false
) : Parcelable
enum class E2EDecoration {
NONE,
WARN_IN_CLEAR,
WARN_SENT_BY_UNVERIFIED,
WARN_SENT_BY_UNKNOWN
}
fun ReadReceiptData.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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