Merge branch 'release/1.1.2'

This commit is contained in:
Benoit Marty 2021-03-16 22:08:39 +01:00
commit ab260e79eb
45 changed files with 668 additions and 441 deletions

View File

@ -7,27 +7,27 @@ assignees: ''
--- ---
**Describe the bug** #### Describe the bug
A clear and concise description of what the bug is. A clear and concise description of what the bug is.
**To Reproduce** #### To Reproduce
Steps to reproduce the behavior: Steps to reproduce the behavior:
1. Go to '...' 1. Go to '...'
2. Click on '....' 2. Click on '....'
3. Scroll down to '....' 3. Scroll down to '....'
4. See error 4. See error
**Expected behavior** #### Expected behavior
A clear and concise description of what you expected to happen. A clear and concise description of what you expected to happen.
**Screenshots** #### Screenshots
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
**Smartphone (please complete the following information):** #### Smartphone (please complete the following information):
- Device: [e.g. Samsung S6] - Device: [e.g. Samsung S6]
- OS: [e.g. Android 6.0] - OS: [e.g. Android 6.0]
**Additional context** #### Additional context
- App version and store [e.g. 1.0.0 - F-Droid] - App version and store [e.g. 1.0.0 - F-Droid]
- Homeserver: [e.g. matrix.org] - Homeserver: [e.g. matrix.org]

View File

@ -7,14 +7,14 @@ assignees: ''
--- ---
**Is your feature request related to a problem? Please describe.** #### Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like** #### Describe the solution you'd like.
A clear and concise description of what you want to happen. A clear and concise description of what you want to happen.
**Describe alternatives you've considered** #### Describe alternatives you've considered.
A clear and concise description of any alternative solutions or features you've considered. A clear and concise description of any alternative solutions or features you've considered.
**Additional context** #### Additional context
Add any other context or screenshots about the feature request here. Add any other context or screenshots about the feature request here.

View File

@ -1,8 +1,21 @@
Changes in Element 1.1.1 (2021-XX-XX) Changes in Element 1.1.2 (2021-03-16)
=================================================== ===================================================
Features ✨: Improvements 🙌:
- - Lazy storage of ReadReceipts
- Do not load room members in e2e after init sync
Bugfix 🐛:
- Add option to cancel stuck messages at bottom of timeline see #516
- Ensure message are decrypted in the room list after a clear cache
- Regression: Video will not play upon tap, but only after swipe #2928
- Cross signing now works with servers with an explicit port in the servername
Other changes:
- Change formatting on issue templates to proper headings.
Changes in Element 1.1.1 (2021-03-10)
===================================================
Improvements 🙌: Improvements 🙌:
- Allow non-HTTPS connections to homeservers on Tor (#2941) - Allow non-HTTPS connections to homeservers on Tor (#2941)
@ -24,16 +37,10 @@ Bugfix 🐛:
Translations 🗣: Translations 🗣:
- All string resources and translations have been moved to the application module. Weblate project for the SDK will be removed. - All string resources and translations have been moved to the application module. Weblate project for the SDK will be removed.
SDK API changes ⚠️:
-
Build 🧱: Build 🧱:
- Update a lot of dependencies, with the help of dependabot. - Update a lot of dependencies, with the help of dependabot.
- Add a script to download and install APK from the CI - Add a script to download and install APK from the CI
Test:
-
Other changes: Other changes:
- Rework edition of event management - Rework edition of event management

View File

@ -0,0 +1,2 @@
Main changes in this version: performance improvement and bug fixes!
Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.1.2

View File

@ -38,7 +38,7 @@ internal class CryptoSessionInfoProvider @Inject constructor(
val encryptionEvent = monarchy.fetchCopied { realm -> val encryptionEvent = monarchy.fetchCopied { realm ->
EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION) EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
.contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"") .contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
.isNotNull(EventEntityFields.STATE_KEY) // should be an empty key .isEmpty(EventEntityFields.STATE_KEY)
.findFirst() .findFirst()
} }
return encryptionEvent != null return encryptionEvent != null

View File

@ -856,15 +856,8 @@ internal class DefaultCryptoService @Inject constructor(
return return
} }
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
val params = LoadRoomMembersTask.Params(roomId) val userIds = getRoomUserIds(roomId)
try { setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds)
loadRoomMembersTask.execute(params)
} catch (throwable: Throwable) {
Timber.e(throwable, "## CRYPTO | onRoomEncryptionEvent ERROR FAILED TO SETUP CRYPTO ")
} finally {
val userIds = getRoomUserIds(roomId)
setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds)
}
} }
} }

View File

