mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-25 15:08:12 +08:00
Merge branch 'release/1.1.2'
This commit is contained in:
commit
ab260e79eb
12
.github/ISSUE_TEMPLATE/bug_report.md
vendored
12
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -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]
|
||||||
|
|
||||||
|
8
.github/ISSUE_TEMPLATE/feature_request.md
vendored
8
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -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.
|
||||||
|
25
CHANGES.md
25
CHANGES.md
@ -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
|
||||||
|
|
||||||
|
2
fastlane/metadata/android/en-US/changelogs/40101020.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40101020.txt
Normal 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
|
@ -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
|
||||||
|
@ -856,17 +856,10 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
val params = LoadRoomMembersTask.Params(roomId)
|
|
||||||
try {
|
|
||||||
loadRoomMembersTask.execute(params)
|
|
||||||
} catch (throwable: Throwable) {
|
|
||||||
Timber.e(throwable, "## CRYPTO | onRoomEncryptionEvent ERROR FAILED TO SETUP CRYPTO ")
|
|
||||||
} finally {
|
|
||||||
val userIds = getRoomUserIds(roomId)
|
val userIds = getRoomUserIds(roomId)
|
||||||
setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds)
|
setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun getRoomUserIds(roomId: String): List<String> {
|
private fun getRoomUserIds(roomId: String): List<String> {
|
||||||
val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser()
|
val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser()
|
||||||
|
@ -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,14 +127,24 @@ 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()
|
||||||
|
|
||||||
@ -146,7 +160,6 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
|||||||
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the devices list statuses
|
* Update the devices list statuses
|
||||||
@ -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()) {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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"]) {
|
||||||
|
@ -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,31 +87,38 @@ 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()) {
|
||||||
|
// Unfortunately we don't have much info on what did exactly changed (is it the cross signing keys of that user,
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cleanup(params)
|
cleanup(params)
|
||||||
return Result.success()
|
return Result.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unfortunately we don't have much info on what did exactly changed (is it the cross signing keys of that user,
|
private suspend fun updateTrust(userListParam: List<String>,
|
||||||
// or a new device?) So we check all again :/
|
cRealm: Realm) {
|
||||||
|
var userList = userListParam
|
||||||
Timber.d("## CrossSigning - Updating trust for $userList")
|
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
|
||||||
var myCrossSigningInfo = realm.where(CrossSigningInfoEntity::class.java)
|
myCrossSigningInfo = getCrossSigningInfo(cryptoRealm, myUserId)
|
||||||
.equalTo(CrossSigningInfoEntityFields.USER_ID, myUserId)
|
|
||||||
.findFirst()?.let { mapCrossSigningInfoEntity(it) }
|
|
||||||
|
|
||||||
var myTrustResult: UserTrustResult? = null
|
var myTrustResult: UserTrustResult? = null
|
||||||
|
|
||||||
@ -112,67 +127,58 @@ internal class UpdateTrustWorker(context: Context,
|
|||||||
// 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)
|
||||||
myTrustResult = crossSigningService.checkSelfTrust(myCrossSigningInfo, myDevices).also {
|
updateCrossSigningKeysTrust(cryptoRealm, myUserId, myTrustResult.isVerified())
|
||||||
updateCrossSigningKeysTrust(realm, myUserId, it.isVerified())
|
|
||||||
// update model reference
|
// update model reference
|
||||||
myCrossSigningInfo = realm.where(CrossSigningInfoEntity::class.java)
|
myCrossSigningInfo = getCrossSigningInfo(cryptoRealm, myUserId)
|
||||||
.equalTo(CrossSigningInfoEntityFields.USER_ID, myUserId)
|
|
||||||
.findFirst()?.let { mapCrossSigningInfoEntity(it) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val otherInfos = userList.map {
|
val otherInfos = userList.associateWith { userId ->
|
||||||
it to realm.where(CrossSigningInfoEntity::class.java)
|
getCrossSigningInfo(cryptoRealm, userId)
|
||||||
.equalTo(CrossSigningInfoEntityFields.USER_ID, it)
|
|
||||||
.findFirst()?.let { mapCrossSigningInfoEntity(it) }
|
|
||||||
}
|
}
|
||||||
.toMap()
|
|
||||||
|
|
||||||
val trusts = otherInfos.map { infoEntry ->
|
val trusts = otherInfos.mapValues { entry ->
|
||||||
infoEntry.key to when (infoEntry.key) {
|
when (entry.key) {
|
||||||
myUserId -> myTrustResult
|
myUserId -> myTrustResult
|
||||||
else -> {
|
else -> {
|
||||||
crossSigningService.checkOtherMSKTrusted(myCrossSigningInfo, infoEntry.value).also {
|
crossSigningService.checkOtherMSKTrusted(myCrossSigningInfo, entry.value).also {
|
||||||
Timber.d("## CrossSigning - user:${infoEntry.key} result:$it")
|
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..
|
// Ok so now we have to check device trust for all these users..
|
||||||
Timber.v("## CrossSigning - Updating devices cross trust users ${trusts.keys}")
|
Timber.v("## CrossSigning - Updating devices cross trust users: ${trusts.keys.logLimit()}")
|
||||||
trusts.keys.forEach {
|
trusts.keys.forEach { userId ->
|
||||||
val devicesEntities = realm.where<UserEntity>()
|
val devicesEntities = cryptoRealm.where<UserEntity>()
|
||||||
.equalTo(UserEntityFields.USER_ID, it)
|
.equalTo(UserEntityFields.USER_ID, userId)
|
||||||
.findFirst()
|
.findFirst()
|
||||||
?.devices
|
?.devices
|
||||||
|
|
||||||
val trustMap = devicesEntities?.map { device ->
|
val trustMap = devicesEntities?.associateWith { device ->
|
||||||
// get up to date from DB has could have been updated
|
// get up to date from DB has could have been updated
|
||||||
val otherInfo = realm.where(CrossSigningInfoEntity::class.java)
|
val otherInfo = getCrossSigningInfo(cryptoRealm, userId)
|
||||||
.equalTo(CrossSigningInfoEntityFields.USER_ID, it)
|
crossSigningService.checkDeviceTrust(myCrossSigningInfo, otherInfo, CryptoMapper.mapToModel(device))
|
||||||
.findFirst()?.let { mapCrossSigningInfoEntity(it) }
|
}
|
||||||
device to crossSigningService.checkDeviceTrust(myCrossSigningInfo, otherInfo, CryptoMapper.mapToModel(device))
|
|
||||||
}?.toMap()
|
|
||||||
|
|
||||||
// Update trust if needed
|
// Update trust if needed
|
||||||
devicesEntities?.forEach { device ->
|
devicesEntities?.forEach { device ->
|
||||||
@ -183,10 +189,9 @@ internal class UpdateTrustWorker(context: Context,
|
|||||||
// need to save
|
// need to save
|
||||||
val trustEntity = device.trustLevelEntity
|
val trustEntity = device.trustLevelEntity
|
||||||
if (trustEntity == null) {
|
if (trustEntity == null) {
|
||||||
realm.createObject(TrustLevelEntity::class.java).let {
|
device.trustLevelEntity = cryptoRealm.createObject(TrustLevelEntity::class.java).also {
|
||||||
it.locallyVerified = false
|
it.locallyVerified = false
|
||||||
it.crossSignedVerified = crossSignedVerified
|
it.crossSignedVerified = crossSignedVerified
|
||||||
device.trustLevelEntity = it
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
trustEntity.crossSignedVerified = crossSignedVerified
|
trustEntity.crossSignedVerified = crossSignedVerified
|
||||||
@ -195,27 +200,32 @@ internal class UpdateTrustWorker(context: Context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// So Cross Signing keys trust is updated, device trust is updated
|
// So Cross Signing keys trust is updated, device trust is updated
|
||||||
// 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 }
|
||||||
Timber.d("## CrossSigning - ... impacted rooms $distinctRoomIds")
|
.also { Timber.d("## CrossSigning - ... impacted rooms ${it.logLimit()}") }
|
||||||
distinctRoomIds.forEach { roomId ->
|
.forEach { roomId ->
|
||||||
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
|
RoomSummaryEntity.where(sessionRealm, roomId)
|
||||||
if (roomSummary?.isEncrypted == true) {
|
.equalTo(RoomSummaryEntityFields.IS_ENCRYPTED, true)
|
||||||
|
.findFirst()
|
||||||
|
?.let { roomSummary ->
|
||||||
Timber.d("## CrossSigning - Check shield state for room $roomId")
|
Timber.d("## CrossSigning - Check shield state for room $roomId")
|
||||||
val allActiveRoomMembers = RoomMemberHelper(realm, roomId).getActiveRoomMemberIds()
|
val allActiveRoomMembers = RoomMemberHelper(sessionRealm, roomId).getActiveRoomMemberIds()
|
||||||
try {
|
try {
|
||||||
val updatedTrust = computeRoomShield(allActiveRoomMembers, roomSummary)
|
val updatedTrust = computeRoomShield(
|
||||||
|
myCrossSigningInfo,
|
||||||
|
cRealm,
|
||||||
|
allActiveRoomMembers,
|
||||||
|
roomSummary
|
||||||
|
)
|
||||||
if (roomSummary.roomEncryptionTrustLevel != updatedTrust) {
|
if (roomSummary.roomEncryptionTrustLevel != updatedTrust) {
|
||||||
Timber.d("## CrossSigning - Shield change detected for $roomId -> $updatedTrust")
|
Timber.d("## CrossSigning - Shield change detected for $roomId -> $updatedTrust")
|
||||||
roomSummary.roomEncryptionTrustLevel = updatedTrust
|
roomSummary.roomEncryptionTrustLevel = updatedTrust
|
||||||
@ -228,8 +238,11 @@ internal class UpdateTrustWorker(context: Context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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,20 +250,21 @@ 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
|
||||||
|
?.forEach { info ->
|
||||||
// optimization to avoid trigger updates when there is no change..
|
// optimization to avoid trigger updates when there is no change..
|
||||||
if (info.trustLevelEntity?.isVerified() != verified) {
|
if (info.trustLevelEntity?.isVerified() != verified) {
|
||||||
Timber.d("## CrossSigning - Trust change for $userId : $verified")
|
Timber.d("## CrossSigning - Trust change for $userId : $verified")
|
||||||
val level = info.trustLevelEntity
|
val level = info.trustLevelEntity
|
||||||
if (level == null) {
|
if (level == null) {
|
||||||
val newLevel = realm.createObject(TrustLevelEntity::class.java)
|
info.trustLevelEntity = cryptoRealm.createObject(TrustLevelEntity::class.java).also {
|
||||||
newLevel.locallyVerified = verified
|
it.locallyVerified = verified
|
||||||
newLevel.crossSignedVerified = verified
|
it.crossSignedVerified = verified
|
||||||
info.trustLevelEntity = newLevel
|
}
|
||||||
} else {
|
} else {
|
||||||
level.locallyVerified = verified
|
level.locallyVerified = verified
|
||||||
level.crossSignedVerified = verified
|
level.crossSignedVerified = verified
|
||||||
@ -259,8 +273,11 @@ internal class UpdateTrustWorker(context: Context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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,16 +289,7 @@ 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()) {
|
||||||
@ -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 {
|
?.map { CryptoMapper.mapToModel(it) }
|
||||||
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
|
||||||
|
@ -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
|
||||||
|
@ -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>) {
|
||||||
|
@ -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 {
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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,9 +38,11 @@ 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(
|
||||||
|
@Assisted private val roomId: String,
|
||||||
@SessionDatabase private val monarchy: Monarchy,
|
@SessionDatabase private val monarchy: Monarchy,
|
||||||
private val realmSessionProvider: RealmSessionProvider,
|
private val realmSessionProvider: RealmSessionProvider,
|
||||||
private val timelineInput: TimelineInput,
|
private val timelineInput: TimelineInput,
|
||||||
@ -51,7 +53,8 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
|
|||||||
private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
|
private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
|
||||||
private val timelineEventMapper: TimelineEventMapper,
|
private val timelineEventMapper: TimelineEventMapper,
|
||||||
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
|
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
|
||||||
private val loadRoomMembersTask: LoadRoomMembersTask
|
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> {
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
*/
|
*/
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
@ -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 -> {
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -41,13 +41,15 @@ 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(
|
||||||
|
@SessionDatabase private val monarchy: Monarchy,
|
||||||
@SessionId private val sessionId: String,
|
@SessionId private val sessionId: String,
|
||||||
private val workManagerProvider: WorkManagerProvider,
|
private val workManagerProvider: WorkManagerProvider,
|
||||||
private val roomSyncHandler: RoomSyncHandler,
|
private val roomSyncHandler: RoomSyncHandler,
|
||||||
private val userAccountDataSyncHandler: UserAccountDataSyncHandler,
|
private val userAccountDataSyncHandler: UserAccountDataSyncHandler,
|
||||||
private val groupSyncHandler: GroupSyncHandler,
|
private val groupSyncHandler: GroupSyncHandler,
|
||||||
private val cryptoSyncHandler: CryptoSyncHandler,
|
private val cryptoSyncHandler: CryptoSyncHandler,
|
||||||
|
private val aggregatorHandler: SyncResponsePostTreatmentAggregatorHandler,
|
||||||
private val cryptoService: DefaultCryptoService,
|
private val cryptoService: DefaultCryptoService,
|
||||||
private val tokenStore: SyncTokenStore,
|
private val tokenStore: SyncTokenStore,
|
||||||
private val processEventForPushTask: ProcessEventForPushTask,
|
private val processEventForPushTask: ProcessEventForPushTask,
|
||||||
@ -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.
|
||||||
|
@ -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>()
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()))!!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
|
@ -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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -1570,15 +1570,19 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleCancelSend(action: EventSharedAction.Cancel) {
|
private fun handleCancelSend(action: EventSharedAction.Cancel) {
|
||||||
|
if (action.force) {
|
||||||
|
roomDetailViewModel.handle(RoomDetailAction.CancelSend(action.eventId, true))
|
||||||
|
} else {
|
||||||
AlertDialog.Builder(requireContext())
|
AlertDialog.Builder(requireContext())
|
||||||
.setTitle(R.string.dialog_title_confirmation)
|
.setTitle(R.string.dialog_title_confirmation)
|
||||||
.setMessage(getString(R.string.event_status_cancel_sending_dialog_message))
|
.setMessage(getString(R.string.event_status_cancel_sending_dialog_message))
|
||||||
.setNegativeButton(R.string.no, null)
|
.setNegativeButton(R.string.no, null)
|
||||||
.setPositiveButton(R.string.yes) { _, _ ->
|
.setPositiveButton(R.string.yes) { _, _ ->
|
||||||
roomDetailViewModel.handle(RoomDetailAction.CancelSend(action.eventId))
|
roomDetailViewModel.handle(RoomDetailAction.CancelSend(action.eventId, false))
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onAvatarClicked(informationData: MessageInformationData) {
|
override fun onAvatarClicked(informationData: MessageInformationData) {
|
||||||
// roomDetailViewModel.handle(RoomDetailAction.RequestVerification(informationData.userId))
|
// roomDetailViewModel.handle(RoomDetailAction.RequestVerification(informationData.userId))
|
||||||
|
@ -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
|
||||||
|
@ -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) :
|
||||||
|
@ -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,10 +131,12 @@ class MessageActionsEpoxyController @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.actions.isNotEmpty()) {
|
||||||
// Separator
|
// Separator
|
||||||
dividerItem {
|
dividerItem {
|
||||||
id("actions_separator")
|
id("actions_separator")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Action
|
// Action
|
||||||
state.actions.forEachIndexed { index, action ->
|
state.actions.forEachIndexed { index, action ->
|
||||||
|
@ -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()) {
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user