@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.crypto package org.matrix.android.sdk.internal.crypto
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel
@ -28,7 +29,7 @@ import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.sync.SyncTokenStore import org.matrix.android.sdk.internal.session.sync.SyncTokenStore
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.launch import org.matrix.android.sdk.internal.util.logLimit
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -39,8 +40,9 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
private val syncTokenStore: SyncTokenStore, private val syncTokenStore: SyncTokenStore,
private val credentials: Credentials, private val credentials: Credentials,
private val downloadKeysForUsersTask: DownloadKeysForUsersTask, private val downloadKeysForUsersTask: DownloadKeysForUsersTask,
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
coroutineDispatchers: MatrixCoroutineDispatchers, coroutineDispatchers: MatrixCoroutineDispatchers,
taskExecutor: TaskExecutor) { private val taskExecutor: TaskExecutor) {
interface UserDevicesUpdateListener { interface UserDevicesUpdateListener {
fun onUsersDeviceUpdate(userIds: List<String>) fun onUsersDeviceUpdate(userIds: List<String>)
@ -75,8 +77,10 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
// HS not ready for retry // HS not ready for retry
private val notReadyToRetryHS = mutableSetOf<String>() private val notReadyToRetryHS = mutableSetOf<String>()
private val cryptoCoroutineContext = coroutineDispatchers.crypto
init { init {
taskExecutor.executorScope.launch(coroutineDispatchers.crypto) { taskExecutor.executorScope.launch(cryptoCoroutineContext) {
var isUpdated = false var isUpdated = false
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
for ((userId, status) in deviceTrackingStatuses) { for ((userId, status) in deviceTrackingStatuses) {
@ -104,7 +108,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
if (':' in userId) { if (':' in userId) {
try { try {
synchronized(notReadyToRetryHS) { synchronized(notReadyToRetryHS) {
res = !notReadyToRetryHS.contains(userId.substringAfterLast(':')) res = !notReadyToRetryHS.contains(userId.substringAfter(':'))
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "## CRYPTO | canRetryKeysDownload() failed") Timber.e(e, "## CRYPTO | canRetryKeysDownload() failed")
@ -123,28 +127,37 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
} }
} }
fun onRoomMembersLoadedFor(roomId: String) {
taskExecutor.executorScope.launch(cryptoCoroutineContext) {
if (cryptoSessionInfoProvider.isRoomEncrypted(roomId)) {
// It's OK to track also device for invited users
val userIds = cryptoSessionInfoProvider.getRoomUserIds(roomId, true)
startTrackingDeviceList(userIds)
refreshOutdatedDeviceLists()
}
}
}
/** /**
* Mark the cached device list for the given user outdated * Mark the cached device list for the given user outdated
* flag the given user for device-list tracking, if they are not already. * flag the given user for device-list tracking, if they are not already.
* *
* @param userIds the user ids list * @param userIds the user ids list
*/ */
fun startTrackingDeviceList(userIds: List<String>?) { fun startTrackingDeviceList(userIds: List<String>) {
if (null != userIds) { var isUpdated = false
var isUpdated = false val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
for (userId in userIds) { for (userId in userIds) {
if (!deviceTrackingStatuses.containsKey(userId) || TRACKING_STATUS_NOT_TRACKED == deviceTrackingStatuses[userId]) { if (!deviceTrackingStatuses.containsKey(userId) || TRACKING_STATUS_NOT_TRACKED == deviceTrackingStatuses[userId]) {
Timber.v("## CRYPTO | startTrackingDeviceList() : Now tracking device list for $userId") Timber.v("## CRYPTO | startTrackingDeviceList() : Now tracking device list for $userId")
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
isUpdated = true isUpdated = true
}
} }
}
if (isUpdated) { if (isUpdated) {
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
}
} }
} }
@ -155,13 +168,17 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
* @param left the user ids list which left a room * @param left the user ids list which left a room
*/ */
fun handleDeviceListsChanges(changed: Collection<String>, left: Collection<String>) { fun handleDeviceListsChanges(changed: Collection<String>, left: Collection<String>) {
Timber.v("## CRYPTO: handleDeviceListsChanges changed:$changed / left:$left") Timber.v("## CRYPTO: handleDeviceListsChanges changed: ${changed.logLimit()} / left: ${left.logLimit()}")
var isUpdated = false var isUpdated = false
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
if (changed.isNotEmpty() || left.isNotEmpty()) {
clearUnavailableServersList()
}
for (userId in changed) { for (userId in changed) {
if (deviceTrackingStatuses.containsKey(userId)) { if (deviceTrackingStatuses.containsKey(userId)) {
Timber.v("## CRYPTO | invalidateUserDeviceList() : Marking device list outdated for $userId") Timber.v("## CRYPTO | handleDeviceListsChanges() : Marking device list outdated for $userId")
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
isUpdated = true isUpdated = true
} }
@ -169,7 +186,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
for (userId in left) { for (userId in left) {
if (deviceTrackingStatuses.containsKey(userId)) { if (deviceTrackingStatuses.containsKey(userId)) {
Timber.v("## CRYPTO | invalidateUserDeviceList() : No longer tracking device list for $userId") Timber.v("## CRYPTO | handleDeviceListsChanges() : No longer tracking device list for $userId")
deviceTrackingStatuses[userId] = TRACKING_STATUS_NOT_TRACKED deviceTrackingStatuses[userId] = TRACKING_STATUS_NOT_TRACKED
isUpdated = true isUpdated = true
} }
@ -307,7 +324,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
* @param downloadUsers the user ids list * @param downloadUsers the user ids list
*/ */
private suspend fun doKeyDownloadForUsers(downloadUsers: List<String>): MXUsersDevicesMap<CryptoDeviceInfo> { private suspend fun doKeyDownloadForUsers(downloadUsers: List<String>): MXUsersDevicesMap<CryptoDeviceInfo> {
Timber.v("## CRYPTO | doKeyDownloadForUsers() : doKeyDownloadForUsers $downloadUsers") Timber.v("## CRYPTO | doKeyDownloadForUsers() : doKeyDownloadForUsers ${downloadUsers.logLimit()}")
// get the user ids which did not already trigger a keys download // get the user ids which did not already trigger a keys download
val filteredUsers = downloadUsers.filter { MatrixPatterns.isUserId(it) } val filteredUsers = downloadUsers.filter { MatrixPatterns.isUserId(it) }
if (filteredUsers.isEmpty()) { if (filteredUsers.isEmpty()) {

View File

@ -312,7 +312,7 @@ internal class MXOlmDevice @Inject constructor(
* @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device.
* @return a list of known session ids for the device. * @return a list of known session ids for the device.
*/ */
fun getSessionIds(theirDeviceIdentityKey: String): Set<String>? { fun getSessionIds(theirDeviceIdentityKey: String): List<String>? {
return store.getDeviceSessionIds(theirDeviceIdentityKey) return store.getDeviceSessionIds(theirDeviceIdentityKey)
} }

View File

@ -154,7 +154,7 @@ internal class MXOlmDecryption(
* @return payload, if decrypted successfully. * @return payload, if decrypted successfully.
*/ */
private fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? { private fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? {
val sessionIds = olmDevice.getSessionIds(theirDeviceIdentityKey) ?: emptySet() val sessionIds = olmDevice.getSessionIds(theirDeviceIdentityKey).orEmpty()
val messageBody = message["body"] as? String ?: return null val messageBody = message["body"] as? String ?: return null
val messageType = when (val typeAsVoid = message["type"]) { val messageType = when (val typeAsVoid = message["type"]) {

View File

@ -33,15 +33,18 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMapper
import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields
import org.matrix.android.sdk.internal.database.awaitTransaction
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.CryptoDatabase import org.matrix.android.sdk.internal.di.CryptoDatabase
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.SessionComponent import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
import org.matrix.android.sdk.internal.util.logLimit
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import timber.log.Timber import timber.log.Timber
@ -65,11 +68,16 @@ internal class UpdateTrustWorker(context: Context,
@Inject lateinit var crossSigningService: DefaultCrossSigningService @Inject lateinit var crossSigningService: DefaultCrossSigningService
// It breaks the crypto store contract, but we need to batch things :/ // It breaks the crypto store contract, but we need to batch things :/
@CryptoDatabase @Inject lateinit var realmConfiguration: RealmConfiguration @CryptoDatabase
@UserId @Inject lateinit var myUserId: String @Inject lateinit var cryptoRealmConfiguration: RealmConfiguration
@SessionDatabase
@Inject lateinit var sessionRealmConfiguration: RealmConfiguration
@UserId
@Inject lateinit var myUserId: String
@Inject lateinit var crossSigningKeysMapper: CrossSigningKeysMapper @Inject lateinit var crossSigningKeysMapper: CrossSigningKeysMapper
@Inject lateinit var updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository @Inject lateinit var updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository
@SessionDatabase @Inject lateinit var sessionRealmConfiguration: RealmConfiguration
// @Inject lateinit var roomSummaryUpdater: RoomSummaryUpdater // @Inject lateinit var roomSummaryUpdater: RoomSummaryUpdater
@Inject lateinit var cryptoStore: IMXCryptoStore @Inject lateinit var cryptoStore: IMXCryptoStore
@ -79,118 +87,114 @@ internal class UpdateTrustWorker(context: Context,
} }
override suspend fun doSafeWork(params: Params): Result { override suspend fun doSafeWork(params: Params): Result {
var userList = params.filename val userList = params.filename
?.let { updateTrustWorkerDataRepository.getParam(it) } ?.let { updateTrustWorkerDataRepository.getParam(it) }
?.userIds ?.userIds
?: params.updatedUserIds.orEmpty() ?: params.updatedUserIds.orEmpty()
if (userList.isEmpty()) { // List should not be empty, but let's avoid go further in case of empty list
// This should not happen, but let's avoid go further in case of empty list if (userList.isNotEmpty()) {
cleanup(params) // Unfortunately we don't have much info on what did exactly changed (is it the cross signing keys of that user,
return Result.success() // or a new device?) So we check all again :/
Timber.d("## CrossSigning - Updating trust for users: ${userList.logLimit()}")
Realm.getInstance(cryptoRealmConfiguration).use { cryptoRealm ->
Realm.getInstance(sessionRealmConfiguration).use {
updateTrust(userList, cryptoRealm)
}
}
} }
// Unfortunately we don't have much info on what did exactly changed (is it the cross signing keys of that user, cleanup(params)
// or a new device?) So we check all again :/ return Result.success()
}
Timber.d("## CrossSigning - Updating trust for $userList")
private suspend fun updateTrust(userListParam: List<String>,
cRealm: Realm) {
var userList = userListParam
var myCrossSigningInfo: MXCrossSigningInfo? = null
// First we check that the users MSK are trusted by mine // First we check that the users MSK are trusted by mine
// After that we check the trust chain for each devices of each users // After that we check the trust chain for each devices of each users
Realm.getInstance(realmConfiguration).use { realm -> awaitTransaction(cryptoRealmConfiguration) { cryptoRealm ->
realm.executeTransaction { // By mapping here to model, this object is not live
// By mapping here to model, this object is not live // I should update it if needed
// I should update it if needed myCrossSigningInfo = getCrossSigningInfo(cryptoRealm, myUserId)
var myCrossSigningInfo = realm.where(CrossSigningInfoEntity::class.java)
.equalTo(CrossSigningInfoEntityFields.USER_ID, myUserId)
.findFirst()?.let { mapCrossSigningInfoEntity(it) }
var myTrustResult: UserTrustResult? = null var myTrustResult: UserTrustResult? = null
if (userList.contains(myUserId)) { if (userList.contains(myUserId)) {
Timber.d("## CrossSigning - Clear all trust as a change on my user was detected") Timber.d("## CrossSigning - Clear all trust as a change on my user was detected")
// i am in the list.. but i don't know exactly the delta of change :/ // i am in the list.. but i don't know exactly the delta of change :/
// If it's my cross signing keys we should refresh all trust // If it's my cross signing keys we should refresh all trust
// do it anyway ? // do it anyway ?
userList = realm.where(CrossSigningInfoEntity::class.java) userList = cryptoRealm.where(CrossSigningInfoEntity::class.java)
.findAll().mapNotNull { it.userId } .findAll()
Timber.d("## CrossSigning - Updating trust for all $userList") .mapNotNull { it.userId }
// check right now my keys and mark it as trusted as other trust depends on it // check right now my keys and mark it as trusted as other trust depends on it
val myDevices = realm.where<UserEntity>() val myDevices = cryptoRealm.where<UserEntity>()
.equalTo(UserEntityFields.USER_ID, myUserId) .equalTo(UserEntityFields.USER_ID, myUserId)
.findFirst() .findFirst()
?.devices ?.devices
?.map { deviceInfo -> ?.map { CryptoMapper.mapToModel(it) }
CryptoMapper.mapToModel(deviceInfo)
}
myTrustResult = crossSigningService.checkSelfTrust(myCrossSigningInfo, myDevices).also {
updateCrossSigningKeysTrust(realm, myUserId, it.isVerified())
// update model reference
myCrossSigningInfo = realm.where(CrossSigningInfoEntity::class.java)
.equalTo(CrossSigningInfoEntityFields.USER_ID, myUserId)
.findFirst()?.let { mapCrossSigningInfoEntity(it) }
}
}
val otherInfos = userList.map { myTrustResult = crossSigningService.checkSelfTrust(myCrossSigningInfo, myDevices)
it to realm.where(CrossSigningInfoEntity::class.java) updateCrossSigningKeysTrust(cryptoRealm, myUserId, myTrustResult.isVerified())
.equalTo(CrossSigningInfoEntityFields.USER_ID, it) // update model reference
.findFirst()?.let { mapCrossSigningInfoEntity(it) } myCrossSigningInfo = getCrossSigningInfo(cryptoRealm, myUserId)
} }
.toMap()
val trusts = otherInfos.map { infoEntry -> val otherInfos = userList.associateWith { userId ->
infoEntry.key to when (infoEntry.key) { getCrossSigningInfo(cryptoRealm, userId)
myUserId -> myTrustResult }
else -> {
crossSigningService.checkOtherMSKTrusted(myCrossSigningInfo, infoEntry.value).also { val trusts = otherInfos.mapValues { entry ->
Timber.d("## CrossSigning - user:${infoEntry.key} result:$it") when (entry.key) {
} myUserId -> myTrustResult
else -> {
crossSigningService.checkOtherMSKTrusted(myCrossSigningInfo, entry.value).also {
Timber.d("## CrossSigning - user:${entry.key} result:$it")
} }
} }
}.toMap() }
}
// TODO! if it's me and my keys has changed... I have to reset trust for everyone! // TODO! if it's me and my keys has changed... I have to reset trust for everyone!
// i have all the new trusts, update DB // i have all the new trusts, update DB
trusts.forEach { trusts.forEach {
val verified = it.value?.isVerified() == true val verified = it.value?.isVerified() == true
updateCrossSigningKeysTrust(realm, it.key, verified) updateCrossSigningKeysTrust(cryptoRealm, it.key, verified)
}
// Ok so now we have to check device trust for all these users..
Timber.v("## CrossSigning - Updating devices cross trust users: ${trusts.keys.logLimit()}")
trusts.keys.forEach { userId ->
val devicesEntities = cryptoRealm.where<UserEntity>()
.equalTo(UserEntityFields.USER_ID, userId)
.findFirst()
?.devices
val trustMap = devicesEntities?.associateWith { device ->
// get up to date from DB has could have been updated
val otherInfo = getCrossSigningInfo(cryptoRealm, userId)
crossSigningService.checkDeviceTrust(myCrossSigningInfo, otherInfo, CryptoMapper.mapToModel(device))
} }
// Ok so now we have to check device trust for all these users.. // Update trust if needed
Timber.v("## CrossSigning - Updating devices cross trust users ${trusts.keys}") devicesEntities?.forEach { device ->
trusts.keys.forEach { val crossSignedVerified = trustMap?.get(device)?.isCrossSignedVerified()
val devicesEntities = realm.where<UserEntity>() Timber.d("## CrossSigning - Trust for ${device.userId}|${device.deviceId} : cross verified: ${trustMap?.get(device)}")
.equalTo(UserEntityFields.USER_ID, it) if (device.trustLevelEntity?.crossSignedVerified != crossSignedVerified) {
.findFirst() Timber.d("## CrossSigning - Trust change detected for ${device.userId}|${device.deviceId} : cross verified: $crossSignedVerified")
?.devices // need to save
val trustEntity = device.trustLevelEntity
val trustMap = devicesEntities?.map { device -> if (trustEntity == null) {
// get up to date from DB has could have been updated device.trustLevelEntity = cryptoRealm.createObject(TrustLevelEntity::class.java).also {
val otherInfo = realm.where(CrossSigningInfoEntity::class.java) it.locallyVerified = false
.equalTo(CrossSigningInfoEntityFields.USER_ID, it) it.crossSignedVerified = crossSignedVerified
.findFirst()?.let { mapCrossSigningInfoEntity(it) }
device to crossSigningService.checkDeviceTrust(myCrossSigningInfo, otherInfo, CryptoMapper.mapToModel(device))
}?.toMap()
// Update trust if needed
devicesEntities?.forEach { device ->
val crossSignedVerified = trustMap?.get(device)?.isCrossSignedVerified()
Timber.d("## CrossSigning - Trust for ${device.userId}|${device.deviceId} : cross verified: ${trustMap?.get(device)}")
if (device.trustLevelEntity?.crossSignedVerified != crossSignedVerified) {
Timber.d("## CrossSigning - Trust change detected for ${device.userId}|${device.deviceId} : cross verified: $crossSignedVerified")
// need to save
val trustEntity = device.trustLevelEntity
if (trustEntity == null) {
realm.createObject(TrustLevelEntity::class.java).let {
it.locallyVerified = false
it.crossSignedVerified = crossSignedVerified
device.trustLevelEntity = it
}
} else {
trustEntity.crossSignedVerified = crossSignedVerified
} }
} else {
trustEntity.crossSignedVerified = crossSignedVerified
} }
} }
} }
@ -201,35 +205,44 @@ internal class UpdateTrustWorker(context: Context,
// We can now update room shields? in the session DB? // We can now update room shields? in the session DB?
Timber.d("## CrossSigning - Updating shields for impacted rooms...") Timber.d("## CrossSigning - Updating shields for impacted rooms...")
Realm.getInstance(sessionRealmConfiguration).use { it -> awaitTransaction(sessionRealmConfiguration) { sessionRealm ->
it.executeTransaction { realm -> sessionRealm.where(RoomMemberSummaryEntity::class.java)
val distinctRoomIds = realm.where(RoomMemberSummaryEntity::class.java) .`in`(RoomMemberSummaryEntityFields.USER_ID, userList.toTypedArray())
.`in`(RoomMemberSummaryEntityFields.USER_ID, userList.toTypedArray()) .distinct(RoomMemberSummaryEntityFields.ROOM_ID)
.distinct(RoomMemberSummaryEntityFields.ROOM_ID) .findAll()
.findAll() .map { it.roomId }
.map { it.roomId } .also { Timber.d("## CrossSigning - ... impacted rooms ${it.logLimit()}") }
Timber.d("## CrossSigning - ... impacted rooms $distinctRoomIds") .forEach { roomId ->
distinctRoomIds.forEach { roomId -> RoomSummaryEntity.where(sessionRealm, roomId)
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() .equalTo(RoomSummaryEntityFields.IS_ENCRYPTED, true)
if (roomSummary?.isEncrypted == true) { .findFirst()
Timber.d("## CrossSigning - Check shield state for room $roomId") ?.let { roomSummary ->
val allActiveRoomMembers = RoomMemberHelper(realm, roomId).getActiveRoomMemberIds() Timber.d("## CrossSigning - Check shield state for room $roomId")
try { val allActiveRoomMembers = RoomMemberHelper(sessionRealm, roomId).getActiveRoomMemberIds()
val updatedTrust = computeRoomShield(allActiveRoomMembers, roomSummary) try {
if (roomSummary.roomEncryptionTrustLevel != updatedTrust) { val updatedTrust = computeRoomShield(
Timber.d("## CrossSigning - Shield change detected for $roomId -> $updatedTrust") myCrossSigningInfo,
roomSummary.roomEncryptionTrustLevel = updatedTrust cRealm,
} allActiveRoomMembers,
} catch (failure: Throwable) { roomSummary
Timber.e(failure) )
} if (roomSummary.roomEncryptionTrustLevel != updatedTrust) {
Timber.d("## CrossSigning - Shield change detected for $roomId -> $updatedTrust")
roomSummary.roomEncryptionTrustLevel = updatedTrust
}
} catch (failure: Throwable) {
Timber.e(failure)
}
}
} }
}
}
} }
}
cleanup(params) private fun getCrossSigningInfo(cryptoRealm: Realm, userId: String): MXCrossSigningInfo? {
return Result.success() return cryptoRealm.where(CrossSigningInfoEntity::class.java)
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
.findFirst()
?.let { mapCrossSigningInfoEntity(it) }
} }
private fun cleanup(params: Params) { private fun cleanup(params: Params) {
@ -237,30 +250,34 @@ internal class UpdateTrustWorker(context: Context,
?.let { updateTrustWorkerDataRepository.delete(it) } ?.let { updateTrustWorkerDataRepository.delete(it) }
} }
private fun updateCrossSigningKeysTrust(realm: Realm, userId: String, verified: Boolean) { private fun updateCrossSigningKeysTrust(cryptoRealm: Realm, userId: String, verified: Boolean) {
val xInfoEntity = realm.where(CrossSigningInfoEntity::class.java) cryptoRealm.where(CrossSigningInfoEntity::class.java)
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId) .equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
.findFirst() .findFirst()
xInfoEntity?.crossSigningKeys?.forEach { info -> ?.crossSigningKeys
// optimization to avoid trigger updates when there is no change.. ?.forEach { info ->
if (info.trustLevelEntity?.isVerified() != verified) { // optimization to avoid trigger updates when there is no change..
Timber.d("## CrossSigning - Trust change for $userId : $verified") if (info.trustLevelEntity?.isVerified() != verified) {
val level = info.trustLevelEntity Timber.d("## CrossSigning - Trust change for $userId : $verified")
if (level == null) { val level = info.trustLevelEntity
val newLevel = realm.createObject(TrustLevelEntity::class.java) if (level == null) {
newLevel.locallyVerified = verified info.trustLevelEntity = cryptoRealm.createObject(TrustLevelEntity::class.java).also {
newLevel.crossSignedVerified = verified it.locallyVerified = verified
info.trustLevelEntity = newLevel it.crossSignedVerified = verified
} else { }
level.locallyVerified = verified } else {
level.crossSignedVerified = verified level.locallyVerified = verified
level.crossSignedVerified = verified
}
}
} }
}
}
} }
private fun computeRoomShield(activeMemberUserIds: List<String>, roomSummaryEntity: RoomSummaryEntity): RoomEncryptionTrustLevel { private fun computeRoomShield(myCrossSigningInfo: MXCrossSigningInfo?,
Timber.d("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} -> $activeMemberUserIds") cryptoRealm: Realm,
activeMemberUserIds: List<String>,
roomSummaryEntity: RoomSummaryEntity): RoomEncryptionTrustLevel {
Timber.d("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} -> ${activeMemberUserIds.logLimit()}")
// The set of “all users” depends on the type of room: // The set of “all users” depends on the type of room:
// For regular / topic rooms which have more than 2 members (including yourself) are considered when decorating a room // For regular / topic rooms which have more than 2 members (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 // For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room
@ -272,17 +289,8 @@ internal class UpdateTrustWorker(context: Context,
val allTrustedUserIds = listToCheck val allTrustedUserIds = listToCheck
.filter { userId -> .filter { userId ->
Realm.getInstance(realmConfiguration).use { getCrossSigningInfo(cryptoRealm, userId)?.isTrusted() == true
it.where(CrossSigningInfoEntity::class.java)
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
.findFirst()?.let { mapCrossSigningInfoEntity(it) }?.isTrusted() == true
}
} }
val myCrossKeys = Realm.getInstance(realmConfiguration).use {
it.where(CrossSigningInfoEntity::class.java)
.equalTo(CrossSigningInfoEntityFields.USER_ID, myUserId)
.findFirst()?.let { mapCrossSigningInfoEntity(it) }
}
return if (allTrustedUserIds.isEmpty()) { return if (allTrustedUserIds.isEmpty()) {
RoomEncryptionTrustLevel.Default RoomEncryptionTrustLevel.Default
@ -291,21 +299,17 @@ internal class UpdateTrustWorker(context: Context,
// If all devices of all verified users are trusted -> green // If all devices of all verified users are trusted -> green
// else -> black // else -> black
allTrustedUserIds allTrustedUserIds
.mapNotNull { uid -> .mapNotNull { userId ->
Realm.getInstance(realmConfiguration).use { cryptoRealm.where<UserEntity>()
it.where<UserEntity>() .equalTo(UserEntityFields.USER_ID, userId)
.equalTo(UserEntityFields.USER_ID, uid) .findFirst()
.findFirst() ?.devices
?.devices ?.map { CryptoMapper.mapToModel(it) }
?.map {
CryptoMapper.mapToModel(it)
}
}
} }
.flatten() .flatten()
.let { allDevices -> .let { allDevices ->
Timber.v("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} devices ${allDevices.map { it.deviceId }}") Timber.v("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} devices ${allDevices.map { it.deviceId }.logLimit()}")
if (myCrossKeys != null) { if (myCrossSigningInfo != null) {
allDevices.any { !it.trustLevel?.crossSigningVerified.orFalse() } allDevices.any { !it.trustLevel?.crossSigningVerified.orFalse() }
} else { } else {
// Legacy method // Legacy method

View File

@ -259,7 +259,7 @@ internal interface IMXCryptoStore {
* @param deviceKey the public key of the other device. * @param deviceKey the public key of the other device.
* @return A set of sessionId, or null if device is not known * @return A set of sessionId, or null if device is not known
*/ */
fun getDeviceSessionIds(deviceKey: String): Set<String>? fun getDeviceSessionIds(deviceKey: String): List<String>?
/** /**
* Retrieve an end-to-end session between the logged-in user and another * Retrieve an end-to-end session between the logged-in user and another

View File

@ -692,7 +692,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
} }
override fun getDeviceSessionIds(deviceKey: String): MutableSet<String> { override fun getDeviceSessionIds(deviceKey: String): List<String> {
return doWithRealm(realmConfiguration) { return doWithRealm(realmConfiguration) {
it.where<OlmSessionEntity>() it.where<OlmSessionEntity>()
.equalTo(OlmSessionEntityFields.DEVICE_KEY, deviceKey) .equalTo(OlmSessionEntityFields.DEVICE_KEY, deviceKey)
@ -701,7 +701,6 @@ internal class RealmCryptoStore @Inject constructor(
sessionEntity.sessionId sessionEntity.sessionId
} }
} }
.toMutableSet()
} }
override fun storeInboundGroupSessions(sessions: List<OlmInboundGroupSessionWrapper2>) { override fun storeInboundGroupSessions(sessions: List<OlmInboundGroupSessionWrapper2>) {
@ -801,7 +800,7 @@ internal class RealmCryptoStore @Inject constructor(
* Note: the result will be only use to export all the keys and not to use the OlmInboundGroupSessionWrapper2, * Note: the result will be only use to export all the keys and not to use the OlmInboundGroupSessionWrapper2,
* so there is no need to use or update `inboundGroupSessionToRelease` for native memory management * so there is no need to use or update `inboundGroupSessionToRelease` for native memory management
*/ */
override fun getInboundGroupSessions(): MutableList<OlmInboundGroupSessionWrapper2> { override fun getInboundGroupSessions(): List<OlmInboundGroupSessionWrapper2> {
return doWithRealm(realmConfiguration) { return doWithRealm(realmConfiguration) {
it.where<OlmInboundGroupSessionEntity>() it.where<OlmInboundGroupSessionEntity>()
.findAll() .findAll()
@ -809,7 +808,6 @@ internal class RealmCryptoStore @Inject constructor(
inboundGroupSessionEntity.getInboundGroupSession() inboundGroupSessionEntity.getInboundGroupSession()
} }
} }
.toMutableList()
} }
override fun removeInboundGroupSession(sessionId: String, senderKey: String) { override fun removeInboundGroupSession(sessionId: String, senderKey: String) {
@ -964,7 +962,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
} }
override fun getRoomsListBlacklistUnverifiedDevices(): MutableList<String> { override fun getRoomsListBlacklistUnverifiedDevices(): List<String> {
return doWithRealm(realmConfiguration) { return doWithRealm(realmConfiguration) {
it.where<CryptoRoomEntity>() it.where<CryptoRoomEntity>()
.equalTo(CryptoRoomEntityFields.BLACKLIST_UNVERIFIED_DEVICES, true) .equalTo(CryptoRoomEntityFields.BLACKLIST_UNVERIFIED_DEVICES, true)
@ -973,10 +971,9 @@ internal class RealmCryptoStore @Inject constructor(
cryptoRoom.roomId cryptoRoom.roomId
} }
} }
.toMutableList()
} }
override fun getDeviceTrackingStatuses(): MutableMap<String, Int> { override fun getDeviceTrackingStatuses(): Map<String, Int> {
return doWithRealm(realmConfiguration) { return doWithRealm(realmConfiguration) {
it.where<UserEntity>() it.where<UserEntity>()
.findAll() .findAll()
@ -987,7 +984,6 @@ internal class RealmCryptoStore @Inject constructor(
entry.value.deviceTrackingStatus entry.value.deviceTrackingStatus
} }
} }
.toMutableMap()
} }
override fun saveDeviceTrackingStatuses(deviceTrackingStatuses: Map<String, Int>) { override fun saveDeviceTrackingStatuses(deviceTrackingStatuses: Map<String, Int>) {

View File

@ -22,6 +22,8 @@ import io.realm.kotlin.createObject
import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.TimeoutCancellationException
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
import org.matrix.android.sdk.internal.crypto.DeviceListManager
import org.matrix.android.sdk.internal.database.awaitNotEmptyResult import org.matrix.android.sdk.internal.database.awaitNotEmptyResult
import org.matrix.android.sdk.internal.database.mapper.toEntity import org.matrix.android.sdk.internal.database.mapper.toEntity
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
@ -57,6 +59,8 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(
private val syncTokenStore: SyncTokenStore, private val syncTokenStore: SyncTokenStore,
private val roomSummaryUpdater: RoomSummaryUpdater, private val roomSummaryUpdater: RoomSummaryUpdater,
private val roomMemberEventHandler: RoomMemberEventHandler, private val roomMemberEventHandler: RoomMemberEventHandler,
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
private val deviceListManager: DeviceListManager,
private val globalErrorReceiver: GlobalErrorReceiver private val globalErrorReceiver: GlobalErrorReceiver
) : LoadRoomMembersTask { ) : LoadRoomMembersTask {
@ -124,6 +128,10 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(
roomEntity.membersLoadStatus = RoomMembersLoadStatusType.LOADED roomEntity.membersLoadStatus = RoomMembersLoadStatusType.LOADED
roomSummaryUpdater.update(realm, roomId, updateMembers = true) roomSummaryUpdater.update(realm, roomId, updateMembers = true)
} }
if (cryptoSessionInfoProvider.isRoomEncrypted(roomId)) {
deviceListManager.onRoomMembersLoadedFor(roomId)
}
} }
private fun getRoomMembersLoadStatus(roomId: String): RoomMembersLoadStatusType { private fun getRoomMembersLoadStatus(roomId: String): RoomMembersLoadStatusType {

View File

@ -117,7 +117,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
} }
if (readReceiptId != null) { if (readReceiptId != null) {
val readReceiptContent = ReadReceiptHandler.createContent(userId, readReceiptId) val readReceiptContent = ReadReceiptHandler.createContent(userId, readReceiptId)
readReceiptHandler.handle(realm, roomId, readReceiptContent, false) readReceiptHandler.handle(realm, roomId, readReceiptContent, false, null)
} }
if (shouldUpdateRoomSummary) { if (shouldUpdateRoomSummary) {
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()

View File

@ -131,8 +131,8 @@ internal class RoomSummaryUpdater @Inject constructor(
// mmm i want to decrypt now or is it ok to do it async? // mmm i want to decrypt now or is it ok to do it async?
tryOrNull { tryOrNull {
eventDecryptor.decryptEvent(root.asDomain(), "") eventDecryptor.decryptEvent(root.asDomain(), "")
// eventDecryptor.decryptEventAsync(root.asDomain(), "", NoOpMatrixCallback())
} }
?.let { root.setDecryptionResult(it) }
} }
if (updateMembers) { if (updateMembers) {
@ -144,7 +144,7 @@ internal class RoomSummaryUpdater @Inject constructor(
roomSummaryEntity.otherMemberIds.clear() roomSummaryEntity.otherMemberIds.clear()
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers) roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers)
if (roomSummaryEntity.isEncrypted) { if (roomSummaryEntity.isEncrypted && otherRoomMembers.isNotEmpty()) {
// mmm maybe we could only refresh shield instead of checking trust also? // mmm maybe we could only refresh shield instead of checking trust also?
crossSigningService.onUsersDeviceUpdate(otherRoomMembers) crossSigningService.onUsersDeviceUpdate(otherRoomMembers)
} }

View File

@ -44,6 +44,7 @@ import org.matrix.android.sdk.internal.database.query.findAllInRoomWithSendState
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.database.query.whereRoomId import org.matrix.android.sdk.internal.database.query.whereRoomId
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
import org.matrix.android.sdk.internal.session.sync.ReadReceiptHandler
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.internal.util.Debouncer import org.matrix.android.sdk.internal.util.Debouncer
@ -73,7 +74,8 @@ internal class DefaultTimeline(
private val timelineInput: TimelineInput, private val timelineInput: TimelineInput,
private val eventDecryptor: TimelineEventDecryptor, private val eventDecryptor: TimelineEventDecryptor,
private val realmSessionProvider: RealmSessionProvider, private val realmSessionProvider: RealmSessionProvider,
private val loadRoomMembersTask: LoadRoomMembersTask private val loadRoomMembersTask: LoadRoomMembersTask,
private val readReceiptHandler: ReadReceiptHandler
) : Timeline, ) : Timeline,
TimelineHiddenReadReceipts.Delegate, TimelineHiddenReadReceipts.Delegate,
TimelineInput.Listener, TimelineInput.Listener,
@ -182,11 +184,27 @@ internal class DefaultTimeline(
} }
.executeBy(taskExecutor) .executeBy(taskExecutor)
// Ensure ReadReceipt from init sync are loaded
ensureReadReceiptAreLoaded(realm)
isReady.set(true) isReady.set(true)
} }
} }
} }
private fun ensureReadReceiptAreLoaded(realm: Realm) {
readReceiptHandler.getContentFromInitSync(roomId)
?.also {
Timber.w("INIT_SYNC Insert when opening timeline RR for room $roomId")
}
?.let { readReceiptContent ->
realm.executeTransactionAsync {
readReceiptHandler.handle(it, roomId, readReceiptContent, false, null)
readReceiptHandler.onContentFromInitSyncHandled(roomId)
}
}
}
private fun TimelineSettings.shouldHandleHiddenReadReceipts(): Boolean { private fun TimelineSettings.shouldHandleHiddenReadReceipts(): Boolean {
return buildReadReceipts && (filters.filterEdits || filters.filterTypes) return buildReadReceipts && (filters.filterEdits || filters.filterTypes)
} }

View File

@ -17,10 +17,10 @@
package org.matrix.android.sdk.internal.session.room.timeline package org.matrix.android.sdk.internal.session.room.timeline
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.realm.Sort import io.realm.Sort
import io.realm.kotlin.where import io.realm.kotlin.where
import org.matrix.android.sdk.api.session.events.model.isImageMessage import org.matrix.android.sdk.api.session.events.model.isImageMessage
@ -38,20 +38,23 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
import org.matrix.android.sdk.internal.session.sync.ReadReceiptHandler
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
internal class DefaultTimelineService @AssistedInject constructor(@Assisted private val roomId: String, internal class DefaultTimelineService @AssistedInject constructor(
@SessionDatabase private val monarchy: Monarchy, @Assisted private val roomId: String,
private val realmSessionProvider: RealmSessionProvider, @SessionDatabase private val monarchy: Monarchy,
private val timelineInput: TimelineInput, private val realmSessionProvider: RealmSessionProvider,
private val taskExecutor: TaskExecutor, private val timelineInput: TimelineInput,
private val contextOfEventTask: GetContextOfEventTask, private val taskExecutor: TaskExecutor,
private val eventDecryptor: TimelineEventDecryptor, private val contextOfEventTask: GetContextOfEventTask,
private val paginationTask: PaginationTask, private val eventDecryptor: TimelineEventDecryptor,
private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, private val paginationTask: PaginationTask,
private val timelineEventMapper: TimelineEventMapper, private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, private val timelineEventMapper: TimelineEventMapper,
private val loadRoomMembersTask: LoadRoomMembersTask private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
private val loadRoomMembersTask: LoadRoomMembersTask,
private val readReceiptHandler: ReadReceiptHandler
) : TimelineService { ) : TimelineService {
@AssistedFactory @AssistedFactory
@ -74,7 +77,8 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
eventDecryptor = eventDecryptor, eventDecryptor = eventDecryptor,
fetchTokenAndPaginateTask = fetchTokenAndPaginateTask, fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
realmSessionProvider = realmSessionProvider, realmSessionProvider = realmSessionProvider,
loadRoomMembersTask = loadRoomMembersTask loadRoomMembersTask = loadRoomMembersTask,
readReceiptHandler = readReceiptHandler
) )
} }
@ -87,7 +91,7 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
} }
override fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>> { override fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>> {
return LiveTimelineEvent(timelineInput, monarchy, taskExecutor.executorScope, timelineEventMapper, roomId, eventId) return LiveTimelineEvent(monarchy, taskExecutor.executorScope, timelineEventMapper, roomId, eventId)
} }
override fun getAttachmentMessages(): List<TimelineEvent> { override fun getAttachmentMessages(): List<TimelineEvent> {

View File

@ -18,8 +18,9 @@ package org.matrix.android.sdk.internal.session.room.timeline
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import io.realm.RealmQuery
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -29,66 +30,57 @@ import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import java.util.concurrent.atomic.AtomicBoolean
/** /**
* This class takes care of handling case where local echo is replaced by the synced event in the db. * This class takes care of handling case where local echo is replaced by the synced event in the db.
*/ */
internal class LiveTimelineEvent(private val timelineInput: TimelineInput, internal class LiveTimelineEvent(private val monarchy: Monarchy,
private val monarchy: Monarchy,
private val coroutineScope: CoroutineScope, private val coroutineScope: CoroutineScope,
private val timelineEventMapper: TimelineEventMapper, private val timelineEventMapper: TimelineEventMapper,
private val roomId: String, private val roomId: String,
private val eventId: String) private val eventId: String)
: TimelineInput.Listener, : MediatorLiveData<Optional<TimelineEvent>>() {
MediatorLiveData<Optional<TimelineEvent>>() {
private var queryLiveData: LiveData<Optional<TimelineEvent>>? = null
// If we are listening to local echo, we want to be aware when event is synced
private var shouldObserveSync = AtomicBoolean(LocalEcho.isLocalEchoId(eventId))
init { init {
buildAndObserveQuery(eventId) buildAndObserveQuery()
} }
private var initialLiveData: LiveData<List<TimelineEvent>>? = null
// Makes sure it's made on the main thread // Makes sure it's made on the main thread
private fun buildAndObserveQuery(eventIdToObserve: String) = coroutineScope.launch(Dispatchers.Main) { private fun buildAndObserveQuery() = coroutineScope.launch(Dispatchers.Main) {
queryLiveData?.also {
removeSource(it)
}
val liveData = monarchy.findAllMappedWithChanges( val liveData = monarchy.findAllMappedWithChanges(
{ TimelineEventEntity.where(it, roomId = roomId, eventId = eventIdToObserve) }, { TimelineEventEntity.where(it, roomId = roomId, eventId = eventId) },
{ timelineEventMapper.map(it) } { timelineEventMapper.map(it) }
) )
queryLiveData = Transformations.map(liveData) { events -> addSource(liveData) { newValue ->
events.firstOrNull().toOptional() value = newValue.firstOrNull().toOptional()
}.also { }
addSource(it) { newValue -> value = newValue } initialLiveData = liveData
if (LocalEcho.isLocalEchoId(eventId)) {
observeTimelineEventWithTxId()
} }
} }
override fun onLocalEchoSynced(roomId: String, localEchoEventId: String, syncedEventId: String) { private fun observeTimelineEventWithTxId() {
if (this.roomId == roomId && localEchoEventId == this.eventId) { val liveData = monarchy.findAllMappedWithChanges(
timelineInput.listeners.remove(this) { it.queryTimelineEventWithTxId() },
shouldObserveSync.set(false) { timelineEventMapper.map(it) }
// rebuild the query with the new eventId )
buildAndObserveQuery(syncedEventId) addSource(liveData) { newValue ->
val optionalValue = newValue.firstOrNull().toOptional()
if (optionalValue.hasValue()) {
initialLiveData?.also { removeSource(it) }
value = optionalValue
}
} }
} }
override fun onActive() { private fun Realm.queryTimelineEventWithTxId(): RealmQuery<TimelineEventEntity> {
super.onActive() return where(TimelineEventEntity::class.java)
if (shouldObserveSync.get()) { .equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
timelineInput.listeners.add(this) .like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, """{*"transaction_id":*"$eventId"*}""")
}
}
override fun onInactive() {
super.onInactive()
if (shouldObserveSync.get()) {
timelineInput.listeners.remove(this)
}
} }
} }

View File

@ -35,16 +35,11 @@ internal class TimelineInput @Inject constructor() {
listeners.toSet().forEach { it.onNewTimelineEvents(roomId, eventIds) } listeners.toSet().forEach { it.onNewTimelineEvents(roomId, eventIds) }
} }
fun onLocalEchoSynced(roomId: String, localEchoEventId: String, syncEventId: String) {
listeners.toSet().forEach { it.onLocalEchoSynced(roomId, localEchoEventId, syncEventId) }
}
val listeners = mutableSetOf<Listener>() val listeners = mutableSetOf<Listener>()
internal interface Listener { internal interface Listener {
fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) = Unit fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) = Unit
fun onLocalEchoUpdated(roomId: String, eventId: String, sendState: SendState) = Unit fun onLocalEchoUpdated(roomId: String, eventId: String, sendState: SendState) = Unit
fun onNewTimelineEvents(roomId: String, eventIds: List<String>) = Unit fun onNewTimelineEvents(roomId: String, eventIds: List<String>) = Unit
fun onLocalEchoSynced(roomId: String, localEchoEventId: String, syncedEventId: String) = Unit
} }
} }

View File

@ -42,9 +42,9 @@ sealed class InitialSyncStrategy {
val minSizeToSplit: Long = 1024 * 1024, val minSizeToSplit: Long = 1024 * 1024,
/** /**
* Limit per room to reach to decide to store a join room ephemeral Events into a file * Limit per room to reach to decide to store a join room ephemeral Events into a file
* Empiric value: 6 kilobytes * Empiric value: 1 kilobytes
*/ */
val minSizeToStoreInFile: Long = 6 * 1024, val minSizeToStoreInFile: Long = 1024,
/** /**
* Max number of rooms to insert at a time in database (to avoid too much RAM usage) * Max number of rooms to insert at a time in database (to avoid too much RAM usage)
*/ */

View File

@ -16,12 +16,13 @@
package org.matrix.android.sdk.internal.session.sync package org.matrix.android.sdk.internal.session.sync
import io.realm.Realm
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity
import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity
import org.matrix.android.sdk.internal.database.query.createUnmanaged import org.matrix.android.sdk.internal.database.query.createUnmanaged
import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import io.realm.Realm
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -35,7 +36,9 @@ typealias ReadReceiptContent = Map<String, Map<String, Map<String, Map<String, D
private const val READ_KEY = "m.read" private const val READ_KEY = "m.read"
private const val TIMESTAMP_KEY = "ts" private const val TIMESTAMP_KEY = "ts"
internal class ReadReceiptHandler @Inject constructor() { internal class ReadReceiptHandler @Inject constructor(
private val roomSyncEphemeralTemporaryStore: RoomSyncEphemeralTemporaryStore
) {
companion object { companion object {
@ -52,22 +55,29 @@ internal class ReadReceiptHandler @Inject constructor() {
} }
} }
fun handle(realm: Realm, roomId: String, content: ReadReceiptContent?, isInitialSync: Boolean) { fun handle(realm: Realm,
if (content == null) { roomId: String,
return content: ReadReceiptContent?,
} isInitialSync: Boolean,
aggregator: SyncResponsePostTreatmentAggregator?) {
content ?: return
try { try {
handleReadReceiptContent(realm, roomId, content, isInitialSync) handleReadReceiptContent(realm, roomId, content, isInitialSync, aggregator)
} catch (exception: Exception) { } catch (exception: Exception) {
Timber.e("Fail to handle read receipt for room $roomId") Timber.e("Fail to handle read receipt for room $roomId")
} }
} }
private fun handleReadReceiptContent(realm: Realm, roomId: String, content: ReadReceiptContent, isInitialSync: Boolean) { private fun handleReadReceiptContent(realm: Realm,
roomId: String,
content: ReadReceiptContent,
isInitialSync: Boolean,
aggregator: SyncResponsePostTreatmentAggregator?) {
if (isInitialSync) { if (isInitialSync) {
initialSyncStrategy(realm, roomId, content) initialSyncStrategy(realm, roomId, content)
} else { } else {
incrementalSyncStrategy(realm, roomId, content) incrementalSyncStrategy(realm, roomId, content, aggregator)
} }
} }
@ -87,7 +97,21 @@ internal class ReadReceiptHandler @Inject constructor() {
realm.insertOrUpdate(readReceiptSummaries) realm.insertOrUpdate(readReceiptSummaries)
} }
private fun incrementalSyncStrategy(realm: Realm, roomId: String, content: ReadReceiptContent) { private fun incrementalSyncStrategy(realm: Realm,
roomId: String,
content: ReadReceiptContent,
aggregator: SyncResponsePostTreatmentAggregator?) {
// First check if we have data from init sync to handle
getContentFromInitSync(roomId)?.let {
Timber.w("INIT_SYNC Insert during incremental sync RR for room $roomId")
doIncrementalSyncStrategy(realm, roomId, it)
aggregator?.ephemeralFilesToDelete?.add(roomId)
}
doIncrementalSyncStrategy(realm, roomId, content)
}
private fun doIncrementalSyncStrategy(realm: Realm, roomId: String, content: ReadReceiptContent) {
for ((eventId, receiptDict) in content) { for ((eventId, receiptDict) in content) {
val userIdsDict = receiptDict[READ_KEY] ?: continue val userIdsDict = receiptDict[READ_KEY] ?: continue
val readReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst() val readReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst()
@ -110,4 +134,27 @@ internal class ReadReceiptHandler @Inject constructor() {
} }
} }
} }
fun getContentFromInitSync(roomId: String): ReadReceiptContent? {
val dataFromFile = roomSyncEphemeralTemporaryStore.read(roomId)
dataFromFile ?: return null
@Suppress("UNCHECKED_CAST")
val content = dataFromFile
.events
.firstOrNull { it.type == EventType.RECEIPT }
?.content as? ReadReceiptContent
if (content == null) {
// We can delete the file now
roomSyncEphemeralTemporaryStore.delete(roomId)
}
return content
}
fun onContentFromInitSyncHandled(roomId: String) {
roomSyncEphemeralTemporaryStore.delete(roomId)
}
} }

View File

@ -0,0 +1,79 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.sync
import com.squareup.moshi.JsonReader
import com.squareup.moshi.Moshi
import okio.buffer
import okio.source
import org.matrix.android.sdk.internal.di.SessionFilesDirectory
import org.matrix.android.sdk.internal.session.sync.model.RoomSyncEphemeral
import org.matrix.android.sdk.internal.util.md5
import timber.log.Timber
import java.io.File
import javax.inject.Inject
internal interface RoomSyncEphemeralTemporaryStore {
fun write(roomId: String, roomSyncEphemeralJson: String)
fun read(roomId: String): RoomSyncEphemeral?
fun reset()
fun delete(roomId: String)
}
internal class RoomSyncEphemeralTemporaryStoreFile @Inject constructor(
@SessionFilesDirectory fileDirectory: File,
moshi: Moshi
) : RoomSyncEphemeralTemporaryStore {
private val workingDir = File(fileDirectory, "rr")
.also { it.mkdirs() }
private val roomSyncEphemeralAdapter = moshi.adapter(RoomSyncEphemeral::class.java)
/**
* Write RoomSyncEphemeral to a file
*/
override fun write(roomId: String, roomSyncEphemeralJson: String) {
Timber.w("INIT_SYNC Store ephemeral events for room $roomId")
getFile(roomId).writeText(roomSyncEphemeralJson)
}
/**
* Read RoomSyncEphemeral from a file, or null if there is no file to read
*/
override fun read(roomId: String): RoomSyncEphemeral? {
return getFile(roomId)
.takeIf { it.exists() }
?.inputStream()
?.use { pos ->
roomSyncEphemeralAdapter.fromJson(JsonReader.of(pos.source().buffer()))
}
}
override fun delete(roomId: String) {
getFile(roomId).delete()
}
override fun reset() {
workingDir.deleteRecursively()
workingDir.mkdirs()
}
private fun getFile(roomId: String): File {
return File(workingDir, "${roomId.md5()}.json")
}
}

View File

@ -60,6 +60,7 @@ import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
import org.matrix.android.sdk.internal.session.room.timeline.TimelineInput import org.matrix.android.sdk.internal.session.room.timeline.TimelineInput
import org.matrix.android.sdk.internal.session.room.typing.TypingEventContent import org.matrix.android.sdk.internal.session.room.typing.TypingEventContent
import org.matrix.android.sdk.internal.session.sync.model.InvitedRoomSync import org.matrix.android.sdk.internal.session.sync.model.InvitedRoomSync
import org.matrix.android.sdk.internal.session.sync.model.LazyRoomSyncEphemeral
import org.matrix.android.sdk.internal.session.sync.model.RoomSync import org.matrix.android.sdk.internal.session.sync.model.RoomSync
import org.matrix.android.sdk.internal.session.sync.model.RoomSyncAccountData import org.matrix.android.sdk.internal.session.sync.model.RoomSyncAccountData
import org.matrix.android.sdk.internal.session.sync.model.RoomsSyncResponse import org.matrix.android.sdk.internal.session.sync.model.RoomsSyncResponse
@ -87,29 +88,21 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
fun handle(realm: Realm, fun handle(realm: Realm,
roomsSyncResponse: RoomsSyncResponse, roomsSyncResponse: RoomsSyncResponse,
isInitialSync: Boolean, isInitialSync: Boolean,
aggregator: SyncResponsePostTreatmentAggregator,
reporter: ProgressReporter? = null) { reporter: ProgressReporter? = null) {
Timber.v("Execute transaction from $this") Timber.v("Execute transaction from $this")
handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), isInitialSync, reporter) handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), isInitialSync, aggregator, reporter)
handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), isInitialSync, reporter) handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), isInitialSync, aggregator, reporter)
handleRoomSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), isInitialSync, reporter) handleRoomSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), isInitialSync, aggregator, reporter)
}
fun handleInitSyncEphemeral(realm: Realm,
roomsSyncResponse: RoomsSyncResponse) {
roomsSyncResponse.join.forEach { roomSync ->
val ephemeralResult = roomSync.value.ephemeral
?.roomSyncEphemeral
?.events
?.takeIf { it.isNotEmpty() }
?.let { events -> handleEphemeral(realm, roomSync.key, events, true) }
roomTypingUsersHandler.handle(realm, roomSync.key, ephemeralResult)
}
} }
// PRIVATE METHODS ***************************************************************************** // PRIVATE METHODS *****************************************************************************
private fun handleRoomSync(realm: Realm, handlingStrategy: HandlingStrategy, isInitialSync: Boolean, reporter: ProgressReporter?) { private fun handleRoomSync(realm: Realm,
handlingStrategy: HandlingStrategy,
isInitialSync: Boolean,
aggregator: SyncResponsePostTreatmentAggregator,
reporter: ProgressReporter?) {
val insertType = if (isInitialSync) { val insertType = if (isInitialSync) {
EventInsertType.INITIAL_SYNC EventInsertType.INITIAL_SYNC
} else { } else {
@ -119,12 +112,12 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
val rooms = when (handlingStrategy) { val rooms = when (handlingStrategy) {
is HandlingStrategy.JOINED -> { is HandlingStrategy.JOINED -> {
if (isInitialSync && initialSyncStrategy is InitialSyncStrategy.Optimized) { if (isInitialSync && initialSyncStrategy is InitialSyncStrategy.Optimized) {
insertJoinRoomsFromInitSync(realm, handlingStrategy, syncLocalTimeStampMillis, reporter) insertJoinRoomsFromInitSync(realm, handlingStrategy, syncLocalTimeStampMillis, aggregator, reporter)
// Rooms are already inserted, return an empty list // Rooms are already inserted, return an empty list
emptyList() emptyList()
} else { } else {
handlingStrategy.data.mapWithProgress(reporter, InitSyncStep.ImportingAccountJoinedRooms, 0.6f) { handlingStrategy.data.mapWithProgress(reporter, InitSyncStep.ImportingAccountJoinedRooms, 0.6f) {
handleJoinedRoom(realm, it.key, it.value, true, insertType, syncLocalTimeStampMillis) handleJoinedRoom(realm, it.key, it.value, insertType, syncLocalTimeStampMillis, aggregator)
} }
} }
} }
@ -145,6 +138,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
private fun insertJoinRoomsFromInitSync(realm: Realm, private fun insertJoinRoomsFromInitSync(realm: Realm,
handlingStrategy: HandlingStrategy.JOINED, handlingStrategy: HandlingStrategy.JOINED,
syncLocalTimeStampMillis: Long, syncLocalTimeStampMillis: Long,
aggregator: SyncResponsePostTreatmentAggregator,
reporter: ProgressReporter?) { reporter: ProgressReporter?) {
val maxSize = (initialSyncStrategy as? InitialSyncStrategy.Optimized)?.maxRoomsToInsert ?: Int.MAX_VALUE val maxSize = (initialSyncStrategy as? InitialSyncStrategy.Optimized)?.maxRoomsToInsert ?: Int.MAX_VALUE
val listSize = handlingStrategy.data.keys.size val listSize = handlingStrategy.data.keys.size
@ -165,9 +159,9 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
realm = realm, realm = realm,
roomId = it, roomId = it,
roomSync = handlingStrategy.data[it] ?: error("Should not happen"), roomSync = handlingStrategy.data[it] ?: error("Should not happen"),
handleEphemeralEvents = false,
insertType = EventInsertType.INITIAL_SYNC, insertType = EventInsertType.INITIAL_SYNC,
syncLocalTimestampMillis = syncLocalTimeStampMillis syncLocalTimestampMillis = syncLocalTimeStampMillis,
aggregator
) )
} }
realm.insertOrUpdate(roomEntities) realm.insertOrUpdate(roomEntities)
@ -177,7 +171,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
} else { } else {
// No need to split // No need to split
val rooms = handlingStrategy.data.mapWithProgress(reporter, InitSyncStep.ImportingAccountJoinedRooms, 0.6f) { val rooms = handlingStrategy.data.mapWithProgress(reporter, InitSyncStep.ImportingAccountJoinedRooms, 0.6f) {
handleJoinedRoom(realm, it.key, it.value, false, EventInsertType.INITIAL_SYNC, syncLocalTimeStampMillis) handleJoinedRoom(realm, it.key, it.value, EventInsertType.INITIAL_SYNC, syncLocalTimeStampMillis, aggregator)
} }
realm.insertOrUpdate(rooms) realm.insertOrUpdate(rooms)
} }
@ -186,17 +180,16 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
private fun handleJoinedRoom(realm: Realm, private fun handleJoinedRoom(realm: Realm,
roomId: String, roomId: String,
roomSync: RoomSync, roomSync: RoomSync,
handleEphemeralEvents: Boolean,
insertType: EventInsertType, insertType: EventInsertType,
syncLocalTimestampMillis: Long): RoomEntity { syncLocalTimestampMillis: Long,
aggregator: SyncResponsePostTreatmentAggregator): RoomEntity {
Timber.v("Handle join sync for room $roomId") Timber.v("Handle join sync for room $roomId")
var ephemeralResult: EphemeralResult? = null val ephemeralResult = (roomSync.ephemeral as? LazyRoomSyncEphemeral.Parsed)
if (handleEphemeralEvents) { ?._roomSyncEphemeral
ephemeralResult = roomSync.ephemeral?.roomSyncEphemeral?.events ?.events
?.takeIf { it.isNotEmpty() } ?.takeIf { it.isNotEmpty() }
?.let { handleEphemeral(realm, roomId, it, insertType == EventInsertType.INITIAL_SYNC) } ?.let { handleEphemeral(realm, roomId, it, insertType == EventInsertType.INITIAL_SYNC, aggregator) }
}
if (roomSync.accountData?.events?.isNotEmpty() == true) { if (roomSync.accountData?.events?.isNotEmpty() == true) {
handleRoomAccountDataEvents(realm, roomId, roomSync.accountData) handleRoomAccountDataEvents(realm, roomId, roomSync.accountData)
@ -400,7 +393,6 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
event.mxDecryptionResult = adapter.fromJson(json) event.mxDecryptionResult = adapter.fromJson(json)
} }
} }
timelineInput.onLocalEchoSynced(roomId, it, event.eventId)
// Finally delete the local echo // Finally delete the local echo
sendingEventEntity.deleteOnCascade(true) sendingEventEntity.deleteOnCascade(true)
} else { } else {
@ -437,14 +429,15 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
private fun handleEphemeral(realm: Realm, private fun handleEphemeral(realm: Realm,
roomId: String, roomId: String,
ephemeralEvents: List<Event>, ephemeralEvents: List<Event>,
isInitialSync: Boolean): EphemeralResult { isInitialSync: Boolean,
aggregator: SyncResponsePostTreatmentAggregator): EphemeralResult {
var result = EphemeralResult() var result = EphemeralResult()
for (event in ephemeralEvents) { for (event in ephemeralEvents) {
when (event.type) { when (event.type) {
EventType.RECEIPT -> { EventType.RECEIPT -> {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
(event.content as? ReadReceiptContent)?.let { readReceiptContent -> (event.content as? ReadReceiptContent)?.let { readReceiptContent ->
readReceiptHandler.handle(realm, roomId, readReceiptContent, isInitialSync) readReceiptHandler.handle(realm, roomId, readReceiptContent, isInitialSync, aggregator)
} }
} }
EventType.TYPING -> { EventType.TYPING -> {

View File

@ -26,6 +26,7 @@ import javax.inject.Inject
internal class RoomTypingUsersHandler @Inject constructor(@UserId private val userId: String, internal class RoomTypingUsersHandler @Inject constructor(@UserId private val userId: String,
private val typingUsersTracker: DefaultTypingUsersTracker) { private val typingUsersTracker: DefaultTypingUsersTracker) {
// TODO This could be handled outside of the Realm transaction. Use the new aggregator?
fun handle(realm: Realm, roomId: String, ephemeralResult: RoomSyncHandler.EphemeralResult?) { fun handle(realm: Realm, roomId: String, ephemeralResult: RoomSyncHandler.EphemeralResult?) {
val roomMemberHelper = RoomMemberHelper(realm, roomId) val roomMemberHelper = RoomMemberHelper(realm, roomId)
val typingIds = ephemeralResult?.typingUserIds?.filter { it != userId }.orEmpty() val typingIds = ephemeralResult?.typingUserIds?.filter { it != userId }.orEmpty()

View File

@ -37,4 +37,7 @@ internal abstract class SyncModule {
@Binds @Binds
abstract fun bindSyncTask(task: DefaultSyncTask): SyncTask abstract fun bindSyncTask(task: DefaultSyncTask): SyncTask
@Binds
abstract fun bindRoomSyncEphemeralTemporaryStore(store: RoomSyncEphemeralTemporaryStoreFile): RoomSyncEphemeralTemporaryStore
} }

View File

@ -41,17 +41,19 @@ import kotlin.system.measureTimeMillis
private const val GET_GROUP_DATA_WORKER = "GET_GROUP_DATA_WORKER" private const val GET_GROUP_DATA_WORKER = "GET_GROUP_DATA_WORKER"
internal class SyncResponseHandler @Inject constructor(@SessionDatabase private val monarchy: Monarchy, internal class SyncResponseHandler @Inject constructor(
@SessionId private val sessionId: String, @SessionDatabase private val monarchy: Monarchy,
private val workManagerProvider: WorkManagerProvider, @SessionId private val sessionId: String,
private val roomSyncHandler: RoomSyncHandler, private val workManagerProvider: WorkManagerProvider,
private val userAccountDataSyncHandler: UserAccountDataSyncHandler, private val roomSyncHandler: RoomSyncHandler,
private val groupSyncHandler: GroupSyncHandler, private val userAccountDataSyncHandler: UserAccountDataSyncHandler,
private val cryptoSyncHandler: CryptoSyncHandler, private val groupSyncHandler: GroupSyncHandler,
private val cryptoService: DefaultCryptoService, private val cryptoSyncHandler: CryptoSyncHandler,
private val tokenStore: SyncTokenStore, private val aggregatorHandler: SyncResponsePostTreatmentAggregatorHandler,
private val processEventForPushTask: ProcessEventForPushTask, private val cryptoService: DefaultCryptoService,
private val pushRuleService: PushRuleService) { private val tokenStore: SyncTokenStore,
private val processEventForPushTask: ProcessEventForPushTask,
private val pushRuleService: PushRuleService) {
suspend fun handleResponse(syncResponse: SyncResponse, suspend fun handleResponse(syncResponse: SyncResponse,
fromToken: String?, fromToken: String?,
@ -81,13 +83,14 @@ internal class SyncResponseHandler @Inject constructor(@SessionDatabase private
}.also { }.also {
Timber.v("Finish handling toDevice in $it ms") Timber.v("Finish handling toDevice in $it ms")
} }
val aggregator = SyncResponsePostTreatmentAggregator()
// Start one big transaction // Start one big transaction
monarchy.awaitTransaction { realm -> monarchy.awaitTransaction { realm ->
measureTimeMillis { measureTimeMillis {
Timber.v("Handle rooms") Timber.v("Handle rooms")
reportSubtask(reporter, InitSyncStep.ImportingAccountRoom, 1, 0.7f) { reportSubtask(reporter, InitSyncStep.ImportingAccountRoom, 1, 0.7f) {
if (syncResponse.rooms != null) { if (syncResponse.rooms != null) {
roomSyncHandler.handle(realm, syncResponse.rooms, isInitialSync, reporter) roomSyncHandler.handle(realm, syncResponse.rooms, isInitialSync, aggregator, reporter)
} }
} }
}.also { }.also {
@ -115,7 +118,10 @@ internal class SyncResponseHandler @Inject constructor(@SessionDatabase private
} }
tokenStore.saveToken(realm, syncResponse.nextBatch) tokenStore.saveToken(realm, syncResponse.nextBatch)
} }
// Everything else we need to do outside the transaction // Everything else we need to do outside the transaction
aggregatorHandler.handle(aggregator)
syncResponse.rooms?.let { syncResponse.rooms?.let {
checkPushRules(it, isInitialSync) checkPushRules(it, isInitialSync)
userAccountDataSyncHandler.synchronizeWithServerIfNeeded(it.invite) userAccountDataSyncHandler.synchronizeWithServerIfNeeded(it.invite)
@ -128,15 +134,6 @@ internal class SyncResponseHandler @Inject constructor(@SessionDatabase private
cryptoSyncHandler.onSyncCompleted(syncResponse) cryptoSyncHandler.onSyncCompleted(syncResponse)
} }
suspend fun handleInitSyncSecondTransaction(syncResponse: SyncResponse) {
// Start another transaction to handle the ephemeral events
monarchy.awaitTransaction { realm ->
if (syncResponse.rooms != null) {
roomSyncHandler.handleInitSyncEphemeral(realm, syncResponse.rooms)
}
}
}
/** /**
* At the moment we don't get any group data through the sync, so we poll where every hour. * At the moment we don't get any group data through the sync, so we poll where every hour.
* You can also force to refetch group data using [Group] API. * You can also force to refetch group data using [Group] API.

View File

@ -0,0 +1,22 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.sync
internal class SyncResponsePostTreatmentAggregator {
// List of RoomId
val ephemeralFilesToDelete = mutableListOf<String>()
}

View File

@ -0,0 +1,33 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.sync
import javax.inject.Inject
internal class SyncResponsePostTreatmentAggregatorHandler @Inject constructor(
private val ephemeralTemporaryStore: RoomSyncEphemeralTemporaryStore
) {
fun handle(synResHaResponsePostTreatmentAggregator: SyncResponsePostTreatmentAggregator) {
cleanupEphemeralFiles(synResHaResponsePostTreatmentAggregator.ephemeralFilesToDelete)
}
private fun cleanupEphemeralFiles(ephemeralFilesToDelete: List<String>) {
ephemeralFilesToDelete.forEach {
ephemeralTemporaryStore.delete(it)
}
}
}

View File

@ -62,7 +62,8 @@ internal class DefaultSyncTask @Inject constructor(
private val globalErrorReceiver: GlobalErrorReceiver, private val globalErrorReceiver: GlobalErrorReceiver,
@SessionFilesDirectory @SessionFilesDirectory
private val fileDirectory: File, private val fileDirectory: File,
private val syncResponseParser: InitialSyncResponseParser private val syncResponseParser: InitialSyncResponseParser,
private val roomSyncEphemeralTemporaryStore: RoomSyncEphemeralTemporaryStore
) : SyncTask { ) : SyncTask {
private val workingDir = File(fileDirectory, "is") private val workingDir = File(fileDirectory, "is")
@ -102,13 +103,16 @@ internal class DefaultSyncTask @Inject constructor(
if (isInitialSync) { if (isInitialSync) {
Timber.v("INIT_SYNC with filter: ${requestParams["filter"]}") Timber.v("INIT_SYNC with filter: ${requestParams["filter"]}")
val initSyncStrategy = initialSyncStrategy val initSyncStrategy = initialSyncStrategy
var syncResp: SyncResponse? = null
logDuration("INIT_SYNC strategy: $initSyncStrategy") { logDuration("INIT_SYNC strategy: $initSyncStrategy") {
if (initSyncStrategy is InitialSyncStrategy.Optimized) { if (initSyncStrategy is InitialSyncStrategy.Optimized) {
roomSyncEphemeralTemporaryStore.reset()
workingDir.mkdirs()
val file = downloadInitSyncResponse(requestParams) val file = downloadInitSyncResponse(requestParams)
syncResp = reportSubtask(initialSyncProgressService, InitSyncStep.ImportingAccount, 1, 0.7F) { reportSubtask(initialSyncProgressService, InitSyncStep.ImportingAccount, 1, 0.7F) {
handleSyncFile(file, initSyncStrategy) handleSyncFile(file, initSyncStrategy)
} }
// Delete all files
workingDir.deleteRecursively()
} else { } else {
val syncResponse = logDuration("INIT_SYNC Request") { val syncResponse = logDuration("INIT_SYNC Request") {
executeRequest<SyncResponse>(globalErrorReceiver) { executeRequest<SyncResponse>(globalErrorReceiver) {
@ -125,15 +129,6 @@ internal class DefaultSyncTask @Inject constructor(
} }
} }
initialSyncProgressService.endAll() initialSyncProgressService.endAll()
if (initSyncStrategy is InitialSyncStrategy.Optimized) {
logDuration("INIT_SYNC Handle ephemeral") {
syncResponseHandler.handleInitSyncSecondTransaction(syncResp!!)
}
initialSyncStatusRepository.setStep(InitialSyncStatus.STEP_SUCCESS)
// Delete all files
workingDir.deleteRecursively()
}
} else { } else {
val syncResponse = executeRequest<SyncResponse>(globalErrorReceiver) { val syncResponse = executeRequest<SyncResponse>(globalErrorReceiver) {
apiCall = syncAPI.sync( apiCall = syncAPI.sync(
@ -147,7 +142,6 @@ internal class DefaultSyncTask @Inject constructor(
} }
private suspend fun downloadInitSyncResponse(requestParams: Map<String, String>): File { private suspend fun downloadInitSyncResponse(requestParams: Map<String, String>): File {
workingDir.mkdirs()
val workingFile = File(workingDir, "initSync.json") val workingFile = File(workingDir, "initSync.json")
val status = initialSyncStatusRepository.getStep() val status = initialSyncStatusRepository.getStep()
if (workingFile.exists() && status >= InitialSyncStatus.STEP_DOWNLOADED) { if (workingFile.exists() && status >= InitialSyncStatus.STEP_DOWNLOADED) {
@ -201,8 +195,8 @@ internal class DefaultSyncTask @Inject constructor(
} }
} }
private suspend fun handleSyncFile(workingFile: File, initSyncStrategy: InitialSyncStrategy.Optimized): SyncResponse { private suspend fun handleSyncFile(workingFile: File, initSyncStrategy: InitialSyncStrategy.Optimized) {
return logDuration("INIT_SYNC handleSyncFile()") { logDuration("INIT_SYNC handleSyncFile()") {
val syncResponse = logDuration("INIT_SYNC Read file and parse") { val syncResponse = logDuration("INIT_SYNC Read file and parse") {
syncResponseParser.parse(initSyncStrategy, workingFile) syncResponseParser.parse(initSyncStrategy, workingFile)
} }
@ -215,7 +209,7 @@ internal class DefaultSyncTask @Inject constructor(
logDuration("INIT_SYNC Database insertion") { logDuration("INIT_SYNC Database insertion") {
syncResponseHandler.handleResponse(syncResponse, null, initialSyncProgressService) syncResponseHandler.handleResponse(syncResponse, null, initialSyncProgressService)
} }
syncResponse initialSyncStatusRepository.setStep(InitialSyncStatus.STEP_SUCCESS)
} }
} }

View File

@ -16,28 +16,10 @@
package org.matrix.android.sdk.internal.session.sync.model package org.matrix.android.sdk.internal.session.sync.model
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.squareup.moshi.JsonReader
import okio.buffer
import okio.source
import java.io.File
@JsonClass(generateAdapter = false) @JsonClass(generateAdapter = false)
internal sealed class LazyRoomSyncEphemeral { internal sealed class LazyRoomSyncEphemeral {
data class Parsed(val _roomSyncEphemeral: RoomSyncEphemeral) : LazyRoomSyncEphemeral() data class Parsed(val _roomSyncEphemeral: RoomSyncEphemeral) : LazyRoomSyncEphemeral()
data class Stored(val roomSyncEphemeralAdapter: JsonAdapter<RoomSyncEphemeral>, val file: File) : LazyRoomSyncEphemeral() object Stored : LazyRoomSyncEphemeral()
val roomSyncEphemeral: RoomSyncEphemeral
get() {
return when (this) {
is Parsed -> _roomSyncEphemeral
is Stored -> {
// Parse the file now
file.inputStream().use { pos ->
roomSyncEphemeralAdapter.fromJson(JsonReader.of(pos.source().buffer()))!!
}
}
}
}
} }

View File

@ -22,11 +22,10 @@ import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter import com.squareup.moshi.JsonWriter
import com.squareup.moshi.ToJson import com.squareup.moshi.ToJson
import org.matrix.android.sdk.internal.session.sync.InitialSyncStrategy import org.matrix.android.sdk.internal.session.sync.InitialSyncStrategy
import org.matrix.android.sdk.internal.session.sync.RoomSyncEphemeralTemporaryStore
import org.matrix.android.sdk.internal.session.sync.model.LazyRoomSyncEphemeral import org.matrix.android.sdk.internal.session.sync.model.LazyRoomSyncEphemeral
import org.matrix.android.sdk.internal.session.sync.model.RoomSyncEphemeral import org.matrix.android.sdk.internal.session.sync.model.RoomSyncEphemeral
import timber.log.Timber import timber.log.Timber
import java.io.File
import java.util.concurrent.atomic.AtomicInteger
internal class DefaultLazyRoomSyncEphemeralJsonAdapter { internal class DefaultLazyRoomSyncEphemeralJsonAdapter {
@ -44,20 +43,15 @@ internal class DefaultLazyRoomSyncEphemeralJsonAdapter {
} }
} }
internal class SplitLazyRoomSyncJsonAdapter( internal class SplitLazyRoomSyncEphemeralJsonAdapter(
private val workingDirectory: File, private val roomSyncEphemeralTemporaryStore: RoomSyncEphemeralTemporaryStore,
private val syncStrategy: InitialSyncStrategy.Optimized private val syncStrategy: InitialSyncStrategy.Optimized
) { ) {
private val atomicInteger = AtomicInteger(0)
private fun createFile(): File {
val index = atomicInteger.getAndIncrement()
return File(workingDirectory, "room_$index.json")
}
@FromJson @FromJson
fun fromJson(reader: JsonReader, delegate: JsonAdapter<RoomSyncEphemeral>): LazyRoomSyncEphemeral? { fun fromJson(reader: JsonReader, delegate: JsonAdapter<RoomSyncEphemeral>): LazyRoomSyncEphemeral? {
val path = reader.path val path = reader.path
val roomId = path.substringAfter("\$.rooms.join.").substringBeforeLast(".ephemeral")
val json = reader.nextSource().inputStream().bufferedReader().use { val json = reader.nextSource().inputStream().bufferedReader().use {
it.readText() it.readText()
} }
@ -65,9 +59,8 @@ internal class SplitLazyRoomSyncJsonAdapter(
return if (json.length > limit) { return if (json.length > limit) {
Timber.v("INIT_SYNC $path content length: ${json.length} copy to a file") Timber.v("INIT_SYNC $path content length: ${json.length} copy to a file")
// Copy the source to a file // Copy the source to a file
val file = createFile() roomSyncEphemeralTemporaryStore.write(roomId, json)
file.writeText(json) LazyRoomSyncEphemeral.Stored
LazyRoomSyncEphemeral.Stored(delegate, file)
} else { } else {
Timber.v("INIT_SYNC $path content length: ${json.length} parse it now") Timber.v("INIT_SYNC $path content length: ${json.length} parse it now")
val roomSync = delegate.fromJson(json) ?: return null val roomSync = delegate.fromJson(json) ?: return null

View File

@ -20,29 +20,33 @@ import com.squareup.moshi.Moshi
import okio.buffer import okio.buffer
import okio.source import okio.source
import org.matrix.android.sdk.internal.session.sync.InitialSyncStrategy import org.matrix.android.sdk.internal.session.sync.InitialSyncStrategy
import org.matrix.android.sdk.internal.session.sync.RoomSyncEphemeralTemporaryStore
import org.matrix.android.sdk.internal.session.sync.model.SyncResponse import org.matrix.android.sdk.internal.session.sync.model.SyncResponse
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
internal class InitialSyncResponseParser @Inject constructor(private val moshi: Moshi) { internal class InitialSyncResponseParser @Inject constructor(
private val moshi: Moshi,
private val roomSyncEphemeralTemporaryStore: RoomSyncEphemeralTemporaryStore
) {
fun parse(syncStrategy: InitialSyncStrategy.Optimized, workingFile: File): SyncResponse { fun parse(syncStrategy: InitialSyncStrategy.Optimized, workingFile: File): SyncResponse {
val syncResponseLength = workingFile.length().toInt() val syncResponseLength = workingFile.length().toInt()
Timber.v("INIT_SYNC Sync file size is $syncResponseLength bytes") Timber.v("INIT_SYNC Sync file size is $syncResponseLength bytes")
val shouldSplit = syncResponseLength >= syncStrategy.minSizeToSplit val shouldSplit = syncResponseLength >= syncStrategy.minSizeToSplit
Timber.v("INIT_SYNC should split in several files: $shouldSplit") Timber.v("INIT_SYNC should split in several files: $shouldSplit")
return getMoshi(syncStrategy, workingFile.parentFile!!, shouldSplit) return getMoshi(syncStrategy, shouldSplit)
.adapter(SyncResponse::class.java) .adapter(SyncResponse::class.java)
.fromJson(workingFile.source().buffer())!! .fromJson(workingFile.source().buffer())!!
} }
private fun getMoshi(syncStrategy: InitialSyncStrategy.Optimized, workingDirectory: File, shouldSplit: Boolean): Moshi { private fun getMoshi(syncStrategy: InitialSyncStrategy.Optimized, shouldSplit: Boolean): Moshi {
// If we don't have to split we'll rely on the already default moshi // If we don't have to split we'll rely on the already default moshi
if (!shouldSplit) return moshi if (!shouldSplit) return moshi
// Otherwise, we create a new adapter for handling Map of Lazy sync // Otherwise, we create a new adapter for handling Map of Lazy sync
return moshi.newBuilder() return moshi.newBuilder()
.add(SplitLazyRoomSyncJsonAdapter(workingDirectory, syncStrategy)) .add(SplitLazyRoomSyncEphemeralJsonAdapter(roomSyncEphemeralTemporaryStore, syncStrategy))
.build() .build()
} }
} }

View File

@ -19,6 +19,18 @@ package org.matrix.android.sdk.internal.util
import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.BuildConfig
import timber.log.Timber import timber.log.Timber
internal fun <T> Collection<T>.logLimit(maxQuantity: Int = 5): String {
return buildString {
append(size)
append(" item(s)")
if (size > maxQuantity) {
append(", first $maxQuantity items")
}
append(": ")
append(this@logLimit.take(maxQuantity))
}
}
internal suspend fun <T> logDuration(message: String, internal suspend fun <T> logDuration(message: String,
block: suspend () -> T): T { block: suspend () -> T): T {
Timber.v("$message -- BEGIN") Timber.v("$message -- BEGIN")

View File

@ -14,7 +14,7 @@ kapt {
// Note: 2 digits max for each value // Note: 2 digits max for each value
ext.versionMajor = 1 ext.versionMajor = 1
ext.versionMinor = 1 ext.versionMinor = 1
ext.versionPatch = 1 ext.versionPatch = 2
static def getGitTimestamp() { static def getGitTimestamp() {
def cmd = 'git show -s --format=%ct' def cmd = 'git show -s --format=%ct'
@ -434,7 +434,7 @@ dependencies {
implementation "androidx.emoji:emoji-appcompat:1.1.0" implementation "androidx.emoji:emoji-appcompat:1.1.0"
implementation 'com.github.BillCarsonFr:JsonViewer:0.5' implementation 'com.github.BillCarsonFr:JsonViewer:0.6'
// WebRTC // WebRTC
// org.webrtc:google-webrtc is for development purposes only // org.webrtc:google-webrtc is for development purposes only
@ -476,7 +476,7 @@ dependencies {
// Plant Timber tree for test // Plant Timber tree for test
androidTestImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1' androidTestImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
// "The one who serves a great Espresso" // "The one who serves a great Espresso"
androidTestImplementation('com.schibsted.spain:barista:3.8.0') { androidTestImplementation('com.schibsted.spain:barista:3.9.0') {
exclude group: 'org.jetbrains.kotlin' exclude group: 'org.jetbrains.kotlin'
} }
} }

View File

@ -57,6 +57,7 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
super.bind(holder) super.bind(holder)
avatarRenderer.render(matrixItem, holder.avatar) avatarRenderer.render(matrixItem, holder.avatar)
holder.avatar.setOnClickListener { userClicked?.invoke() } holder.avatar.setOnClickListener { userClicked?.invoke() }
holder.sender.setOnClickListener { userClicked?.invoke() }
holder.sender.setTextOrHide(matrixItem.displayName) holder.sender.setTextOrHide(matrixItem.displayName)
holder.body.movementMethod = movementMethod holder.body.movementMethod = movementMethod
holder.body.text = body holder.body.text = body

View File

@ -58,7 +58,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
data class ResendMessage(val eventId: String) : RoomDetailAction() data class ResendMessage(val eventId: String) : RoomDetailAction()
data class RemoveFailedEcho(val eventId: String) : RoomDetailAction() data class RemoveFailedEcho(val eventId: String) : RoomDetailAction()
data class CancelSend(val eventId: String) : RoomDetailAction() data class CancelSend(val eventId: String, val force: Boolean) : RoomDetailAction()
data class ReplyToOptions(val eventId: String, val optionIndex: Int, val optionValue: String) : RoomDetailAction() data class ReplyToOptions(val eventId: String, val optionIndex: Int, val optionValue: String) : RoomDetailAction()

View File

@ -1570,14 +1570,18 @@ class RoomDetailFragment @Inject constructor(
} }
private fun handleCancelSend(action: EventSharedAction.Cancel) { private fun handleCancelSend(action: EventSharedAction.Cancel) {
AlertDialog.Builder(requireContext()) if (action.force) {
.setTitle(R.string.dialog_title_confirmation) roomDetailViewModel.handle(RoomDetailAction.CancelSend(action.eventId, true))
.setMessage(getString(R.string.event_status_cancel_sending_dialog_message)) } else {
.setNegativeButton(R.string.no, null) AlertDialog.Builder(requireContext())
.setPositiveButton(R.string.yes) { _, _ -> .setTitle(R.string.dialog_title_confirmation)
roomDetailViewModel.handle(RoomDetailAction.CancelSend(action.eventId)) .setMessage(getString(R.string.event_status_cancel_sending_dialog_message))
} .setNegativeButton(R.string.no, null)
.show() .setPositiveButton(R.string.yes) { _, _ ->
roomDetailViewModel.handle(RoomDetailAction.CancelSend(action.eventId, false))
}
.show()
}
} }
override fun onAvatarClicked(informationData: MessageInformationData) { override fun onAvatarClicked(informationData: MessageInformationData) {

View File

@ -1208,6 +1208,10 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
private fun handleCancel(action: RoomDetailAction.CancelSend) { private fun handleCancel(action: RoomDetailAction.CancelSend) {
if (action.force) {
room.cancelSend(action.eventId)
return
}
val targetEventId = action.eventId val targetEventId = action.eventId
room.getTimeLineEvent(targetEventId)?.let { room.getTimeLineEvent(targetEventId)?.let {
// State must be in one of the sending states // State must be in one of the sending states

View File

@ -63,7 +63,7 @@ sealed class EventSharedAction(@StringRes val titleRes: Int,
data class Redact(val eventId: String, val askForReason: Boolean) : data class Redact(val eventId: String, val askForReason: Boolean) :
EventSharedAction(R.string.message_action_item_redact, R.drawable.ic_delete, true) EventSharedAction(R.string.message_action_item_redact, R.drawable.ic_delete, true)
data class Cancel(val eventId: String) : data class Cancel(val eventId: String, val force: Boolean) :
EventSharedAction(R.string.cancel, R.drawable.ic_close_round) EventSharedAction(R.string.cancel, R.drawable.ic_close_round)
data class ViewSource(val content: String) : data class ViewSource(val content: String) :

View File

@ -73,12 +73,19 @@ class MessageActionsEpoxyController @Inject constructor(
text(stringProvider.getString(R.string.unable_to_send_message)) text(stringProvider.getString(R.string.unable_to_send_message))
drawableStart(R.drawable.ic_warning_badge) drawableStart(R.drawable.ic_warning_badge)
} }
} else if (sendState != SendState.SYNCED) { } else if (sendState?.isSending().orFalse()) {
bottomSheetSendStateItem { bottomSheetSendStateItem {
id("send_state") id("send_state")
showProgress(true) showProgress(true)
text(stringProvider.getString(R.string.event_status_sending_message)) text(stringProvider.getString(R.string.event_status_sending_message))
} }
} else if (sendState == SendState.SENT) {
bottomSheetSendStateItem {
id("send_state")
showProgress(false)
drawableStart(R.drawable.ic_message_sent)
text(stringProvider.getString(R.string.event_status_sent_message))
}
} }
when (state.informationData.e2eDecoration) { when (state.informationData.e2eDecoration) {
@ -124,9 +131,11 @@ class MessageActionsEpoxyController @Inject constructor(
} }
} }
// Separator if (state.actions.isNotEmpty()) {
dividerItem { // Separator
id("actions_separator") dividerItem {
id("actions_separator")
}
} }
// Action // Action

View File

@ -250,6 +250,9 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
timelineEvent.root.sendState == SendState.SYNCED -> { timelineEvent.root.sendState == SendState.SYNCED -> {
addActionsForSyncedState(timelineEvent, actionPermissions, messageContent, msgType) addActionsForSyncedState(timelineEvent, actionPermissions, messageContent, msgType)
} }
timelineEvent.root.sendState == SendState.SENT -> {
addActionsForSentNotSyncedState(timelineEvent)
}
} }
} }
} }
@ -287,10 +290,22 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
private fun ArrayList<EventSharedAction>.addActionsForSendingState(timelineEvent: TimelineEvent) { private fun ArrayList<EventSharedAction>.addActionsForSendingState(timelineEvent: TimelineEvent) {
// TODO is uploading attachment? // TODO is uploading attachment?
if (canCancel(timelineEvent)) { if (canCancel(timelineEvent)) {
add(EventSharedAction.Cancel(timelineEvent.eventId)) add(EventSharedAction.Cancel(timelineEvent.eventId, false))
} }
} }
private fun ArrayList<EventSharedAction>.addActionsForSentNotSyncedState(timelineEvent: TimelineEvent) {
// If sent but not synced (synapse stuck at bottom bug)
// Still offer action to cancel (will only remove local echo)
timelineEvent.root.eventId?.let {
add(EventSharedAction.Cancel(it, true))
}
// TODO Can be redacted
// TODO sent by me or sufficient power level
}
private fun ArrayList<EventSharedAction>.addActionsForSyncedState(timelineEvent: TimelineEvent, private fun ArrayList<EventSharedAction>.addActionsForSyncedState(timelineEvent: TimelineEvent,
actionPermissions: ActionPermissions, actionPermissions: ActionPermissions,
messageContent: MessageContent?, messageContent: MessageContent?,
@ -337,12 +352,6 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
if (canSave(msgType) && messageContent is MessageWithAttachmentContent) { if (canSave(msgType) && messageContent is MessageWithAttachmentContent) {
add(EventSharedAction.Save(timelineEvent.eventId, messageContent)) add(EventSharedAction.Save(timelineEvent.eventId, messageContent))
} }
if (timelineEvent.root.sendState == SendState.SENT) {
// TODO Can be redacted
// TODO sent by me or sufficient power level
}
} }
if (vectorPreferences.developerMode()) { if (vectorPreferences.developerMode()) {

View File

@ -92,7 +92,8 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
return RoomSummaryItem_() return RoomSummaryItem_()
.id(roomSummary.roomId) .id(roomSummary.roomId)
.avatarRenderer(avatarRenderer) .avatarRenderer(avatarRenderer)
.encryptionTrustLevel(roomSummary.roomEncryptionTrustLevel) // We do not display shield in the room list anymore
// .encryptionTrustLevel(roomSummary.roomEncryptionTrustLevel)
.matrixItem(roomSummary.toMatrixItem()) .matrixItem(roomSummary.toMatrixItem())
.lastEventTime(latestEventTime) .lastEventTime(latestEventTime)
.typingMessage(typingMessage) .typingMessage(typingMessage)

View File

@ -29,7 +29,6 @@ import androidx.core.view.ViewCompat
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.transition.Transition import androidx.transition.Transition
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
@ -132,7 +131,7 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen
if (savedInstanceState == null) { if (savedInstanceState == null) {
pager2.setCurrentItem(initialIndex, false) pager2.setCurrentItem(initialIndex, false)
// The page change listener is not notified of the change... // The page change listener is not notified of the change...
lifecycleScope.launchWhenResumed { pager2.post {
onSelectedPositionChanged(initialIndex) onSelectedPositionChanged(initialIndex)
} }
} }

View File

@ -4,7 +4,8 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/bottom_sheet_message_preview" android:id="@+id/bottom_sheet_message_preview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
tools:background="#1FF00FF0">
<ImageView <ImageView
android:id="@+id/bottom_sheet_message_preview_avatar" android:id="@+id/bottom_sheet_message_preview_avatar"
@ -13,7 +14,7 @@
android:layout_margin="@dimen/layout_horizontal_margin" android:layout_margin="@dimen/layout_horizontal_margin"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:background="@drawable/circle" android:background="@drawable/circle"
android:contentDescription="@string/avatar" android:importantForAccessibility="no"
android:scaleType="centerCrop" android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@ -23,17 +24,17 @@
<TextView <TextView
android:id="@+id/bottom_sheet_message_preview_sender" android:id="@+id/bottom_sheet_message_preview_sender"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin" android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:ellipsize="end" android:ellipsize="end"
android:fontFamily="sans-serif-bold" android:fontFamily="sans-serif-bold"
android:singleLine="true" android:singleLine="true"
android:textColor="@color/riotx_accent" android:textColor="?riotx_text_primary"
android:textSize="14sp" android:textSize="14sp"
android:textStyle="bold" android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@+id/bottom_sheet_message_preview_timestamp"
app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar" app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar"
app:layout_constraintTop_toTopOf="@id/bottom_sheet_message_preview_avatar" app:layout_constraintTop_toTopOf="@id/bottom_sheet_message_preview_avatar"
tools:text="@tools:sample/full_names" /> tools:text="@tools:sample/full_names" />
@ -45,15 +46,14 @@
android:layout_marginEnd="@dimen/layout_horizontal_margin" android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:textColor="?riotx_text_secondary" android:textColor="?riotx_text_secondary"
android:textSize="12sp" android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="@id/bottom_sheet_message_preview_sender" app:layout_constraintBottom_toBottomOf="@id/bottom_sheet_message_preview_sender"
app:layout_constraintEnd_toEndOf="parent"
tools:text="Friday 8pm" /> tools:text="Friday 8pm" />
<TextView <TextView
android:id="@+id/bottom_sheet_message_preview_body" android:id="@+id/bottom_sheet_message_preview_body"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin" android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginBottom="4dp" android:layout_marginBottom="4dp"
@ -62,8 +62,10 @@
android:textColor="?riotx_text_secondary" android:textColor="?riotx_text_secondary"
android:textIsSelectable="false" android:textIsSelectable="false"
android:textSize="14sp" android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar" app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar"
app:layout_constraintTop_toBottomOf="@id/bottom_sheet_message_preview_sender" app:layout_constraintTop_toBottomOf="@id/bottom_sheet_message_preview_sender"
tools:text="Quis harum id autem cumque consequatur laboriosam aliquam sed. Sint accusamus dignissimos nobis ullam earum debitis aspernatur. Sint accusamus dignissimos nobis ullam earum debitis aspernatur. " /> tools:text="Quis harum id autem cumque consequatur laboriosam aliquam sed. Sint accusamus dignissimos nobis ullam earum debitis aspernatur. Sint accusamus dignissimos nobis ullam earum debitis aspernatur. " />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -208,6 +208,8 @@
<string name="initial_sync_start_importing_account_groups">Initial Sync:\nImporting Communities</string> <string name="initial_sync_start_importing_account_groups">Initial Sync:\nImporting Communities</string>
<string name="initial_sync_start_importing_account_data">Initial Sync:\nImporting Account Data</string> <string name="initial_sync_start_importing_account_data">Initial Sync:\nImporting Account Data</string>
<string name="event_status_sent_message">Message sent</string>
<string name="event_status_sending_message">Sending message…</string> <string name="event_status_sending_message">Sending message…</string>
<string name="clear_timeline_send_queue">Clear sending queue</string> <string name="clear_timeline_send_queue">Clear sending queue</string>