Merge branch 'develop' into feature/room_profile

This commit is contained in:
ganfra 2020-01-09 11:56:09 +01:00
commit f18ec8d021
244 changed files with 5051 additions and 2700 deletions

View File

@ -5,13 +5,26 @@ Features ✨:
-
Improvements 🙌:
-
- The initial sync is now handled by a foreground service
- Render aliases and canonical alias change in the timeline
- Fix autocompletion issues and add support for rooms and groups
- Introduce developer mode in the settings (#745, #796)
- Improve devices list screen
- Add settings for rageshake sensibility
- Fix autocompletion issues and add support for rooms, groups, and emoji (#780)
- Show skip to bottom FAB while scrolling down (#752)
Other changes:
-
- Change the way RiotX identifies a session to allow the SDK to support several sessions with the same user (#800)
- Exclude play-services-oss-licenses library from F-Droid build (#814)
Bugfix 🐛:
-
- Fix crash when opening room creation screen from the room filtering screen
- Fix avatar image disappearing (#777)
- Fix read marker banner when permalink
- Fix joining upgraded rooms (#697)
- Fix matrix.org room directory not being browsable (#807)
- Hide non working settings (#751)
Translations 🗣:
-

View File

@ -17,13 +17,16 @@
package im.vector.matrix.rx
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.session.room.model.ReadReceipt
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.notification.RoomNotificationState
import im.vector.matrix.android.api.session.room.send.UserDraft
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional
import io.reactivex.Observable
import io.reactivex.Single
@ -31,18 +34,22 @@ class RxRoom(private val room: Room) {
fun liveRoomSummary(): Observable<Optional<RoomSummary>> {
return room.getRoomSummaryLive().asObservable()
.startWith(room.roomSummary().toOptional())
}
fun liveRoomMemberIds(): Observable<List<String>> {
return room.getRoomMemberIdsLive().asObservable()
fun liveRoomMembers(queryParams: RoomMemberQueryParams): Observable<List<RoomMember>> {
return room.getRoomMembersLive(queryParams).asObservable()
.startWith(room.getRoomMembers(queryParams))
}
fun liveAnnotationSummary(eventId: String): Observable<Optional<EventAnnotationsSummary>> {
return room.getEventSummaryLive(eventId).asObservable()
return room.getEventAnnotationsSummaryLive(eventId).asObservable()
.startWith(room.getEventAnnotationsSummary(eventId).toOptional())
}
fun liveTimelineEvent(eventId: String): Observable<Optional<TimelineEvent>> {
return room.getTimeLineEventLive(eventId).asObservable()
.startWith(room.getTimeLineEvent(eventId).toOptional())
}
fun liveReadMarker(): Observable<Optional<String>> {

View File

@ -18,8 +18,10 @@ package im.vector.matrix.rx
import androidx.paging.PagedList
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.group.GroupSummaryQueryParams
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.pushers.Pusher
import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.sync.SyncState
@ -30,40 +32,43 @@ import io.reactivex.Single
class RxSession(private val session: Session) {
fun liveRoomSummaries(): Observable<List<RoomSummary>> {
return session.liveRoomSummaries().asObservable()
fun liveRoomSummaries(queryParams: RoomSummaryQueryParams): Observable<List<RoomSummary>> {
return session.getRoomSummariesLive(queryParams).asObservable()
.startWith(session.getRoomSummaries(queryParams))
}
fun liveGroupSummaries(): Observable<List<GroupSummary>> {
return session.liveGroupSummaries().asObservable()
fun liveGroupSummaries(queryParams: GroupSummaryQueryParams): Observable<List<GroupSummary>> {
return session.getGroupSummariesLive(queryParams).asObservable()
.startWith(session.getGroupSummaries(queryParams))
}
fun liveBreadcrumbs(): Observable<List<RoomSummary>> {
return session.liveBreadcrumbs().asObservable()
return session.getBreadcrumbsLive().asObservable()
.startWith(session.getBreadcrumbs())
}
fun liveSyncState(): Observable<SyncState> {
return session.syncState().asObservable()
return session.getSyncStateLive().asObservable()
}
fun livePushers(): Observable<List<Pusher>> {
return session.livePushers().asObservable()
return session.getPushersLive().asObservable()
}
fun liveUser(userId: String): Observable<Optional<User>> {
return session.liveUser(userId).asObservable().distinctUntilChanged()
return session.getUserLive(userId).asObservable().distinctUntilChanged()
}
fun liveUsers(): Observable<List<User>> {
return session.liveUsers().asObservable()
return session.getUsersLive().asObservable()
}
fun liveIgnoredUsers(): Observable<List<User>> {
return session.liveIgnoredUsers().asObservable()
return session.getIgnoredUsersLive().asObservable()
}
fun livePagedUsers(filter: String? = null): Observable<PagedList<User>> {
return session.livePagedUsers(filter).asObservable()
return session.getPagedUsersLive(filter).asObservable()
}
fun createRoom(roomParams: CreateRoomParams): Single<String> = singleBuilder {

View File

@ -10,7 +10,7 @@ buildscript {
jcenter()
}
dependencies {
classpath "io.realm:realm-gradle-plugin:5.12.0"
classpath "io.realm:realm-gradle-plugin:6.0.2"
}
}
@ -102,7 +102,6 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
implementation "androidx.appcompat:appcompat:1.1.0"
implementation "androidx.recyclerview:recyclerview:1.1.0-beta05"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
@ -119,14 +118,14 @@ dependencies {
implementation "ru.noties.markwon:core:$markwon_version"
// Image
implementation 'androidx.exifinterface:exifinterface:1.0.0'
implementation 'androidx.exifinterface:exifinterface:1.1.0'
// Database
implementation 'com.github.Zhuinden:realm-monarchy:0.5.1'
kapt 'dk.ilios:realmfieldnameshelper:1.1.1'
// Work
implementation "androidx.work:work-runtime-ktx:2.3.0-alpha01"
implementation "androidx.work:work-runtime-ktx:2.3.0-beta02"
// FP
implementation "io.arrow-kt:arrow-core:$arrow_version"

View File

@ -19,4 +19,4 @@ package im.vector.matrix.android
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.Dispatchers.Main
internal val testCoroutineDispatchers = MatrixCoroutineDispatchers(Main, Main, Main, Main, Main)
internal val testCoroutineDispatchers = MatrixCoroutineDispatchers(Main, Main, Main, Main)

View File

@ -1,60 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.auth
import androidx.test.annotation.UiThreadTest
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.GrantPermissionRule
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.OkReplayRuleChainNoActivity
import im.vector.matrix.android.api.auth.AuthenticationService
import okreplay.*
import org.junit.ClassRule
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
internal class AuthenticationServiceTest : InstrumentedTest {
lateinit var authenticationService: AuthenticationService
lateinit var okReplayInterceptor: OkReplayInterceptor
private val okReplayConfig = OkReplayConfig.Builder()
.tapeRoot(AndroidTapeRoot(
context(), javaClass))
.defaultMode(TapeMode.READ_WRITE) // or TapeMode.READ_ONLY
.sslEnabled(true)
.interceptor(okReplayInterceptor)
.build()
@get:Rule
val testRule = OkReplayRuleChainNoActivity(okReplayConfig).get()
@Test
@UiThreadTest
@OkReplay(tape = "auth", mode = TapeMode.READ_WRITE)
fun auth() {
}
companion object {
@ClassRule
@JvmField
val grantExternalStoragePermissionRule: GrantPermissionRule =
GrantPermissionRule.grant(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
}

View File

@ -16,20 +16,31 @@
package im.vector.matrix.android.internal.crypto
import androidx.test.ext.junit.runners.AndroidJUnit4
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import io.realm.Realm
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.matrix.olm.OlmAccount
import org.matrix.olm.OlmManager
import org.matrix.olm.OlmSession
private const val DUMMY_DEVICE_KEY = "DeviceKey"
class CryptoStoreTest {
@RunWith(AndroidJUnit4::class)
class CryptoStoreTest : InstrumentedTest {
private val cryptoStoreHelper = CryptoStoreHelper()
@Before
fun setup() {
Realm.init(context())
}
@Test
fun test_metadata_realm_ok() {
val cryptoStore: IMXCryptoStore = cryptoStoreHelper.createStore()

View File

@ -19,8 +19,12 @@ package im.vector.matrix.android.session.room.timeline
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.internal.database.helper.*
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.internal.database.helper.add
import im.vector.matrix.android.internal.database.helper.lastStateIndex
import im.vector.matrix.android.internal.database.helper.merge
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.SessionRealmModule
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeListOfEvents
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeMessageEvent
@ -28,7 +32,6 @@ import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeR
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.kotlin.createObject
import org.amshove.kluent.shouldBeFalse
import org.amshove.kluent.shouldBeTrue
import org.amshove.kluent.shouldEqual
import org.junit.Before
@ -43,7 +46,11 @@ internal class ChunkEntityTest : InstrumentedTest {
@Before
fun setup() {
Realm.init(context())
val testConfig = RealmConfiguration.Builder().inMemory().name("test-realm").build()
val testConfig = RealmConfiguration.Builder()
.inMemory()
.name("test-realm")
.modules(SessionRealmModule())
.build()
monarchy = Monarchy.Builder().setRealmConfiguration(testConfig).build()
}
@ -141,30 +148,6 @@ internal class ChunkEntityTest : InstrumentedTest {
}
}
@Test
fun merge_shouldEventsBeLinked_whenMergingLinkedWithUnlinked() {
monarchy.runTransactionSync { realm ->
val chunk1: ChunkEntity = realm.createObject()
val chunk2: ChunkEntity = realm.createObject()
chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = false)
chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS)
chunk1.isUnlinked().shouldBeFalse()
}
}
@Test
fun merge_shouldEventsBeUnlinked_whenMergingUnlinkedWithUnlinked() {
monarchy.runTransactionSync { realm ->
val chunk1: ChunkEntity = realm.createObject()
val chunk2: ChunkEntity = realm.createObject()
chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS)
chunk1.isUnlinked().shouldBeTrue()
}
}
@Test
fun merge_shouldPrevTokenMerged_whenMergingForwards() {
monarchy.runTransactionSync { realm ->
@ -172,8 +155,8 @@ internal class ChunkEntityTest : InstrumentedTest {
val chunk2: ChunkEntity = realm.createObject()
val prevToken = "prev_token"
chunk1.prevToken = prevToken
chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk1.merge("roomId", chunk2, PaginationDirection.FORWARDS)
chunk1.prevToken shouldEqual prevToken
}
@ -186,10 +169,19 @@ internal class ChunkEntityTest : InstrumentedTest {
val chunk2: ChunkEntity = realm.createObject()
val nextToken = "next_token"
chunk1.nextToken = nextToken
chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS)
chunk1.nextToken shouldEqual nextToken
}
}
private fun ChunkEntity.addAll(roomId: String,
events: List<Event>,
direction: PaginationDirection,
stateIndexOffset: Int = 0) {
events.forEach { event ->
add(roomId, event, direction, stateIndexOffset)
}
}
}

View File

@ -16,7 +16,6 @@
package im.vector.matrix.android.session.room.timeline
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
@ -25,12 +24,6 @@ import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.internal.database.helper.addAll
import im.vector.matrix.android.internal.database.helper.addOrUpdate
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import io.realm.kotlin.createObject
import kotlin.random.Random
object RoomDataHelper {
@ -73,19 +66,4 @@ object RoomDataHelper {
val roomMember = RoomMember(Membership.JOIN, "Fake name #${Random.nextLong()}").toContent()
return createFakeEvent(EventType.STATE_ROOM_MEMBER, roomMember)
}
fun fakeInitialSync(monarchy: Monarchy, roomId: String) {
monarchy.runTransactionSync { realm ->
val roomEntity = realm.createObject<RoomEntity>(roomId)
roomEntity.membership = Membership.JOIN
val eventList = createFakeListOfEvents(10)
val chunkEntity = realm.createObject<ChunkEntity>().apply {
nextToken = null
prevToken = Random.nextLong(System.currentTimeMillis()).toString()
isLastForward = true
}
chunkEntity.addAll(roomId, eventList, PaginationDirection.FORWARDS)
roomEntity.addOrUpdate(chunkEntity)
}
}
}

View File

@ -66,7 +66,7 @@ internal class TimelineTest : InstrumentedTest {
// val latch = CountDownLatch(2)
// var timelineEvents: List<TimelineEvent> = emptyList()
// timeline.listener = object : Timeline.Listener {
// override fun onUpdated(snapshot: List<TimelineEvent>) {
// override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
// if (snapshot.isNotEmpty()) {
// if (initialLoad == 0) {
// initialLoad = snapshot.size

View File

@ -28,6 +28,12 @@ fun MXDeviceInfo.getFingerprintHumanReadable() = fingerprint()
?.chunked(4)
?.joinToString(separator = " ")
fun MutableList<DeviceInfo>.sortByLastSeen() {
sortWith(DatedObjectComparators.descComparator)
/* ==========================================================================================
* DeviceInfo
* ========================================================================================== */
fun List<DeviceInfo>.sortByLastSeen(): List<DeviceInfo> {
val list = toMutableList()
list.sortWith(DatedObjectComparators.descComparator)
return list
}

View File

@ -14,13 +14,15 @@
* limitations under the License.
*/
package im.vector.riotx.core.error
package im.vector.matrix.android.api.failure
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import javax.net.ssl.HttpsURLConnection
fun Throwable.is401(): Boolean {
return (this is Failure.ServerError && httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */
&& error.code == MatrixError.M_UNAUTHORIZED)
}
fun Throwable.is401() =
this is Failure.ServerError
&& httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */
&& error.code == MatrixError.M_UNAUTHORIZED
fun Throwable.isTokenError() =
this is Failure.ServerError
&& (error.code == MatrixError.M_UNKNOWN_TOKEN || error.code == MatrixError.M_MISSING_TOKEN)

View File

@ -17,7 +17,6 @@
package im.vector.matrix.android.api.permalinks
import android.text.Spannable
import im.vector.matrix.android.api.MatrixPatterns
/**
* MatrixLinkify take a piece of text and turns all of the
@ -30,7 +29,13 @@ object MatrixLinkify {
*
* @param spannable the text in which the matrix items has to be clickable.
*/
@Suppress("UNUSED_PARAMETER")
fun addLinks(spannable: Spannable, callback: MatrixPermalinkSpan.Callback?): Boolean {
/**
* I disable it because it mess up with pills, and even with pills, it does not work correctly:
* The url is not correct. Ex: for @user:matrix.org, the url will be @user:matrix.org, instead of a matrix.to
*/
/*
// sanity checks
if (spannable.isEmpty()) {
return false
@ -50,5 +55,7 @@ object MatrixLinkify {
}
}
return hasMatch
*/
return false
}
}

View File

@ -56,23 +56,23 @@ object PermalinkParser {
val identifier = params.getOrNull(0)
val extraParameter = params.getOrNull(1)
if (identifier.isNullOrEmpty()) {
return PermalinkData.FallbackLink(uri)
}
return when {
identifier.isNullOrEmpty() -> PermalinkData.FallbackLink(uri)
MatrixPatterns.isUserId(identifier) -> PermalinkData.UserLink(userId = identifier)
MatrixPatterns.isGroupId(identifier) -> PermalinkData.GroupLink(groupId = identifier)
MatrixPatterns.isRoomId(identifier) -> {
val eventId = extraParameter.takeIf {
!it.isNullOrEmpty() && MatrixPatterns.isEventId(it)
}
PermalinkData.RoomLink(roomIdOrAlias = identifier, isRoomAlias = false, eventId = eventId)
PermalinkData.RoomLink(
roomIdOrAlias = identifier,
isRoomAlias = false,
eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) }
)
}
MatrixPatterns.isRoomAlias(identifier) -> {
val eventId = extraParameter.takeIf {
!it.isNullOrEmpty() && MatrixPatterns.isEventId(it)
}
PermalinkData.RoomLink(roomIdOrAlias = identifier, isRoomAlias = true, eventId = eventId)
PermalinkData.RoomLink(
roomIdOrAlias = identifier,
isRoomAlias = true,
eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) }
)
}
else -> PermalinkData.FallbackLink(uri)
}

View File

@ -0,0 +1,35 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.query
/**
* Basic query language. All these cases are mutually exclusive.
*/
sealed class QueryStringValue {
object NoCondition : QueryStringValue()
object IsNull : QueryStringValue()
object IsNotNull : QueryStringValue()
object IsEmpty : QueryStringValue()
object IsNotEmpty : QueryStringValue()
data class Equals(val string: String, val case: Case) : QueryStringValue()
data class Contains(val string: String, val case: Case) : QueryStringValue()
enum class Case {
SENSITIVE,
INSENSITIVE
}
}

View File

@ -107,7 +107,12 @@ interface Session :
* This method allows to listen the sync state.
* @return a [LiveData] of [SyncState].
*/
fun syncState(): LiveData<SyncState>
fun getSyncStateLive(): LiveData<SyncState>
/**
* This methods return true if an initial sync has been processed
*/
fun hasAlreadySynced(): Boolean
/**
* This method allow to close a session. It does stop some services.

View File

@ -24,7 +24,7 @@ import im.vector.matrix.android.api.MatrixCallback
interface CacheService {
/**
* Clear the whole cached data, except credentials. Once done, the session is closed and has to be opened again
* Clear the whole cached data, except credentials. Once done, the sync has to be restarted by the sdk user.
*/
fun clearCache(callback: MatrixCallback<Unit>)
}

View File

@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
@ -89,6 +90,8 @@ interface CryptoService {
fun getDevicesList(callback: MatrixCallback<DevicesListResponse>)
fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>)
fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int
fun isRoomEncrypted(roomId: String): Boolean

View File

@ -50,10 +50,10 @@ object EventType {
const val STATE_ROOM_POWER_LEVELS = "m.room.power_levels"
const val STATE_ROOM_ALIASES = "m.room.aliases"
const val STATE_ROOM_TOMBSTONE = "m.room.tombstone"
const val STATE_CANONICAL_ALIAS = "m.room.canonical_alias"
const val STATE_HISTORY_VISIBILITY = "m.room.history_visibility"
const val STATE_RELATED_GROUPS = "m.room.related_groups"
const val STATE_PINNED_EVENT = "m.room.pinned_events"
const val STATE_ROOM_CANONICAL_ALIAS = "m.room.canonical_alias"
const val STATE_ROOM_HISTORY_VISIBILITY = "m.room.history_visibility"
const val STATE_ROOM_RELATED_GROUPS = "m.room.related_groups"
const val STATE_ROOM_PINNED_EVENT = "m.room.pinned_events"
// Call Events
@ -86,10 +86,12 @@ object EventType {
STATE_ROOM_JOIN_RULES,
STATE_ROOM_GUEST_ACCESS,
STATE_ROOM_POWER_LEVELS,
STATE_ROOM_ALIASES,
STATE_ROOM_TOMBSTONE,
STATE_HISTORY_VISIBILITY,
STATE_RELATED_GROUPS,
STATE_PINNED_EVENT
STATE_ROOM_CANONICAL_ALIAS,
STATE_ROOM_HISTORY_VISIBILITY,
STATE_ROOM_RELATED_GROUPS,
STATE_ROOM_PINNED_EVENT
)
fun isStateEvent(type: String): Boolean {

View File

@ -31,9 +31,22 @@ interface GroupService {
*/
fun getGroup(groupId: String): Group?
/**
* Get a groupSummary from a groupId
* @param groupId the groupId to look for.
* @return the groupSummary with groupId or null
*/
fun getGroupSummary(groupId: String): GroupSummary?
/**
* Get a list of group summaries. This list is a snapshot of the data.
* @return the list of [GroupSummary]
*/
fun getGroupSummaries(groupSummaryQueryParams: GroupSummaryQueryParams): List<GroupSummary>
/**
* Get a live list of group summaries. This list is refreshed as soon as the data changes.
* @return the [LiveData] of [GroupSummary]
*/
fun liveGroupSummaries(): LiveData<List<GroupSummary>>
fun getGroupSummariesLive(groupSummaryQueryParams: GroupSummaryQueryParams): LiveData<List<GroupSummary>>
}

View File

@ -0,0 +1,44 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.group
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.room.model.Membership
fun groupSummaryQueryParams(init: (GroupSummaryQueryParams.Builder.() -> Unit) = {}): GroupSummaryQueryParams {
return GroupSummaryQueryParams.Builder().apply(init).build()
}
/**
* This class can be used to filter group summaries
*/
data class GroupSummaryQueryParams(
val displayName: QueryStringValue,
val memberships: List<Membership>
) {
class Builder {
var displayName: QueryStringValue = QueryStringValue.IsNotEmpty
var memberships: List<Membership> = Membership.all()
fun build() = GroupSummaryQueryParams(
displayName = displayName,
memberships = memberships
)
}
}

View File

@ -58,7 +58,7 @@ interface PushersService {
const val EVENT_ID_ONLY = "event_id_only"
}
fun livePushers(): LiveData<List<Pusher>>
fun getPushersLive(): LiveData<List<Pusher>>
fun pushers() : List<Pusher>
}

View File

@ -56,5 +56,8 @@ interface Room :
*/
fun getRoomSummaryLive(): LiveData<Optional<RoomSummary>>
/**
* A current snapshot of [RoomSummary] associated with the room
*/
fun roomSummary(): RoomSummary?
}

View File

@ -53,16 +53,35 @@ interface RoomService {
fun getRoom(roomId: String): Room?
/**
* Get a live list of room summaries. This list is refreshed as soon as the data changes.
* @return the [LiveData] of [RoomSummary]
* Get a roomSummary from a roomId or a room alias
* @param roomIdOrAlias the roomId or the alias of a room to look for.
* @return a matching room summary or null
*/
fun liveRoomSummaries(): LiveData<List<RoomSummary>>
fun getRoomSummary(roomIdOrAlias: String): RoomSummary?
/**
* Get a snapshot list of room summaries.
* @return the immutable list of [RoomSummary]
*/
fun getRoomSummaries(queryParams: RoomSummaryQueryParams): List<RoomSummary>
/**
* Get a live list of room summaries. This list is refreshed as soon as the data changes.
* @return the [LiveData] of List[RoomSummary]
*/
fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams): LiveData<List<RoomSummary>>
/**
* Get a snapshot list of Breadcrumbs
* @return the immutable list of [RoomSummary]
*/
fun getBreadcrumbs(): List<RoomSummary>
/**
* Get a live list of Breadcrumbs
* @return the [LiveData] of [RoomSummary]
*/
fun liveBreadcrumbs(): LiveData<List<RoomSummary>>
fun getBreadcrumbsLive(): LiveData<List<RoomSummary>>
/**
* Inform the Matrix SDK that a room is displayed.

View File

@ -0,0 +1,48 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.room
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.room.model.Membership
fun roomSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): RoomSummaryQueryParams {
return RoomSummaryQueryParams.Builder().apply(init).build()
}
/**
* This class can be used to filter room summaries to use with:
* [im.vector.matrix.android.api.session.room.Room] and [im.vector.matrix.android.api.session.room.RoomService]
*/
data class RoomSummaryQueryParams(
val displayName: QueryStringValue,
val canonicalAlias: QueryStringValue,
val memberships: List<Membership>
) {
class Builder {
var displayName: QueryStringValue = QueryStringValue.IsNotEmpty
var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition
var memberships: List<Membership> = Membership.all()
fun build() = RoomSummaryQueryParams(
displayName = displayName,
canonicalAlias = canonicalAlias,
memberships = memberships
)
}
}

View File

@ -41,11 +41,18 @@ interface MembershipService {
fun getRoomMember(userId: String): RoomMember?
/**
* Return all the roomMembers ids of the room
*
* Return all the roomMembers of the room with params
* @param queryParams the params to query for
* @return a roomMember list.
*/
fun getRoomMembers(queryParams: RoomMemberQueryParams): List<RoomMember>
/**
* Return all the roomMembers of the room filtered by memberships
* @param queryParams the params to query for
* @return a [LiveData] of roomMember list.
*/
fun getRoomMemberIdsLive(): LiveData<List<String>>
fun getRoomMembersLive(queryParams: RoomMemberQueryParams): LiveData<List<RoomMember>>
fun getNumberOfJoinedMembers(): Int

View File

@ -0,0 +1,44 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.room.members
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.room.model.Membership
fun roomMemberQueryParams(init: (RoomMemberQueryParams.Builder.() -> Unit) = {}): RoomMemberQueryParams {
return RoomMemberQueryParams.Builder().apply(init).build()
}
/**
* This class can be used to filter room members
*/
data class RoomMemberQueryParams(
val displayName: QueryStringValue,
val memberships: List<Membership>
) {
class Builder {
var displayName: QueryStringValue = QueryStringValue.IsNotEmpty
var memberships: List<Membership> = Membership.all()
fun build() = RoomMemberQueryParams(
displayName = displayName,
memberships = memberships
)
}
}

View File

@ -43,4 +43,14 @@ enum class Membership(val value: String) {
fun isLeft(): Boolean {
return this == KNOCK || this == LEAVE || this == BAN
}
companion object {
fun activeMemberships(): List<Membership> {
return listOf(INVITE, JOIN)
}
fun all(): List<Membership> {
return values().asList()
}
}
}

View File

@ -20,7 +20,7 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Class representing the EventType.STATE_CANONICAL_ALIAS state event content
* Class representing the EventType.STATE_ROOM_CANONICAL_ALIAS state event content
*/
@JsonClass(generateAdapter = true)
data class RoomCanonicalAliasContent(

View File

@ -16,23 +16,12 @@
package im.vector.matrix.android.api.session.room.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.events.model.UnsignedData
/**
* Class representing the EventType.STATE_ROOM_MEMBER state event content
* Class representing a simplified version of EventType.STATE_ROOM_MEMBER state event content
*/
@JsonClass(generateAdapter = true)
data class RoomMember(
@Json(name = "membership") val membership: Membership,
@Json(name = "reason") val reason: String? = null,
@Json(name = "displayname") val displayName: String? = null,
@Json(name = "avatar_url") val avatarUrl: String? = null,
@Json(name = "is_direct") val isDirect: Boolean = false,
@Json(name = "third_party_invite") val thirdPartyInvite: Invite? = null,
@Json(name = "unsigned") val unsignedData: UnsignedData? = null
) {
val safeReason
get() = reason?.takeIf { it.isNotBlank() }
}
val membership: Membership,
val userId: String,
val displayName: String? = null,
val avatarUrl: String? = null
)

View File

@ -0,0 +1,38 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.room.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.events.model.UnsignedData
/**
* Class representing the EventType.STATE_ROOM_MEMBER state event content
*/
@JsonClass(generateAdapter = true)
data class RoomMemberContent(
@Json(name = "membership") val membership: Membership,
@Json(name = "reason") val reason: String? = null,
@Json(name = "displayname") val displayName: String? = null,
@Json(name = "avatar_url") val avatarUrl: String? = null,
@Json(name = "is_direct") val isDirect: Boolean = false,
@Json(name = "third_party_invite") val thirdPartyInvite: Invite? = null,
@Json(name = "unsigned") val unsignedData: UnsignedData? = null
) {
val safeReason
get() = reason?.takeIf { it.isNotBlank() }
}

View File

@ -43,7 +43,8 @@ data class RoomSummary(
val membership: Membership = Membership.NONE,
val versioningState: VersioningState = VersioningState.NONE,
val readMarkerId: String? = null,
val userDrafts: List<UserDraft> = emptyList()
val userDrafts: List<UserDraft> = emptyList(),
var isEncrypted: Boolean
) {
val isVersioned: Boolean

View File

@ -145,13 +145,13 @@ class CreateRoomParams {
*/
fun setHistoryVisibility(historyVisibility: RoomHistoryVisibility?) {
// Remove the existing value if any.
initialStates?.removeAll { it.getClearType() == EventType.STATE_HISTORY_VISIBILITY }
initialStates?.removeAll { it.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY }
if (historyVisibility != null) {
val contentMap = HashMap<String, RoomHistoryVisibility>()
contentMap["history_visibility"] = historyVisibility
val historyVisibilityEvent = Event(type = EventType.STATE_HISTORY_VISIBILITY,
val historyVisibilityEvent = Event(type = EventType.STATE_ROOM_HISTORY_VISIBILITY,
stateKey = "",
content = contentMap.toContent())

View File

@ -98,7 +98,7 @@ interface RelationService {
/**
* Reply to an event in the timeline (must be in same room)
* https://matrix.org/docs/spec/client_server/r0.4.0.html#id350
* The replyText can be a Spannable and contains special spans (UserMentionSpan) that will be translated
* The replyText can be a Spannable and contains special spans (MatrixItemSpan) that will be translated
* by the sdk into pills.
* @param eventReplied the event referenced by the reply
* @param replyText the reply text
@ -108,5 +108,17 @@ interface RelationService {
replyText: CharSequence,
autoMarkdown: Boolean = false): Cancelable?
fun getEventSummaryLive(eventId: String): LiveData<Optional<EventAnnotationsSummary>>
/**
* Get the current EventAnnotationsSummary
* @param eventId the eventId to look for EventAnnotationsSummary
* @return the EventAnnotationsSummary found
*/
fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary?
/**
* Get a LiveData of EventAnnotationsSummary for the specified eventId
* @param eventId the eventId to look for EventAnnotationsSummary
* @return the LiveData of EventAnnotationsSummary
*/
fun getEventAnnotationsSummaryLive(eventId: String): LiveData<Optional<EventAnnotationsSummary>>
}

View File

@ -19,9 +19,9 @@ package im.vector.matrix.android.api.session.room.send
import im.vector.matrix.android.api.util.MatrixItem
/**
* Tag class for spans that should mention a user.
* Tag class for spans that should mention a matrix item.
* These Spans will be transformed into pills when detected in message to send
*/
interface UserMentionSpan {
interface MatrixItemSpan {
val matrixItem: MatrixItem
}

View File

@ -29,7 +29,7 @@ interface SendService {
/**
* Method to send a text message asynchronously.
* The text to send can be a Spannable and contains special spans (UserMentionSpan) that will be translated
* The text to send can be a Spannable and contains special spans (MatrixItemSpan) that will be translated
* by the sdk into pills.
* @param text the text message to send
* @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE

View File

@ -65,7 +65,7 @@ interface Timeline {
/**
* This is the main method to enrich the timeline with new data.
* It will call the onUpdated method from [Listener] when the data will be processed.
* It will call the onTimelineUpdated method from [Listener] when the data will be processed.
* It also ensures only one pagination by direction is launched at a time, so you can safely call this multiple time in a row.
*/
fun paginate(direction: Direction, count: Int)
@ -106,7 +106,12 @@ interface Timeline {
* Call when the timeline has been updated through pagination or sync.
* @param snapshot the most up to date snapshot
*/
fun onUpdated(snapshot: List<TimelineEvent>)
fun onTimelineUpdated(snapshot: List<TimelineEvent>)
/**
* Called whenever an error we can't recover from occurred
*/
fun onTimelineFailure(throwable: Throwable)
}
/**

View File

@ -50,25 +50,25 @@ interface UserService {
* @param userId the userId to look for.
* @return a LiveData of user with userId
*/
fun liveUser(userId: String): LiveData<Optional<User>>
fun getUserLive(userId: String): LiveData<Optional<User>>
/**
* Observe a live list of users sorted alphabetically
* @return a Livedata of users
*/
fun liveUsers(): LiveData<List<User>>
fun getUsersLive(): LiveData<List<User>>
/**
* Observe a live [PagedList] of users sorted alphabetically. You can filter the users.
* @param filter the filter. It will look into userId and displayName.
* @return a Livedata of users
*/
fun livePagedUsers(filter: String? = null): LiveData<PagedList<User>>
fun getPagedUsersLive(filter: String? = null): LiveData<PagedList<User>>
/**
* Get list of ignored users
*/
fun liveIgnoredUsers(): LiveData<List<User>>
fun getIgnoredUsersLive(): LiveData<List<User>>
/**
* Ignore users

View File

@ -18,6 +18,7 @@ package im.vector.matrix.android.api.util
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.matrix.android.api.session.user.model.User
@ -62,6 +63,9 @@ sealed class MatrixItem(
init {
if (BuildConfig.DEBUG) checkId()
}
// Best name is the id, and we keep the displayName of the room for the case we need the first letter
override fun getBestName() = id
}
data class GroupItem(override val id: String,
@ -71,9 +75,12 @@ sealed class MatrixItem(
init {
if (BuildConfig.DEBUG) checkId()
}
// Best name is the id, and we keep the displayName of the room for the case we need the first letter
override fun getBestName() = id
}
fun getBestName(): String {
open fun getBestName(): String {
return displayName?.takeIf { it.isNotBlank() } ?: id
}
@ -95,7 +102,7 @@ sealed class MatrixItem(
}
fun firstLetterOfDisplayName(): String {
return getBestName()
return (displayName?.takeIf { it.isNotBlank() } ?: id)
.let { dn ->
var startIndex = 0
val initial = dn[startIndex]
@ -138,4 +145,6 @@ sealed class MatrixItem(
fun User.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)
fun GroupSummary.toMatrixItem() = MatrixItem.GroupItem(groupId, displayName, avatarUrl)
fun RoomSummary.toMatrixItem() = MatrixItem.RoomItem(roomId, displayName, avatarUrl)
fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlias ?: roomId, displayName, avatarUrl)
fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name, avatarUrl)
fun RoomMember.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)

View File

@ -0,0 +1,23 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.auth
import im.vector.matrix.android.internal.util.md5
internal fun createSessionId(userId: String, deviceId: String?): String {
return (if (deviceId.isNullOrBlank()) userId else "$userId|$deviceId").md5()
}

View File

@ -16,6 +16,9 @@
package im.vector.matrix.android.internal.auth.db
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.internal.auth.createSessionId
import im.vector.matrix.android.internal.di.MoshiProvider
import io.realm.DynamicRealm
import io.realm.RealmMigration
import timber.log.Timber
@ -23,35 +26,60 @@ import timber.log.Timber
internal object AuthRealmMigration : RealmMigration {
// Current schema version
const val SCHEMA_VERSION = 2L
const val SCHEMA_VERSION = 3L
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.d("Migrating Auth Realm from $oldVersion to $newVersion")
if (oldVersion <= 0) {
Timber.d("Step 0 -> 1")
Timber.d("Create PendingSessionEntity")
if (oldVersion <= 0) migrateTo1(realm)
if (oldVersion <= 1) migrateTo2(realm)
if (oldVersion <= 2) migrateTo3(realm)
}
realm.schema.create("PendingSessionEntity")
.addField(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, String::class.java)
.setRequired(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, true)
.addField(PendingSessionEntityFields.CLIENT_SECRET, String::class.java)
.setRequired(PendingSessionEntityFields.CLIENT_SECRET, true)
.addField(PendingSessionEntityFields.SEND_ATTEMPT, Integer::class.java)
.setRequired(PendingSessionEntityFields.SEND_ATTEMPT, true)
.addField(PendingSessionEntityFields.RESET_PASSWORD_DATA_JSON, String::class.java)
.addField(PendingSessionEntityFields.CURRENT_SESSION, String::class.java)
.addField(PendingSessionEntityFields.IS_REGISTRATION_STARTED, Boolean::class.java)
.addField(PendingSessionEntityFields.CURRENT_THREE_PID_DATA_JSON, String::class.java)
}
private fun migrateTo1(realm: DynamicRealm) {
Timber.d("Step 0 -> 1")
Timber.d("Create PendingSessionEntity")
if (oldVersion <= 1) {
Timber.d("Step 1 -> 2")
Timber.d("Add boolean isTokenValid in SessionParamsEntity, with value true")
realm.schema.create("PendingSessionEntity")
.addField(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, String::class.java)
.setRequired(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, true)
.addField(PendingSessionEntityFields.CLIENT_SECRET, String::class.java)
.setRequired(PendingSessionEntityFields.CLIENT_SECRET, true)
.addField(PendingSessionEntityFields.SEND_ATTEMPT, Integer::class.java)
.setRequired(PendingSessionEntityFields.SEND_ATTEMPT, true)
.addField(PendingSessionEntityFields.RESET_PASSWORD_DATA_JSON, String::class.java)
.addField(PendingSessionEntityFields.CURRENT_SESSION, String::class.java)
.addField(PendingSessionEntityFields.IS_REGISTRATION_STARTED, Boolean::class.java)
.addField(PendingSessionEntityFields.CURRENT_THREE_PID_DATA_JSON, String::class.java)
}
realm.schema.get("SessionParamsEntity")
?.addField(SessionParamsEntityFields.IS_TOKEN_VALID, Boolean::class.java)
?.transform { it.set(SessionParamsEntityFields.IS_TOKEN_VALID, true) }
}
private fun migrateTo2(realm: DynamicRealm) {
Timber.d("Step 1 -> 2")
Timber.d("Add boolean isTokenValid in SessionParamsEntity, with value true")
realm.schema.get("SessionParamsEntity")
?.addField(SessionParamsEntityFields.IS_TOKEN_VALID, Boolean::class.java)
?.transform { it.set(SessionParamsEntityFields.IS_TOKEN_VALID, true) }
}
private fun migrateTo3(realm: DynamicRealm) {
Timber.d("Step 2 -> 3")
Timber.d("Update SessionParamsEntity primary key, to allow several sessions with the same userId")
realm.schema.get("SessionParamsEntity")
?.removePrimaryKey()
?.addField(SessionParamsEntityFields.SESSION_ID, String::class.java)
?.setRequired(SessionParamsEntityFields.SESSION_ID, true)
?.transform {
val userId = it.getString(SessionParamsEntityFields.USER_ID)
val credentialsJson = it.getString(SessionParamsEntityFields.CREDENTIALS_JSON)
val credentials = MoshiProvider.providesMoshi()
.adapter(Credentials::class.java)
.fromJson(credentialsJson)
it.set(SessionParamsEntityFields.SESSION_ID, createSessionId(userId, credentials?.deviceId))
}
?.addPrimaryKey(SessionParamsEntityFields.SESSION_ID)
}
}

View File

@ -20,7 +20,8 @@ import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
internal open class SessionParamsEntity(
@PrimaryKey var userId: String = "",
@PrimaryKey var sessionId: String = "",
var userId: String = "",
var credentialsJson: String = "",
var homeServerConnectionConfigJson: String = "",
// Set to false when the token is invalid and the user has been soft logged out

View File

@ -20,6 +20,7 @@ import com.squareup.moshi.Moshi
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.internal.auth.createSessionId
import javax.inject.Inject
internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
@ -49,6 +50,7 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
return null
}
return SessionParamsEntity(
createSessionId(sessionParams.credentials.userId, sessionParams.credentials.deviceId),
sessionParams.credentials.userId,
credentialsJson,
homeServerConnectionConfigJson,

View File

@ -47,7 +47,7 @@ internal abstract class CryptoModule {
@Module
companion object {
internal const val DB_ALIAS_PREFIX = "crypto_module_"
internal fun getKeyAlias(userMd5: String) = "crypto_module_$userMd5"
@JvmStatic
@Provides
@ -59,7 +59,7 @@ internal abstract class CryptoModule {
return RealmConfiguration.Builder()
.directory(directory)
.apply {
realmKeysUtils.configureEncryption(this, "$DB_ALIAS_PREFIX$userMd5")
realmKeysUtils.configureEncryption(this, getKeyAlias(userMd5))
}
.name("crypto_store.realm")
.modules(RealmCryptoStoreModule())
@ -123,6 +123,9 @@ internal abstract class CryptoModule {
@Binds
abstract fun bindGetDevicesTask(getDevicesTask: DefaultGetDevicesTask): GetDevicesTask
@Binds
abstract fun bindGetDeviceInfoTask(task: DefaultGetDeviceInfoTask): GetDeviceInfoTask
@Binds
abstract fun bindSetDeviceNameTask(setDeviceNameTask: DefaultSetDeviceNameTask): SetDeviceNameTask

View File

@ -50,6 +50,7 @@ import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
@ -127,6 +128,7 @@ internal class DefaultCryptoService @Inject constructor(
private val deleteDeviceWithUserPasswordTask: DeleteDeviceWithUserPasswordTask,
// Tasks
private val getDevicesTask: GetDevicesTask,
private val getDeviceInfoTask: GetDeviceInfoTask,
private val setDeviceNameTask: SetDeviceNameTask,
private val uploadKeysTask: UploadKeysTask,
private val loadRoomMembersTask: LoadRoomMembersTask,
@ -145,17 +147,17 @@ internal class DefaultCryptoService @Inject constructor(
fun onStateEvent(roomId: String, event: Event) {
when {
event.getClearType() == EventType.ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
event.getClearType() == EventType.STATE_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
event.getClearType() == EventType.ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
event.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
}
}
fun onLiveEvent(roomId: String, event: Event) {
when {
event.getClearType() == EventType.ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
event.getClearType() == EventType.STATE_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
event.getClearType() == EventType.ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
event.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
}
}
@ -199,6 +201,14 @@ internal class DefaultCryptoService @Inject constructor(
.executeBy(taskExecutor)
}
override fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>) {
getDeviceInfoTask
.configureWith(GetDeviceInfoTask.Params(deviceId)) {
this.callback = callback
}
.executeBy(taskExecutor)
}
override fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int {
return cryptoStore.inboundGroupSessionsCount(onlyBackedUp)
}

View File

@ -25,11 +25,18 @@ internal interface CryptoApi {
/**
* Get the devices list
* Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-devices
* Doc: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-devices
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices")
fun getDevices(): Call<DevicesListResponse>
/**
* Get the device info by id
* Doc: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-devices-deviceid
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices/{deviceId}")
fun getDeviceInfo(@Path("deviceId") deviceId: String): Call<DeviceInfo>
/**
* Upload device and/or one-time keys.
* Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-upload

View File

@ -0,0 +1,37 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.tasks
import im.vector.matrix.android.internal.crypto.api.CryptoApi
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject
internal interface GetDeviceInfoTask : Task<GetDeviceInfoTask.Params, DeviceInfo> {
data class Params(val deviceId: String)
}
internal class DefaultGetDeviceInfoTask @Inject constructor(private val cryptoApi: CryptoApi)
: GetDeviceInfoTask {
override suspend fun execute(params: GetDeviceInfoTask.Params): DeviceInfo {
return executeRequest {
apiCall = cryptoApi.getDeviceInfo(params.deviceId)
}
}
}

View File

@ -19,12 +19,16 @@ package im.vector.matrix.android.internal.database
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.util.createBackgroundHandler
import io.realm.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelChildren
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
internal interface LiveEntityObserver {
fun start()
fun dispose()
fun cancelProcess()
fun isStarted(): Boolean
}
@ -35,6 +39,7 @@ internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val r
val BACKGROUND_HANDLER = createBackgroundHandler("LIVE_ENTITY_BACKGROUND")
}
protected val observerScope = CoroutineScope(SupervisorJob())
protected abstract val query: Monarchy.Query<T>
private val isStarted = AtomicBoolean(false)
private val backgroundRealm = AtomicReference<Realm>()
@ -59,10 +64,15 @@ internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val r
backgroundRealm.getAndSet(null).also {
it.close()
}
observerScope.coroutineContext.cancelChildren()
}
}
}
override fun cancelProcess() {
observerScope.coroutineContext.cancelChildren()
}
override fun isStarted(): Boolean {
return isStarted.get()
}

View File

@ -16,43 +16,36 @@
package im.vector.matrix.android.internal.database
import im.vector.matrix.android.internal.util.createBackgroundHandler
import io.realm.*
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicReference
import kotlinx.coroutines.*
class RealmQueryLatch<E : RealmObject>(private val realmConfiguration: RealmConfiguration,
private val realmQueryBuilder: (Realm) -> RealmQuery<E>) {
internal suspend fun <T> awaitNotEmptyResult(realmConfiguration: RealmConfiguration,
timeoutMillis: Long,
builder: (Realm) -> RealmQuery<T>) {
withTimeout(timeoutMillis) {
// Confine Realm interaction to a single thread with Looper.
withContext(Dispatchers.Main) {
val latch = CompletableDeferred<Unit>()
private companion object {
val QUERY_LATCH_HANDLER = createBackgroundHandler("REALM_QUERY_LATCH")
}
Realm.getInstance(realmConfiguration).use { realm ->
val result = builder(realm).findAllAsync()
@Throws(InterruptedException::class)
fun await(timeout: Long, timeUnit: TimeUnit) {
val realmRef = AtomicReference<Realm>()
val latch = CountDownLatch(1)
QUERY_LATCH_HANDLER.post {
val realm = Realm.getInstance(realmConfiguration)
realmRef.set(realm)
val result = realmQueryBuilder(realm).findAllAsync()
result.addChangeListener(object : RealmChangeListener<RealmResults<E>> {
override fun onChange(t: RealmResults<E>) {
if (t.isNotEmpty()) {
result.removeChangeListener(this)
latch.countDown()
val listener = object : RealmChangeListener<RealmResults<T>> {
override fun onChange(it: RealmResults<T>) {
if (it.isNotEmpty()) {
result.removeChangeListener(this)
latch.complete(Unit)
}
}
}
})
}
try {
latch.await(timeout, timeUnit)
} catch (exception: InterruptedException) {
throw exception
} finally {
QUERY_LATCH_HANDLER.post {
realmRef.getAndSet(null).close()
result.addChangeListener(listener)
try {
latch.await()
} catch (e: CancellationException) {
result.removeChangeListener(listener)
throw e
}
}
}
}

View File

@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.database
import android.content.Context
import im.vector.matrix.android.internal.database.model.SessionRealmModule
import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.di.UserCacheDirectory
import im.vector.matrix.android.internal.di.UserMd5
import im.vector.matrix.android.internal.session.SessionModule
@ -37,13 +38,14 @@ private const val REALM_NAME = "disk_store.realm"
*/
internal class SessionRealmConfigurationFactory @Inject constructor(private val realmKeysUtils: RealmKeysUtils,
@UserCacheDirectory val directory: File,
@SessionId val sessionId: String,
@UserMd5 val userMd5: String,
context: Context) {
private val sharedPreferences = context.getSharedPreferences("im.vector.matrix.android.realm", Context.MODE_PRIVATE)
fun create(): RealmConfiguration {
val shouldClearRealm = sharedPreferences.getBoolean("$REALM_SHOULD_CLEAR_FLAG_$userMd5", false)
val shouldClearRealm = sharedPreferences.getBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", false)
if (shouldClearRealm) {
Timber.v("************************************************************")
Timber.v("The realm file session was corrupted and couldn't be loaded.")
@ -53,14 +55,15 @@ internal class SessionRealmConfigurationFactory @Inject constructor(private val
}
sharedPreferences
.edit()
.putBoolean("$REALM_SHOULD_CLEAR_FLAG_$userMd5", true)
.putBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", true)
.apply()
val realmConfiguration = RealmConfiguration.Builder()
.compactOnLaunch()
.directory(directory)
.name(REALM_NAME)
.apply {
realmKeysUtils.configureEncryption(this, "${SessionModule.DB_ALIAS_PREFIX}$userMd5")
realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5))
}
.modules(SessionRealmModule())
.deleteRealmIfMigrationNeeded()
@ -71,7 +74,7 @@ internal class SessionRealmConfigurationFactory @Inject constructor(private val
Timber.v("Successfully create realm instance")
sharedPreferences
.edit()
.putBoolean("$REALM_SHOULD_CLEAR_FLAG_$userMd5", false)
.putBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", false)
.apply()
}
return realmConfiguration

View File

@ -21,27 +21,14 @@ import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.mapper.toEntity
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
import im.vector.matrix.android.internal.database.model.*
import im.vector.matrix.android.internal.database.query.find
import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.extensions.assertIsManaged
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import io.realm.Sort
// By default if a chunk is empty we consider it unlinked
internal fun ChunkEntity.isUnlinked(): Boolean {
assertIsManaged()
return timelineEvents.where()
.equalTo(TimelineEventEntityFields.ROOT.IS_UNLINKED, false)
.findAll()
.isEmpty()
}
import io.realm.kotlin.createObject
internal fun ChunkEntity.deleteOnCascade() {
assertIsManaged()
@ -51,11 +38,10 @@ internal fun ChunkEntity.deleteOnCascade() {
internal fun ChunkEntity.merge(roomId: String,
chunkToMerge: ChunkEntity,
direction: PaginationDirection) {
direction: PaginationDirection): List<TimelineEventEntity> {
assertIsManaged()
val isChunkToMergeUnlinked = chunkToMerge.isUnlinked()
val isCurrentChunkUnlinked = this.isUnlinked()
val isUnlinked = isCurrentChunkUnlinked && isChunkToMergeUnlinked
val isChunkToMergeUnlinked = chunkToMerge.isUnlinked
val isCurrentChunkUnlinked = isUnlinked
if (isCurrentChunkUnlinked && !isChunkToMergeUnlinked) {
this.timelineEvents.forEach { it.root?.isUnlinked = false }
@ -70,49 +56,21 @@ internal fun ChunkEntity.merge(roomId: String,
this.isLastBackward = chunkToMerge.isLastBackward
eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING)
}
val events = eventsToMerge.mapNotNull { it.root?.asDomain() }
val eventIds = ArrayList<String>()
events.forEach { event ->
add(roomId, event, direction, isUnlinked = isUnlinked)
if (event.eventId != null) {
eventIds.add(event.eventId)
}
}
updateSenderDataFor(eventIds)
}
internal fun ChunkEntity.addAll(roomId: String,
events: List<Event>,
direction: PaginationDirection,
stateIndexOffset: Int = 0,
// Set to true for Event retrieved from a Permalink (i.e. not linked to live Chunk)
isUnlinked: Boolean = false) {
assertIsManaged()
val eventIds = ArrayList<String>()
events.forEach { event ->
add(roomId, event, direction, stateIndexOffset, isUnlinked)
if (event.eventId != null) {
eventIds.add(event.eventId)
}
}
updateSenderDataFor(eventIds)
}
internal fun ChunkEntity.updateSenderDataFor(eventIds: List<String>) {
for (eventId in eventIds) {
val timelineEventEntity = timelineEvents.find(eventId) ?: continue
timelineEventEntity.updateSenderData()
}
return eventsToMerge
.mapNotNull {
val event = it.root?.asDomain() ?: return@mapNotNull null
add(roomId, event, direction)
}
}
internal fun ChunkEntity.add(roomId: String,
event: Event,
direction: PaginationDirection,
stateIndexOffset: Int = 0,
isUnlinked: Boolean = false) {
stateIndexOffset: Int = 0
): TimelineEventEntity? {
assertIsManaged()
if (event.eventId != null && timelineEvents.find(event.eventId) != null) {
return
return null
}
var currentDisplayIndex = lastDisplayIndex(direction, 0)
if (direction == PaginationDirection.FORWARDS) {
@ -134,12 +92,15 @@ internal fun ChunkEntity.add(roomId: String,
}
}
val isChunkUnlinked = isUnlinked
val localId = TimelineEventEntity.nextId(realm)
val eventId = event.eventId ?: ""
val senderId = event.senderId ?: ""
val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst()
?: ReadReceiptsSummaryEntity(eventId, roomId)
?: realm.createObject<ReadReceiptsSummaryEntity>(eventId).apply {
this.roomId = roomId
}
// Update RR for the sender of a new message with a dummy one
@ -156,13 +117,15 @@ internal fun ChunkEntity.add(roomId: String,
}
}
val eventEntity = TimelineEventEntity(localId).also {
it.root = event.toEntity(roomId).apply {
this.stateIndex = currentStateIndex
this.isUnlinked = isUnlinked
this.displayIndex = currentDisplayIndex
this.sendState = SendState.SYNCED
}
val rootEvent = event.toEntity(roomId).apply {
this.stateIndex = currentStateIndex
this.displayIndex = currentDisplayIndex
this.sendState = SendState.SYNCED
this.isUnlinked = isChunkUnlinked
}
val eventEntity = realm.createObject<TimelineEventEntity>().also {
it.localId = localId
it.root = realm.copyToRealm(rootEvent)
it.eventId = eventId
it.roomId = roomId
it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
@ -170,6 +133,7 @@ internal fun ChunkEntity.add(roomId: String,
}
val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size
timelineEvents.add(position, eventEntity)
return eventEntity
}
internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {

View File

@ -60,7 +60,7 @@ internal fun RoomEntity.addSendingEvent(event: Event) {
this.sendState = SendState.UNSENT
}
val roomMembers = RoomMembers(realm, roomId)
val myUser = roomMembers.get(senderId)
val myUser = roomMembers.getLastRoomMember(senderId)
val localId = TimelineEventEntity.nextId(realm)
val timelineEventEntity = TimelineEventEntity(localId).also {
it.root = eventEntity
@ -69,7 +69,6 @@ internal fun RoomEntity.addSendingEvent(event: Event) {
it.senderName = myUser?.displayName
it.senderAvatar = myUser?.avatarUrl
it.isUniqueDisplayName = roomMembers.isUniqueDisplayName(myUser?.displayName)
it.senderMembershipEvent = roomMembers.queryRoomMemberEvent(senderId).findFirst()
}
sendingTimelineEvents.add(0, timelineEventEntity)
}

View File

@ -16,74 +16,9 @@
package im.vector.matrix.android.internal.database.helper
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.internal.database.mapper.ContentMapper
import im.vector.matrix.android.internal.database.model.*
import im.vector.matrix.android.internal.database.query.next
import im.vector.matrix.android.internal.database.query.prev
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.extensions.assertIsManaged
import im.vector.matrix.android.internal.session.room.membership.RoomMembers
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
import io.realm.Realm
import io.realm.RealmList
import io.realm.RealmQuery
internal fun TimelineEventEntity.updateSenderData() {
assertIsManaged()
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return
val stateIndex = root?.stateIndex ?: return
val senderId = root?.sender ?: return
val chunkEntity = chunk?.firstOrNull() ?: return
val isUnlinked = chunkEntity.isUnlinked()
var senderMembershipEvent: EventEntity?
var senderRoomMemberContent: String?
var senderRoomMemberPrevContent: String?
when {
stateIndex <= 0 -> {
senderMembershipEvent = chunkEntity.timelineEvents.buildQuery(senderId, isUnlinked).next(from = stateIndex)?.root
senderRoomMemberContent = senderMembershipEvent?.prevContent
senderRoomMemberPrevContent = senderMembershipEvent?.content
}
else -> {
senderMembershipEvent = chunkEntity.timelineEvents.buildQuery(senderId, isUnlinked).prev(since = stateIndex)?.root
senderRoomMemberContent = senderMembershipEvent?.content
senderRoomMemberPrevContent = senderMembershipEvent?.prevContent
}
}
// We fallback to untimelinedStateEvents if we can't find membership events in timeline
if (senderMembershipEvent == null) {
senderMembershipEvent = roomEntity.untimelinedStateEvents
.where()
.equalTo(EventEntityFields.STATE_KEY, senderId)
.equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_MEMBER)
.prev(since = stateIndex)
senderRoomMemberContent = senderMembershipEvent?.content
senderRoomMemberPrevContent = senderMembershipEvent?.prevContent
}
ContentMapper.map(senderRoomMemberContent).toModel<RoomMember>()?.also {
this.senderAvatar = it.avatarUrl
this.senderName = it.displayName
this.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName)
}
// We try to fallback on prev content if we got a room member state events with null fields
if (root?.type == EventType.STATE_ROOM_MEMBER) {
ContentMapper.map(senderRoomMemberPrevContent).toModel<RoomMember>()?.also {
if (this.senderAvatar == null && it.avatarUrl != null) {
this.senderAvatar = it.avatarUrl
}
if (this.senderName == null && it.displayName != null) {
this.senderName = it.displayName
this.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName)
}
}
}
this.senderMembershipEvent = senderMembershipEvent
}
internal fun TimelineEventEntity.Companion.nextId(realm: Realm): Long {
val currentIdNum = realm.where(TimelineEventEntity::class.java).max(TimelineEventEntityFields.LOCAL_ID)
@ -93,10 +28,3 @@ internal fun TimelineEventEntity.Companion.nextId(realm: Realm): Long {
currentIdNum.toLong() + 1
}
}
private fun RealmList<TimelineEventEntity>.buildQuery(sender: String, isUnlinked: Boolean): RealmQuery<TimelineEventEntity> {
return where()
.equalTo(TimelineEventEntityFields.ROOT.STATE_KEY, sender)
.equalTo(TimelineEventEntityFields.ROOT.TYPE, EventType.STATE_ROOM_MEMBER)
.equalTo(TimelineEventEntityFields.ROOT.IS_UNLINKED, isUnlinked)
}

View File

@ -0,0 +1,148 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.database.helper
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomMemberContent
import im.vector.matrix.android.internal.database.mapper.ContentMapper
import im.vector.matrix.android.internal.database.model.*
import im.vector.matrix.android.internal.database.query.next
import im.vector.matrix.android.internal.database.query.prev
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.extensions.assertIsManaged
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.room.membership.RoomMembers
import io.realm.RealmList
import io.realm.RealmQuery
import javax.inject.Inject
/**
* This is an internal cache to avoid querying all the time the room member events
*/
@SessionScope
internal class TimelineEventSenderVisitor @Inject constructor() {
internal data class Key(
val roomId: String,
val stateIndex: Int,
val senderId: String
)
internal class Value(
var senderAvatar: String? = null,
var senderName: String? = null,
var isUniqueDisplayName: Boolean = false,
var senderMembershipEventId: String? = null
)
private val values = HashMap<Key, Value>()
fun clear() {
values.clear()
}
fun clear(roomId: String, senderId: String) {
val keysToRemove = values.keys.filter { it.senderId == senderId && it.roomId == roomId }
keysToRemove.forEach {
values.remove(it)
}
}
fun visit(timelineEventEntities: List<TimelineEventEntity>) = timelineEventEntities.forEach { visit(it) }
fun visit(timelineEventEntity: TimelineEventEntity) {
if (!timelineEventEntity.isValid) {
return
}
val key = Key(
roomId = timelineEventEntity.roomId,
stateIndex = timelineEventEntity.root?.stateIndex ?: 0,
senderId = timelineEventEntity.root?.sender ?: ""
)
val result = values.getOrPut(key) {
timelineEventEntity.computeValue()
}
timelineEventEntity.apply {
this.isUniqueDisplayName = result.isUniqueDisplayName
this.senderAvatar = result.senderAvatar
this.senderName = result.senderName
this.senderMembershipEventId = result.senderMembershipEventId
}
}
private fun RealmList<TimelineEventEntity>.buildQuery(sender: String, isUnlinked: Boolean): RealmQuery<TimelineEventEntity> {
return where()
.equalTo(TimelineEventEntityFields.ROOT.STATE_KEY, sender)
.equalTo(TimelineEventEntityFields.ROOT.TYPE, EventType.STATE_ROOM_MEMBER)
.equalTo(TimelineEventEntityFields.ROOT.IS_UNLINKED, isUnlinked)
}
private fun TimelineEventEntity.computeValue(): Value {
assertIsManaged()
val result = Value()
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return result
val stateIndex = root?.stateIndex ?: return result
val senderId = root?.sender ?: return result
val chunkEntity = chunk?.firstOrNull() ?: return result
val isUnlinked = chunkEntity.isUnlinked
var senderMembershipEvent: EventEntity?
var senderRoomMemberContent: String?
var senderRoomMemberPrevContent: String?
if (stateIndex <= 0) {
senderMembershipEvent = chunkEntity.timelineEvents.buildQuery(senderId, isUnlinked).next(from = stateIndex)?.root
senderRoomMemberContent = senderMembershipEvent?.prevContent
senderRoomMemberPrevContent = senderMembershipEvent?.content
} else {
senderMembershipEvent = chunkEntity.timelineEvents.buildQuery(senderId, isUnlinked).prev(since = stateIndex)?.root
senderRoomMemberContent = senderMembershipEvent?.content
senderRoomMemberPrevContent = senderMembershipEvent?.prevContent
}
// We fallback to untimelinedStateEvents if we can't find membership events in timeline
if (senderMembershipEvent == null) {
senderMembershipEvent = roomEntity.untimelinedStateEvents
.where()
.equalTo(EventEntityFields.STATE_KEY, senderId)
.equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_MEMBER)
.prev(since = stateIndex)
senderRoomMemberContent = senderMembershipEvent?.content
senderRoomMemberPrevContent = senderMembershipEvent?.prevContent
}
ContentMapper.map(senderRoomMemberContent).toModel<RoomMemberContent>()?.also {
result.senderAvatar = it.avatarUrl
result.senderName = it.displayName
result.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName)
}
// We try to fallback on prev content if we got a room member state events with null fields
if (root?.type == EventType.STATE_ROOM_MEMBER) {
ContentMapper.map(senderRoomMemberPrevContent).toModel<RoomMemberContent>()?.also {
if (result.senderAvatar == null && it.avatarUrl != null) {
result.senderAvatar = it.avatarUrl
}
if (result.senderName == null && it.displayName != null) {
result.senderName = it.displayName
result.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName)
}
}
}
result.senderMembershipEventId = senderMembershipEvent?.eventId
return result
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.database.mapper
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.internal.database.model.RoomMemberEntity
internal object RoomMemberMapper {
fun map(roomMemberEntity: RoomMemberEntity): RoomMember {
return RoomMember(
userId = roomMemberEntity.userId,
avatarUrl = roomMemberEntity.avatarUrl,
displayName = roomMemberEntity.displayName,
membership = roomMemberEntity.membership
)
}
}
internal fun RoomMemberEntity.asDomain(): RoomMember {
return RoomMemberMapper.map(this)
}

View File

@ -72,7 +72,8 @@ internal class RoomSummaryMapper @Inject constructor(
readMarkerId = roomSummaryEntity.readMarkerId,
userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList(),
canonicalAlias = roomSummaryEntity.canonicalAlias,
aliases = roomSummaryEntity.aliases.toList()
aliases = roomSummaryEntity.aliases.toList(),
isEncrypted = roomSummaryEntity.isEncrypted
)
}
}

View File

@ -30,7 +30,8 @@ internal open class ChunkEntity(@Index var prevToken: String? = null,
var backwardsDisplayIndex: Int? = null,
var forwardsDisplayIndex: Int? = null,
var backwardsStateIndex: Int? = null,
var forwardsStateIndex: Int? = null
var forwardsStateIndex: Int? = null,
var isUnlinked: Boolean = false
) : RealmObject() {
fun identifier() = "${prevToken}_$nextToken"

View File

@ -0,0 +1,43 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.database.model
import im.vector.matrix.android.api.session.room.model.Membership
import io.realm.RealmObject
import io.realm.annotations.Index
import io.realm.annotations.PrimaryKey
internal open class RoomMemberEntity(@PrimaryKey var primaryKey: String = "",
@Index var userId: String = "",
@Index var roomId: String = "",
var displayName: String = "",
var avatarUrl: String = "",
var reason: String? = null,
var isDirect: Boolean = false
) : RealmObject() {
private var membershipStr: String = Membership.NONE.name
var membership: Membership
get() {
return Membership.valueOf(membershipStr)
}
set(value) {
membershipStr = value.name
}
companion object
}

View File

@ -42,7 +42,9 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
var breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS,
var canonicalAlias: String? = null,
var aliases: RealmList<String> = RealmList(),
var flatAliases: String = ""
// this is required for querying
var flatAliases: String = "",
var isEncrypted: Boolean = false
) : RealmObject() {
private var membershipStr: String = Membership.NONE.name

View File

@ -49,6 +49,7 @@ import io.realm.annotations.RealmModule
ReadMarkerEntity::class,
UserDraftsEntity::class,
DraftEntity::class,
HomeServerCapabilitiesEntity::class
HomeServerCapabilitiesEntity::class,
RoomMemberEntity::class
])
internal class SessionRealmModule

View File

@ -29,7 +29,7 @@ internal open class TimelineEventEntity(var localId: Long = 0,
var senderName: String? = null,
var isUniqueDisplayName: Boolean = false,
var senderAvatar: String? = null,
var senderMembershipEvent: EventEntity? = null,
var senderMembershipEventId: String? = null,
var readReceipts: ReadReceiptsSummaryEntity? = null
) : RealmObject() {

View File

@ -57,9 +57,15 @@ internal fun ChunkEntity.Companion.findIncludingEvent(realm: Realm, eventId: Str
return findAllIncludingEvents(realm, listOf(eventId)).firstOrNull()
}
internal fun ChunkEntity.Companion.create(realm: Realm, prevToken: String?, nextToken: String?): ChunkEntity {
internal fun ChunkEntity.Companion.create(
realm: Realm,
prevToken: String?,
nextToken: String?,
isUnlinked: Boolean
): ChunkEntity {
return realm.createObject<ChunkEntity>().apply {
this.prevToken = prevToken
this.nextToken = nextToken
this.isUnlinked = isUnlinked
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.database.query
import im.vector.matrix.android.internal.database.model.RoomMemberEntity
import im.vector.matrix.android.internal.database.model.RoomMemberEntityFields
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.kotlin.where
internal fun RoomMemberEntity.Companion.where(realm: Realm, roomId: String, userId: String? = null): RealmQuery<RoomMemberEntity> {
val query = realm
.where<RoomMemberEntity>()
.equalTo(RoomMemberEntityFields.ROOM_ID, roomId)
if (userId != null) {
query.equalTo(RoomMemberEntityFields.USER_ID, userId)
}
return query
}

View File

@ -54,7 +54,7 @@ internal fun TimelineEventEntity.Companion.where(realm: Realm,
internal fun TimelineEventEntity.Companion.findWithSenderMembershipEvent(realm: Realm, senderMembershipEventId: String): List<TimelineEventEntity> {
return realm.where<TimelineEventEntity>()
.equalTo(TimelineEventEntityFields.SENDER_MEMBERSHIP_EVENT.EVENT_ID, senderMembershipEventId)
.equalTo(TimelineEventEntityFields.SENDER_MEMBERSHIP_EVENT_ID, senderMembershipEventId)
.findAll()
}

View File

@ -26,7 +26,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher
import org.matrix.olm.OlmManager
import java.util.concurrent.Executors
@Module
internal object MatrixModule {
@ -38,8 +37,7 @@ internal object MatrixModule {
return MatrixCoroutineDispatchers(io = Dispatchers.IO,
computation = Dispatchers.Default,
main = Dispatchers.Main,
crypto = createBackgroundHandler("Crypto_Thread").asCoroutineDispatcher(),
sync = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
crypto = createBackgroundHandler("Crypto_Thread").asCoroutineDispatcher()
)
}

View File

@ -31,3 +31,10 @@ internal annotation class UserId
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
internal annotation class UserMd5
/**
* Used to inject the sessionId, which is defined as md5(userId|deviceId)
*/
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
internal annotation class SessionId

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.network
import android.content.Context
import androidx.annotation.WorkerThread
import com.novoda.merlin.Merlin
import com.novoda.merlin.MerlinsBeard
import im.vector.matrix.android.internal.di.MatrixScope
@ -28,8 +29,8 @@ import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@MatrixScope
internal class NetworkConnectivityChecker @Inject constructor(context: Context,
backgroundDetectionObserver: BackgroundDetectionObserver)
internal class NetworkConnectivityChecker @Inject constructor(private val context: Context,
private val backgroundDetectionObserver: BackgroundDetectionObserver)
: BackgroundDetectionObserver.Listener {
private val merlin = Merlin.Builder()
@ -37,19 +38,33 @@ internal class NetworkConnectivityChecker @Inject constructor(context: Context,
.withDisconnectableCallbacks()
.build(context)
private val listeners = Collections.synchronizedSet(LinkedHashSet<Listener>())
private val merlinsBeard = MerlinsBeard.Builder().build(context)
// True when internet is available
var hasInternetAccess = MerlinsBeard.Builder().build(context).isConnected
private set
private val listeners = Collections.synchronizedSet(LinkedHashSet<Listener>())
private var hasInternetAccess = merlinsBeard.isConnected
init {
backgroundDetectionObserver.register(this)
}
/**
* Returns true when internet is available
*/
@WorkerThread
fun hasInternetAccess(): Boolean {
// If we are in background we have unbound merlin, so we have to check
return if (backgroundDetectionObserver.isInBackground) {
merlinsBeard.hasInternetAccess()
} else {
hasInternetAccess
}
}
override fun onMoveToForeground() {
merlin.bind()
merlinsBeard.hasInternetAccess {
hasInternetAccess = it
}
merlin.registerDisconnectable {
if (hasInternetAccess) {
Timber.v("On Disconnect")
@ -76,14 +91,17 @@ internal class NetworkConnectivityChecker @Inject constructor(context: Context,
merlin.unbind()
}
// In background you won't get notification as merlin is unbound
suspend fun waitUntilConnected() {
if (hasInternetAccess) {
return
} else {
Timber.v("Waiting for network...")
suspendCoroutine<Unit> { continuation ->
register(object : Listener {
override fun onConnect() {
unregister(this)
Timber.v("Connected to network...")
continuation.resume(Unit)
}
})

View File

@ -0,0 +1,33 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.query
import io.realm.RealmObject
import io.realm.RealmQuery
fun <T : RealmObject, E : Enum<E>> RealmQuery<T>.process(field: String, enums: List<Enum<E>>): RealmQuery<T> {
val lastEnumValue = enums.lastOrNull()
beginGroup()
for (enumValue in enums) {
equalTo(field, enumValue.name)
if (enumValue != lastEnumValue) {
or()
}
}
endGroup()
return this
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.query
import im.vector.matrix.android.api.query.QueryStringValue
import io.realm.Case
import io.realm.RealmObject
import io.realm.RealmQuery
import timber.log.Timber
fun <T : RealmObject> RealmQuery<T>.process(field: String, queryStringValue: QueryStringValue): RealmQuery<T> {
when (queryStringValue) {
is QueryStringValue.NoCondition -> Timber.v("No condition to process")
is QueryStringValue.IsNotNull -> isNotNull(field)
is QueryStringValue.IsNull -> isNull(field)
is QueryStringValue.IsEmpty -> isEmpty(field)
is QueryStringValue.IsNotEmpty -> isNotEmpty(field)
is QueryStringValue.Equals -> equalTo(field, queryStringValue.string, queryStringValue.case.toRealmCase())
is QueryStringValue.Contains -> contains(field, queryStringValue.string, queryStringValue.case.toRealmCase())
}
return this
}
private fun QueryStringValue.Case.toRealmCase(): Case {
return when (this) {
QueryStringValue.Case.INSENSITIVE -> Case.INSENSITIVE
QueryStringValue.Case.SENSITIVE -> Case.SENSITIVE
}
}

View File

@ -25,7 +25,7 @@ import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
import im.vector.matrix.android.internal.di.UserMd5
import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.md5
@ -42,7 +42,7 @@ import java.io.IOException
import javax.inject.Inject
internal class DefaultFileService @Inject constructor(private val context: Context,
@UserMd5 private val userMd5: String,
@SessionId private val sessionId: String,
private val contentUrlResolver: ContentUrlResolver,
private val coroutineDispatchers: MatrixCoroutineDispatchers) : FileService {
@ -103,9 +103,9 @@ internal class DefaultFileService @Inject constructor(private val context: Conte
return when (downloadMode) {
FileService.DownloadMode.FOR_INTERNAL_USE -> {
// Create dir tree (MF stands for Matrix File):
// <cache>/MF/<md5(userId)>/<md5(id)>/
// <cache>/MF/<sessionId>/<md5(id)>/
val tmpFolderRoot = File(context.cacheDir, "MF")
val tmpFolderUser = File(tmpFolderRoot, userMd5)
val tmpFolderUser = File(tmpFolderRoot, sessionId)
File(tmpFolderUser, id.md5())
}
FileService.DownloadMode.TO_EXPORT -> {

View File

@ -45,6 +45,8 @@ import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.crypto.DefaultCryptoService
import im.vector.matrix.android.internal.database.LiveEntityObserver
import im.vector.matrix.android.internal.session.sync.SyncTaskSequencer
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
import im.vector.matrix.android.internal.session.sync.job.SyncThread
import im.vector.matrix.android.internal.session.sync.job.SyncWorker
import kotlinx.coroutines.Dispatchers
@ -76,24 +78,26 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
private val secureStorageService: Lazy<SecureStorageService>,
private val syncThreadProvider: Provider<SyncThread>,
private val contentUrlResolver: ContentUrlResolver,
private val syncTokenStore: SyncTokenStore,
private val syncTaskSequencer: SyncTaskSequencer,
private val sessionParamsStore: SessionParamsStore,
private val contentUploadProgressTracker: ContentUploadStateTracker,
private val initialSyncProgressService: Lazy<InitialSyncProgressService>,
private val homeServerCapabilitiesService: Lazy<HomeServerCapabilitiesService>)
: Session,
RoomService by roomService.get(),
RoomDirectoryService by roomDirectoryService.get(),
GroupService by groupService.get(),
UserService by userService.get(),
CryptoService by cryptoService.get(),
SignOutService by signOutService.get(),
FilterService by filterService.get(),
PushRuleService by pushRuleService.get(),
PushersService by pushersService.get(),
FileService by fileService.get(),
InitialSyncProgressService by initialSyncProgressService.get(),
SecureStorageService by secureStorageService.get(),
HomeServerCapabilitiesService by homeServerCapabilitiesService.get() {
RoomService by roomService.get(),
RoomDirectoryService by roomDirectoryService.get(),
GroupService by groupService.get(),
UserService by userService.get(),
CryptoService by cryptoService.get(),
SignOutService by signOutService.get(),
FilterService by filterService.get(),
PushRuleService by pushRuleService.get(),
PushersService by pushersService.get(),
FileService by fileService.get(),
InitialSyncProgressService by initialSyncProgressService.get(),
SecureStorageService by secureStorageService.get(),
HomeServerCapabilitiesService by homeServerCapabilitiesService.get() {
private var isOpen = false
@ -149,12 +153,17 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
cryptoService.get().close()
isOpen = false
EventBus.getDefault().unregister(this)
syncTaskSequencer.close()
}
override fun syncState(): LiveData<SyncState> {
override fun getSyncStateLive(): LiveData<SyncState> {
return getSyncThread().liveState()
}
override fun hasAlreadySynced(): Boolean {
return syncTokenStore.getLastToken() != null
}
private fun getSyncThread(): SyncThread {
return syncThread ?: syncThreadProvider.get().also {
syncThread = it
@ -164,23 +173,14 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
override fun clearCache(callback: MatrixCallback<Unit>) {
stopSync()
stopAnyBackgroundSync()
cacheService.get().clearCache(object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
startSync(true)
callback.onSuccess(data)
}
override fun onFailure(failure: Throwable) {
startSync(true)
callback.onFailure(failure)
}
})
liveEntityObservers.forEach { it.cancelProcess() }
cacheService.get().clearCache(callback)
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onGlobalError(globalError: GlobalError) {
if (globalError is GlobalError.InvalidToken
&& globalError.softLogout) {
&& globalError.softLogout) {
// Mark the token has invalid
GlobalScope.launch(Dispatchers.IO) {
sessionParamsStore.setTokenInvalid(myUserId)

View File

@ -46,6 +46,7 @@ import im.vector.matrix.android.internal.session.sync.job.SyncWorker
import im.vector.matrix.android.internal.session.user.UserModule
import im.vector.matrix.android.internal.session.user.accountdata.AccountDataModule
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
@Component(dependencies = [MatrixComponent::class],
modules = [
@ -69,6 +70,8 @@ import im.vector.matrix.android.internal.task.TaskExecutor
@SessionScope
internal interface SessionComponent {
fun coroutineDispatchers(): MatrixCoroutineDispatchers
fun session(): Session
fun syncTask(): SyncTask

View File

@ -30,6 +30,7 @@ import im.vector.matrix.android.api.session.InitialSyncProgressService
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService
import im.vector.matrix.android.api.session.securestorage.SecureStorageService
import im.vector.matrix.android.internal.auth.createSessionId
import im.vector.matrix.android.internal.database.LiveEntityObserver
import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory
import im.vector.matrix.android.internal.di.*
@ -54,8 +55,7 @@ internal abstract class SessionModule {
@Module
companion object {
internal const val DB_ALIAS_PREFIX = "session_db_"
internal fun getKeyAlias(userMd5: String) = "session_db_$userMd5"
@JvmStatic
@Provides
@ -83,11 +83,26 @@ internal abstract class SessionModule {
return userId.md5()
}
@JvmStatic
@SessionId
@Provides
fun providesSessionId(credentials: Credentials): String {
return createSessionId(credentials.userId, credentials.deviceId)
}
@JvmStatic
@Provides
@UserCacheDirectory
fun providesFilesDir(@UserMd5 userMd5: String, context: Context): File {
return File(context.filesDir, userMd5)
fun providesFilesDir(@UserMd5 userMd5: String,
@SessionId sessionId: String,
context: Context): File {
// Temporary code for migration
val old = File(context.filesDir, userMd5)
if (old.exists()) {
old.renameTo(File(context.filesDir, sessionId))
}
return File(context.filesDir, sessionId)
}
@JvmStatic

View File

@ -20,11 +20,16 @@ import androidx.lifecycle.LiveData
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.group.Group
import im.vector.matrix.android.api.session.group.GroupService
import im.vector.matrix.android.api.session.group.GroupSummaryQueryParams
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.GroupSummaryEntity
import im.vector.matrix.android.internal.database.model.GroupSummaryEntityFields
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.query.process
import im.vector.matrix.android.internal.util.fetchCopyMap
import io.realm.Realm
import io.realm.RealmQuery
import javax.inject.Inject
internal class DefaultGroupService @Inject constructor(private val monarchy: Monarchy) : GroupService {
@ -33,10 +38,30 @@ internal class DefaultGroupService @Inject constructor(private val monarchy: Mon
return null
}
override fun liveGroupSummaries(): LiveData<List<GroupSummary>> {
return monarchy.findAllMappedWithChanges(
{ realm -> GroupSummaryEntity.where(realm).isNotEmpty(GroupSummaryEntityFields.DISPLAY_NAME) },
override fun getGroupSummary(groupId: String): GroupSummary? {
return monarchy.fetchCopyMap(
{ realm -> GroupSummaryEntity.where(realm, groupId).findFirst() },
{ it, _ -> it.asDomain() }
)
}
override fun getGroupSummaries(groupSummaryQueryParams: GroupSummaryQueryParams): List<GroupSummary> {
return monarchy.fetchAllMappedSync(
{ groupSummariesQuery(it, groupSummaryQueryParams) },
{ it.asDomain() }
)
}
override fun getGroupSummariesLive(groupSummaryQueryParams: GroupSummaryQueryParams): LiveData<List<GroupSummary>> {
return monarchy.findAllMappedWithChanges(
{ groupSummariesQuery(it, groupSummaryQueryParams) },
{ it.asDomain() }
)
}
private fun groupSummariesQuery(realm: Realm, queryParams: GroupSummaryQueryParams): RealmQuery<GroupSummaryEntity> {
return GroupSummaryEntity.where(realm)
.process(GroupSummaryEntityFields.DISPLAY_NAME, queryParams.displayName)
.process(GroupSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships)
}
}

View File

@ -22,6 +22,7 @@ import androidx.work.WorkManager
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
import im.vector.matrix.android.internal.database.awaitTransaction
import im.vector.matrix.android.internal.database.model.GroupEntity
import im.vector.matrix.android.internal.database.model.GroupSummaryEntity
import im.vector.matrix.android.internal.database.query.where
@ -31,6 +32,7 @@ import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWor
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
import io.realm.OrderedCollectionChangeSet
import io.realm.RealmResults
import kotlinx.coroutines.launch
import javax.inject.Inject
private const val GET_GROUP_DATA_WORKER = "GET_GROUP_DATA_WORKER"
@ -49,14 +51,19 @@ internal class GroupSummaryUpdater @Inject constructor(private val context: Cont
.mapNotNull { results[it] }
fetchGroupsData(modifiedGroupEntity
.filter { it.membership == Membership.JOIN || it.membership == Membership.INVITE }
.map { it.groupId }
.toList())
.filter { it.membership == Membership.JOIN || it.membership == Membership.INVITE }
.map { it.groupId }
.toList())
deleteGroups(modifiedGroupEntity
modifiedGroupEntity
.filter { it.membership == Membership.LEAVE }
.map { it.groupId }
.toList())
.toList()
.also {
observerScope.launch {
deleteGroups(it)
}
}
}
private fun fetchGroupsData(groupIds: List<String>) {
@ -77,12 +84,9 @@ internal class GroupSummaryUpdater @Inject constructor(private val context: Cont
/**
* Delete the GroupSummaryEntity of left groups
*/
private fun deleteGroups(groupIds: List<String>) {
monarchy
.writeAsync { realm ->
GroupSummaryEntity.where(realm, groupIds)
.findAll()
.deleteAllFromRealm()
}
private suspend fun deleteGroups(groupIds: List<String>) = awaitTransaction(monarchy.realmConfiguration) { realm ->
GroupSummaryEntity.where(realm, groupIds)
.findAll()
.deleteAllFromRealm()
}
}

View File

@ -86,7 +86,7 @@ internal class DefaultPusherService @Inject constructor(private val context: Con
.executeBy(taskExecutor)
}
override fun livePushers(): LiveData<List<Pusher>> {
override fun getPushersLive(): LiveData<List<Pusher>> {
return monarchy.findAllMappedWithChanges(
{ realm -> PusherEntity.where(realm) },
{ it.asDomain() }

View File

@ -21,6 +21,7 @@ import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.VersioningState
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
@ -30,7 +31,9 @@ import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
import im.vector.matrix.android.internal.database.query.findByAlias
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.query.process
import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
@ -38,7 +41,9 @@ import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask
import im.vector.matrix.android.internal.session.user.accountdata.UpdateBreadcrumbsTask
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.fetchCopyMap
import io.realm.Realm
import io.realm.RealmQuery
import javax.inject.Inject
internal class DefaultRoomService @Inject constructor(private val monarchy: Monarchy,
@ -69,30 +74,66 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
}
}
override fun liveRoomSummaries(): LiveData<List<RoomSummary>> {
return monarchy.findAllMappedWithChanges(
{ realm ->
RoomSummaryEntity.where(realm)
.isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME)
.notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name)
},
override fun getRoomSummary(roomIdOrAlias: String): RoomSummary? {
return monarchy
.fetchCopyMap({
if (roomIdOrAlias.startsWith("!")) {
// It's a roomId
RoomSummaryEntity.where(it, roomId = roomIdOrAlias).findFirst()
} else {
// Assume it's a room alias
RoomSummaryEntity.findByAlias(it, roomIdOrAlias)
}
}, { entity, _ ->
roomSummaryMapper.map(entity)
})
}
override fun getRoomSummaries(queryParams: RoomSummaryQueryParams): List<RoomSummary> {
return monarchy.fetchAllMappedSync(
{ roomSummariesQuery(it, queryParams) },
{ roomSummaryMapper.map(it) }
)
}
override fun liveBreadcrumbs(): LiveData<List<RoomSummary>> {
override fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams): LiveData<List<RoomSummary>> {
return monarchy.findAllMappedWithChanges(
{ realm ->
RoomSummaryEntity.where(realm)
.isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME)
.notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name)
.greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummaryEntity.NOT_IN_BREADCRUMBS)
.sort(RoomSummaryEntityFields.BREADCRUMBS_INDEX)
},
{ roomSummariesQuery(it, queryParams) },
{ roomSummaryMapper.map(it) }
)
}
private fun roomSummariesQuery(realm: Realm, queryParams: RoomSummaryQueryParams): RealmQuery<RoomSummaryEntity> {
val query = RoomSummaryEntity.where(realm)
query.process(RoomSummaryEntityFields.DISPLAY_NAME, queryParams.displayName)
query.process(RoomSummaryEntityFields.CANONICAL_ALIAS, queryParams.canonicalAlias)
query.process(RoomSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships)
query.notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name)
return query
}
override fun getBreadcrumbs(): List<RoomSummary> {
return monarchy.fetchAllMappedSync(
{ breadcrumbsQuery(it) },
{ roomSummaryMapper.map(it) }
)
}
override fun getBreadcrumbsLive(): LiveData<List<RoomSummary>> {
return monarchy.findAllMappedWithChanges(
{ breadcrumbsQuery(it) },
{ roomSummaryMapper.map(it) }
)
}
private fun breadcrumbsQuery(realm: Realm): RealmQuery<RoomSummaryEntity> {
return RoomSummaryEntity.where(realm)
.isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME)
.notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name)
.greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummaryEntity.NOT_IN_BREADCRUMBS)
.sort(RoomSummaryEntityFields.BREADCRUMBS_INDEX)
}
override fun onRoomDisplayed(roomId: String): Cancelable {
return updateBreadcrumbsTask
.configureWith(UpdateBreadcrumbsTask.Params(roomId))

View File

@ -23,11 +23,10 @@ import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.query.types
import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import io.realm.OrderedCollectionChangeSet
import io.realm.RealmConfiguration
import io.realm.RealmResults
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ -39,8 +38,7 @@ import javax.inject.Inject
internal class EventRelationsAggregationUpdater @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration,
@UserId private val userId: String,
private val task: EventRelationsAggregationTask,
private val taskExecutor: TaskExecutor) :
private val task: EventRelationsAggregationTask) :
RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
override val query = Monarchy.Query<EventEntity> {
@ -63,6 +61,8 @@ internal class EventRelationsAggregationUpdater @Inject constructor(@SessionData
insertedDomains,
userId
)
task.configureWith(params).executeBy(taskExecutor)
observerScope.launch {
task.execute(params)
}
}
}

View File

@ -212,11 +212,12 @@ internal interface RoomAPI {
/**
* Join the given room.
*
* @param roomId the room id
* @param roomIdOrAlias the room id or alias
* @param server_name the servers to attempt to join the room through
* @param params the request body
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/join")
fun join(@Path("roomId") roomId: String,
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "join/{roomIdOrAlias}")
fun join(@Path("roomIdOrAlias") roomIdOrAlias: String,
@Query("server_name") viaServers: List<String>,
@Body params: Map<String, String?>): Call<Unit>

View File

@ -20,10 +20,9 @@ import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomAvatarContent
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.internal.database.mapper.ContentMapper
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.model.RoomMemberEntityFields
import im.vector.matrix.android.internal.database.query.prev
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.UserId
@ -47,19 +46,15 @@ internal class RoomAvatarResolver @Inject constructor(private val monarchy: Mona
return@doWithRealm
}
val roomMembers = RoomMembers(realm, roomId)
val members = roomMembers.queryRoomMembersEvent().findAll()
val members = roomMembers.queryActiveRoomMembersEvent().findAll()
// detect if it is a room with no more than 2 members (i.e. an alone or a 1:1 chat)
if (members.size == 1) {
res = members.firstOrNull()?.toRoomMember()?.avatarUrl
res = members.firstOrNull()?.avatarUrl
} else if (members.size == 2) {
val firstOtherMember = members.where().notEqualTo(EventEntityFields.STATE_KEY, userId).findFirst()
res = firstOtherMember?.toRoomMember()?.avatarUrl
val firstOtherMember = members.where().notEqualTo(RoomMemberEntityFields.USER_ID, userId).findFirst()
res = firstOtherMember?.avatarUrl
}
}
return res
}
private fun EventEntity?.toRoomMember(): RoomMember? {
return ContentMapper.map(this?.content).toModel<RoomMember>()
}
}

View File

@ -20,6 +20,7 @@ import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.room.draft.DefaultDraftService
import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService
import im.vector.matrix.android.internal.session.room.notification.DefaultRoomPushRuleService
@ -35,6 +36,7 @@ internal interface RoomFactory {
fun create(roomId: String): Room
}
@SessionScope
internal class DefaultRoomFactory @Inject constructor(private val monarchy: Monarchy,
private val roomSummaryMapper: RoomSummaryMapper,
private val cryptoService: CryptoService,

View File

@ -24,10 +24,11 @@ import im.vector.matrix.android.api.session.room.model.RoomAliasesContent
import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
import im.vector.matrix.android.internal.database.mapper.ContentMapper
import im.vector.matrix.android.internal.database.model.*
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.query.*
import im.vector.matrix.android.internal.database.query.isEventRead
import im.vector.matrix.android.internal.database.query.latestEvent
import im.vector.matrix.android.internal.database.query.prev
@ -38,7 +39,6 @@ import im.vector.matrix.android.internal.session.room.membership.RoomMembers
import im.vector.matrix.android.internal.session.sync.model.RoomSyncSummary
import im.vector.matrix.android.internal.session.sync.model.RoomSyncUnreadNotifications
import io.realm.Realm
import io.realm.kotlin.createObject
import javax.inject.Inject
internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId: String,
@ -52,7 +52,7 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId
EventType.STATE_ROOM_NAME,
EventType.STATE_ROOM_TOPIC,
EventType.STATE_ROOM_MEMBER,
EventType.STATE_HISTORY_VISIBILITY,
EventType.STATE_ROOM_HISTORY_VISIBILITY,
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_ANSWER,
@ -69,9 +69,7 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId
roomSummary: RoomSyncSummary? = null,
unreadNotifications: RoomSyncUnreadNotifications? = null,
updateMembers: Boolean = false) {
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId)
val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
if (roomSummary != null) {
if (roomSummary.heroes.isNotEmpty()) {
roomSummaryEntity.heroes.clear()
@ -93,12 +91,13 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId
val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = PREVIEWABLE_TYPES)
val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()
val lastCanonicalAliasEvent = EventEntity.where(realm, roomId, EventType.STATE_CANONICAL_ALIAS).prev()
val lastCanonicalAliasEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_CANONICAL_ALIAS).prev()
val lastAliasesEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).prev()
val encryptionEvent = EventEntity.where(realm, roomId, EventType.ENCRYPTION).prev()
roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0
// avoid this call if we are sure there are unread events
|| !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId)
// avoid this call if we are sure there are unread events
|| !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId)
roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString()
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId)
@ -107,18 +106,20 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId
roomSummaryEntity.canonicalAlias = ContentMapper.map(lastCanonicalAliasEvent?.content).toModel<RoomCanonicalAliasContent>()
?.canonicalAlias
val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel<RoomAliasesContent>()?.aliases ?: emptyList()
val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel<RoomAliasesContent>()?.aliases
?: emptyList()
roomSummaryEntity.aliases.clear()
roomSummaryEntity.aliases.addAll(roomAliases)
roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|")
roomSummaryEntity.isEncrypted = encryptionEvent != null
if (updateMembers) {
val otherRoomMembers = RoomMembers(realm, roomId)
.queryRoomMembersEvent()
.notEqualTo(EventEntityFields.STATE_KEY, userId)
.notEqualTo(RoomMemberEntityFields.USER_ID, userId)
.findAll()
.asSequence()
.map { it.stateKey }
.map { it.userId }
roomSummaryEntity.otherMemberIds.clear()
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers)

View File

@ -20,7 +20,7 @@ import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.room.failure.CreateRoomFailure
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse
import im.vector.matrix.android.internal.database.RealmQueryLatch
import im.vector.matrix.android.internal.database.awaitNotEmptyResult
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomEntityFields
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
@ -34,6 +34,7 @@ import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAcco
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.awaitTransaction
import io.realm.RealmConfiguration
import kotlinx.coroutines.TimeoutCancellationException
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@ -53,13 +54,12 @@ internal class DefaultCreateRoomTask @Inject constructor(private val roomAPI: Ro
}
val roomId = createRoomResponse.roomId!!
// Wait for room to come back from the sync (but it can maybe be in the DB if the sync response is received before)
val rql = RealmQueryLatch<RoomEntity>(realmConfiguration) { realm ->
realm.where(RoomEntity::class.java)
.equalTo(RoomEntityFields.ROOM_ID, roomId)
}
try {
rql.await(timeout = 1L, timeUnit = TimeUnit.MINUTES)
} catch (exception: Exception) {
awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->
realm.where(RoomEntity::class.java)
.equalTo(RoomEntityFields.ROOM_ID, roomId)
}
} catch (exception: TimeoutCancellationException) {
throw CreateRoomFailure.CreatedWithTimeout
}
if (params.isDirect()) {

View File

@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.VersioningState
import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
import im.vector.matrix.android.internal.database.awaitTransaction
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
@ -30,9 +31,9 @@ import im.vector.matrix.android.internal.database.query.types
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.SessionDatabase
import io.realm.OrderedCollectionChangeSet
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.RealmResults
import kotlinx.coroutines.launch
import javax.inject.Inject
internal class RoomCreateEventLiveObserver @Inject constructor(@SessionDatabase
@ -51,21 +52,21 @@ internal class RoomCreateEventLiveObserver @Inject constructor(@SessionDatabase
}
.toList()
.also {
handleRoomCreateEvents(it)
observerScope.launch {
handleRoomCreateEvents(it)
}
}
}
private fun handleRoomCreateEvents(createEvents: List<Event>) = Realm.getInstance(realmConfiguration).use {
it.executeTransactionAsync { realm ->
for (event in createEvents) {
val createRoomContent = event.getClearContent().toModel<RoomCreateContent>()
val predecessorRoomId = createRoomContent?.predecessor?.roomId ?: continue
private suspend fun handleRoomCreateEvents(createEvents: List<Event>) = awaitTransaction(realmConfiguration) { realm ->
for (event in createEvents) {
val createRoomContent = event.getClearContent().toModel<RoomCreateContent>()
val predecessorRoomId = createRoomContent?.predecessor?.roomId ?: continue
val predecessorRoomSummary = RoomSummaryEntity.where(realm, predecessorRoomId).findFirst()
?: RoomSummaryEntity(predecessorRoomId)
predecessorRoomSummary.versioningState = VersioningState.UPGRADED_ROOM_JOINED
realm.insertOrUpdate(predecessorRoomSummary)
}
val predecessorRoomSummary = RoomSummaryEntity.where(realm, predecessorRoomId).findFirst()
?: RoomSummaryEntity(predecessorRoomId)
predecessorRoomSummary.versioningState = VersioningState.UPGRADED_ROOM_JOINED
realm.insertOrUpdate(predecessorRoomSummary)
}
}
}

View File

@ -21,18 +21,23 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.members.MembershipService
import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.RoomMemberEntity
import im.vector.matrix.android.internal.database.model.RoomMemberEntityFields
import im.vector.matrix.android.internal.query.process
import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask
import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.fetchCopied
import io.realm.Realm
import io.realm.RealmQuery
internal class DefaultMembershipService @AssistedInject constructor(@Assisted private val roomId: String,
private val monarchy: Monarchy,
@ -58,29 +63,44 @@ internal class DefaultMembershipService @AssistedInject constructor(@Assisted pr
}
override fun getRoomMember(userId: String): RoomMember? {
val eventEntity = monarchy.fetchCopied {
RoomMembers(it, roomId).queryRoomMemberEvent(userId).findFirst()
val roomMemberEntity = monarchy.fetchCopied {
RoomMembers(it, roomId).getLastRoomMember(userId)
}
return eventEntity?.asDomain()?.content.toModel()
return roomMemberEntity?.asDomain()
}
override fun getRoomMemberIdsLive(): LiveData<List<String>> {
return monarchy.findAllMappedWithChanges(
override fun getRoomMembers(queryParams: RoomMemberQueryParams): List<RoomMember> {
return monarchy.fetchAllMappedSync(
{
RoomMembers(it, roomId).queryRoomMembersEvent()
roomMembersQuery(it, queryParams)
},
{
it.stateKey!!
it.asDomain()
}
)
}
override fun getRoomMembersLive(queryParams: RoomMemberQueryParams): LiveData<List<RoomMember>> {
return monarchy.findAllMappedWithChanges(
{
roomMembersQuery(it, queryParams)
},
{
it.asDomain()
}
)
}
private fun roomMembersQuery(realm: Realm, queryParams: RoomMemberQueryParams): RealmQuery<RoomMemberEntity> {
return RoomMembers(realm, roomId).queryRoomMembersEvent()
.process(RoomMemberEntityFields.MEMBERSHIP_STR, queryParams.memberships)
.process(RoomMemberEntityFields.DISPLAY_NAME, queryParams.displayName)
}
override fun getNumberOfJoinedMembers(): Int {
var result = 0
monarchy.runTransactionSync {
result = RoomMembers(it, roomId).getNumberOfJoinedMembers()
return Realm.getInstance(monarchy.realmConfiguration).use {
RoomMembers(it, roomId).getNumberOfJoinedMembers()
}
return result
}
override fun invite(userId: String, reason: String?, callback: MatrixCallback<Unit>): Cancelable {

View File

@ -18,15 +18,14 @@ package im.vector.matrix.android.internal.session.room.membership
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.internal.database.helper.TimelineEventSenderVisitor
import im.vector.matrix.android.internal.database.helper.addStateEvent
import im.vector.matrix.android.internal.database.helper.updateSenderData
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
import im.vector.matrix.android.internal.session.user.UserEntityFactory
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.awaitTransaction
import io.realm.Realm
@ -44,7 +43,9 @@ internal interface LoadRoomMembersTask : Task<LoadRoomMembersTask.Params, Unit>
internal class DefaultLoadRoomMembersTask @Inject constructor(private val roomAPI: RoomAPI,
private val monarchy: Monarchy,
private val syncTokenStore: SyncTokenStore,
private val roomSummaryUpdater: RoomSummaryUpdater
private val roomSummaryUpdater: RoomSummaryUpdater,
private val roomMemberEventHandler: RoomMemberEventHandler,
private val timelineEventSenderVisitor: TimelineEventSenderVisitor
) : LoadRoomMembersTask {
override suspend fun execute(params: LoadRoomMembersTask.Params) {
@ -66,12 +67,11 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(private val roomAP
for (roomMemberEvent in response.roomMemberEvents) {
roomEntity.addStateEvent(roomMemberEvent)
UserEntityFactory.createOrNull(roomMemberEvent)?.also {
realm.insertOrUpdate(it)
}
roomMemberEventHandler.handle(realm, roomId, roomMemberEvent)
}
timelineEventSenderVisitor.clear()
roomEntity.chunks.flatMap { it.timelineEvents }.forEach {
it.updateSenderData()
timelineEventSenderVisitor.visit(it)
}
roomEntity.areAllMembersLoaded = true
roomSummaryUpdater.update(realm, roomId, updateMembers = true)

View File

@ -23,9 +23,10 @@ import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.*
import im.vector.matrix.android.internal.database.mapper.ContentMapper
import im.vector.matrix.android.internal.database.model.*
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomMemberEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.query.prev
import im.vector.matrix.android.internal.database.query.where
@ -62,7 +63,7 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context:
return@doWithRealm
}
val canonicalAlias = EventEntity.where(realm, roomId, EventType.STATE_CANONICAL_ALIAS).prev()
val canonicalAlias = EventEntity.where(realm, roomId, EventType.STATE_ROOM_CANONICAL_ALIAS).prev()
name = ContentMapper.map(canonicalAlias?.content).toModel<RoomCanonicalAliasContent>()?.canonicalAlias
if (!name.isNullOrEmpty()) {
return@doWithRealm
@ -75,43 +76,46 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context:
}
val roomMembers = RoomMembers(realm, roomId)
val loadedMembers = roomMembers.queryRoomMembersEvent().findAll()
val activeMembers = roomMembers.queryActiveRoomMembersEvent().findAll()
if (roomEntity?.membership == Membership.INVITE) {
val inviteMeEvent = roomMembers.queryRoomMemberEvent(userId).findFirst()
val inviteMeEvent = roomMembers.getLastStateEvent(userId)
val inviterId = inviteMeEvent?.sender
name = if (inviterId != null) {
val inviterMemberEvent = loadedMembers.where()
.equalTo(EventEntityFields.STATE_KEY, inviterId)
activeMembers.where()
.equalTo(RoomMemberEntityFields.USER_ID, inviterId)
.findFirst()
inviterMemberEvent?.toRoomMember()?.displayName
?.displayName
} else {
context.getString(R.string.room_displayname_room_invite)
}
} else if (roomEntity?.membership == Membership.JOIN) {
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
val otherMembersSubset: List<EventEntity> = if (roomSummary?.heroes?.isNotEmpty() == true) {
roomSummary.heroes.mapNotNull {
roomMembers.getStateEvent(it)
val otherMembersSubset: List<RoomMemberEntity> = if (roomSummary?.heroes?.isNotEmpty() == true) {
roomSummary.heroes.mapNotNull { userId ->
roomMembers.getLastRoomMember(userId)?.takeIf {
it.membership == Membership.INVITE || it.membership == Membership.JOIN
}
}
} else {
loadedMembers.where()
.notEqualTo(EventEntityFields.STATE_KEY, userId)
activeMembers.where()
.notEqualTo(RoomMemberEntityFields.USER_ID, userId)
.limit(3)
.findAll()
.createSnapshot()
}
val otherMembersCount = roomMembers.getNumberOfMembers() - 1
val otherMembersCount = otherMembersSubset.count()
name = when (otherMembersCount) {
0 -> context.getString(R.string.room_displayname_empty_room)
1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers)
2 -> context.getString(R.string.room_displayname_two_members,
resolveRoomMemberName(otherMembersSubset[0], roomMembers),
resolveRoomMemberName(otherMembersSubset[1], roomMembers)
resolveRoomMemberName(otherMembersSubset[0], roomMembers),
resolveRoomMemberName(otherMembersSubset[1], roomMembers)
)
else -> context.resources.getQuantityString(R.plurals.room_displayname_three_and_more_members,
roomMembers.getNumberOfJoinedMembers() - 1,
resolveRoomMemberName(otherMembersSubset[0], roomMembers),
roomMembers.getNumberOfJoinedMembers() - 1)
roomMembers.getNumberOfJoinedMembers() - 1,
resolveRoomMemberName(otherMembersSubset[0], roomMembers),
roomMembers.getNumberOfJoinedMembers() - 1)
}
}
return@doWithRealm
@ -119,19 +123,14 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context:
return name ?: roomId
}
private fun resolveRoomMemberName(eventEntity: EventEntity?,
private fun resolveRoomMemberName(roomMember: RoomMemberEntity?,
roomMembers: RoomMembers): String? {
if (eventEntity == null) return null
val roomMember = eventEntity.toRoomMember() ?: return null
if (roomMember == null) return null
val isUnique = roomMembers.isUniqueDisplayName(roomMember.displayName)
return if (isUnique) {
roomMember.displayName
} else {
"${roomMember.displayName} (${eventEntity.stateKey})"
"${roomMember.displayName} (${roomMember.userId})"
}
}
private fun EventEntity?.toRoomMember(): RoomMember? {
return ContentMapper.map(this?.content).toModel<RoomMember>()
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.room.membership
import im.vector.matrix.android.api.session.room.model.RoomMemberContent
import im.vector.matrix.android.internal.database.model.RoomMemberEntity
internal object RoomMemberEntityFactory {
fun create(roomId: String, userId: String, roomMember: RoomMemberContent): RoomMemberEntity {
val primaryKey = "${roomId}_$userId"
return RoomMemberEntity(
primaryKey = primaryKey,
userId = userId,
roomId = roomId,
displayName = roomMember.displayName ?: "",
avatarUrl = roomMember.avatarUrl ?: ""
).apply {
membership = roomMember.membership
}
}
}

View File

@ -0,0 +1,44 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.room.membership
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomMemberContent
import im.vector.matrix.android.internal.session.user.UserEntityFactory
import io.realm.Realm
import javax.inject.Inject
internal class RoomMemberEventHandler @Inject constructor() {
fun handle(realm: Realm, roomId: String, event: Event): Boolean {
if (event.type != EventType.STATE_ROOM_MEMBER) {
return false
}
val roomMember = event.content.toModel<RoomMemberContent>() ?: return false
val userId = event.stateKey ?: return false
val roomMemberEntity = RoomMemberEntityFactory.create(roomId, userId, roomMember)
realm.insertOrUpdate(roomMemberEntity)
if (roomMember.membership in Membership.activeMemberships()) {
val userEntity = UserEntityFactory.create(userId, roomMember)
realm.insertOrUpdate(userEntity)
}
return true
}
}

View File

@ -17,12 +17,10 @@
package im.vector.matrix.android.internal.session.room.membership
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.*
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.model.RoomMemberEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.query.where
import io.realm.Realm
@ -42,19 +40,18 @@ internal class RoomMembers(private val realm: Realm,
RoomSummaryEntity.where(realm, roomId).findFirst()
}
fun getStateEvent(userId: String): EventEntity? {
fun getLastStateEvent(userId: String): EventEntity? {
return EventEntity
.where(realm, roomId, EventType.STATE_ROOM_MEMBER)
.sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING)
.equalTo(EventEntityFields.STATE_KEY, userId)
.sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING)
.findFirst()
}
fun get(userId: String): RoomMember? {
return getStateEvent(userId)
?.let {
it.asDomain().content?.toModel<RoomMember>()
}
fun getLastRoomMember(userId: String): RoomMemberEntity? {
return RoomMemberEntity
.where(realm, roomId, userId)
.findFirst()
}
fun isUniqueDisplayName(displayName: String?): Boolean {
@ -69,36 +66,37 @@ internal class RoomMembers(private val realm: Realm,
.size == 1
}
fun queryRoomMembersEvent(): RealmQuery<EventEntity> {
return EventEntity
.where(realm, roomId, EventType.STATE_ROOM_MEMBER)
.sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING)
.isNotNull(EventEntityFields.STATE_KEY)
.distinct(EventEntityFields.STATE_KEY)
.isNotNull(EventEntityFields.CONTENT)
fun queryRoomMembersEvent(): RealmQuery<RoomMemberEntity> {
return RoomMemberEntity.where(realm, roomId)
}
fun queryJoinedRoomMembersEvent(): RealmQuery<EventEntity> {
return queryRoomMembersEvent().contains(EventEntityFields.CONTENT, "\"membership\":\"join\"")
}
fun queryInvitedRoomMembersEvent(): RealmQuery<EventEntity> {
return queryRoomMembersEvent().contains(EventEntityFields.CONTENT, "\"membership\":\"invite\"")
}
fun queryRoomMemberEvent(userId: String): RealmQuery<EventEntity> {
fun queryJoinedRoomMembersEvent(): RealmQuery<RoomMemberEntity> {
return queryRoomMembersEvent()
.equalTo(EventEntityFields.STATE_KEY, userId)
.equalTo(RoomMemberEntityFields.MEMBERSHIP_STR, Membership.JOIN.name)
}
fun queryInvitedRoomMembersEvent(): RealmQuery<RoomMemberEntity> {
return queryRoomMembersEvent()
.equalTo(RoomMemberEntityFields.MEMBERSHIP_STR, Membership.INVITE.name)
}
fun queryActiveRoomMembersEvent(): RealmQuery<RoomMemberEntity> {
return queryRoomMembersEvent()
.beginGroup()
.equalTo(RoomMemberEntityFields.MEMBERSHIP_STR, Membership.INVITE.name)
.or()
.equalTo(RoomMemberEntityFields.MEMBERSHIP_STR, Membership.JOIN.name)
.endGroup()
}
fun getNumberOfJoinedMembers(): Int {
return roomSummary?.joinedMembersCount
?: queryJoinedRoomMembersEvent().findAll().size
?: queryJoinedRoomMembersEvent().findAll().size
}
fun getNumberOfInvitedMembers(): Int {
return roomSummary?.invitedMembersCount
?: queryInvitedRoomMembersEvent().findAll().size
?: queryInvitedRoomMembersEvent().findAll().size
}
fun getNumberOfMembers(): Int {
@ -111,7 +109,7 @@ internal class RoomMembers(private val realm: Realm,
* @return a roomMember id list of joined or invited members.
*/
fun getActiveRoomMemberIds(): List<String> {
return getRoomMemberIdsFiltered { it.membership == Membership.JOIN || it.membership == Membership.INVITE }
return queryActiveRoomMembersEvent().findAll().map { it.userId }
}
/**
@ -120,21 +118,6 @@ internal class RoomMembers(private val realm: Realm,
* @return a roomMember id list of joined members.
*/
fun getJoinedRoomMemberIds(): List<String> {
return getRoomMemberIdsFiltered { it.membership == Membership.JOIN }
}
/* ==========================================================================================
* Private
* ========================================================================================== */
private fun getRoomMemberIdsFiltered(predicate: (RoomMember) -> Boolean): List<String> {
return RoomMembers(realm, roomId)
.queryRoomMembersEvent()
.findAll()
.map { it.asDomain() }
.associateBy { it.stateKey!! }
.filterValues { predicate(it.content.toModel<RoomMember>()!!) }
.keys
.toList()
return queryJoinedRoomMembersEvent().findAll().map { it.userId }
}
}

View File

@ -17,7 +17,7 @@
package im.vector.matrix.android.internal.session.room.membership.joining
import im.vector.matrix.android.api.session.room.failure.JoinRoomFailure
import im.vector.matrix.android.internal.database.RealmQueryLatch
import im.vector.matrix.android.internal.database.awaitNotEmptyResult
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomEntityFields
import im.vector.matrix.android.internal.di.SessionDatabase
@ -26,6 +26,7 @@ import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
import im.vector.matrix.android.internal.task.Task
import io.realm.RealmConfiguration
import kotlinx.coroutines.TimeoutCancellationException
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@ -46,18 +47,16 @@ internal class DefaultJoinRoomTask @Inject constructor(private val roomAPI: Room
executeRequest<Unit> {
apiCall = roomAPI.join(params.roomId, params.viaServers, mapOf("reason" to params.reason))
}
val roomId = params.roomId
// Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before)
val rql = RealmQueryLatch<RoomEntity>(realmConfiguration) { realm ->
realm.where(RoomEntity::class.java)
.equalTo(RoomEntityFields.ROOM_ID, roomId)
}
try {
rql.await(timeout = 1L, timeUnit = TimeUnit.MINUTES)
} catch (exception: Exception) {
awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->
realm.where(RoomEntity::class.java)
.equalTo(RoomEntityFields.ROOM_ID, params.roomId)
}
} catch (exception: TimeoutCancellationException) {
throw JoinRoomFailure.JoinedWithTimeout
}
setReadMarkers(roomId)
setReadMarkers(params.roomId)
}
private suspend fun setReadMarkers(roomId: String) {

View File

@ -23,11 +23,10 @@ import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.query.types
import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import io.realm.OrderedCollectionChangeSet
import io.realm.RealmConfiguration
import io.realm.RealmResults
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ -36,8 +35,7 @@ import javax.inject.Inject
* As it will actually delete the content, it should be called last in the list of listener.
*/
internal class EventsPruner @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration,
private val pruneEventTask: PruneEventTask,
private val taskExecutor: TaskExecutor) :
private val pruneEventTask: PruneEventTask) :
RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
override val query = Monarchy.Query<EventEntity> { EventEntity.types(it, listOf(EventType.REDACTION)) }
@ -50,7 +48,9 @@ internal class EventsPruner @Inject constructor(@SessionDatabase realmConfigurat
.mapNotNull { results[it]?.asDomain() }
.toList()
val params = PruneEventTask.Params(insertedDomains)
pruneEventTask.configureWith(params).executeBy(taskExecutor)
observerScope.launch {
val params = PruneEventTask.Params(insertedDomains)
pruneEventTask.execute(params)
}
}
}

View File

@ -20,7 +20,7 @@ import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.LocalEcho
import im.vector.matrix.android.api.session.events.model.UnsignedData
import im.vector.matrix.android.internal.database.helper.updateSenderData
import im.vector.matrix.android.internal.database.helper.TimelineEventSenderVisitor
import im.vector.matrix.android.internal.database.mapper.ContentMapper
import im.vector.matrix.android.internal.database.mapper.EventMapper
import im.vector.matrix.android.internal.database.model.EventEntity
@ -41,7 +41,8 @@ internal interface PruneEventTask : Task<PruneEventTask.Params, Unit> {
)
}
internal class DefaultPruneEventTask @Inject constructor(private val monarchy: Monarchy) : PruneEventTask {
internal class DefaultPruneEventTask @Inject constructor(private val monarchy: Monarchy,
private val timelineEventSenderVisitor: TimelineEventSenderVisitor) : PruneEventTask {
override suspend fun execute(params: PruneEventTask.Params) {
monarchy.awaitTransaction { realm ->
@ -65,12 +66,14 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M
val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst()
?: return
val allowedKeys = computeAllowedKeys(eventToPrune.type)
val typeToPrune = eventToPrune.type
val stateKey = eventToPrune.stateKey
val allowedKeys = computeAllowedKeys(typeToPrune)
if (allowedKeys.isNotEmpty()) {
val prunedContent = ContentMapper.map(eventToPrune.content)?.filterKeys { key -> allowedKeys.contains(key) }
eventToPrune.content = ContentMapper.map(prunedContent)
} else {
when (eventToPrune.type) {
when (typeToPrune) {
EventType.ENCRYPTED,
EventType.MESSAGE -> {
Timber.d("REDACTION for message ${eventToPrune.eventId}")
@ -94,21 +97,20 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M
// }
}
}
if (eventToPrune.type == EventType.STATE_ROOM_MEMBER) {
if (typeToPrune == EventType.STATE_ROOM_MEMBER && stateKey != null) {
timelineEventSenderVisitor.clear(roomId = eventToPrune.roomId, senderId = stateKey)
val timelineEventsToUpdate = TimelineEventEntity.findWithSenderMembershipEvent(realm, eventToPrune.eventId)
for (timelineEvent in timelineEventsToUpdate) {
timelineEvent.updateSenderData()
}
timelineEventSenderVisitor.visit(timelineEventsToUpdate)
}
}
private fun computeAllowedKeys(type: String): List<String> {
// Add filtered content, allowed keys in content depends on the event type
return when (type) {
EventType.STATE_ROOM_MEMBER -> listOf("membership")
EventType.STATE_ROOM_CREATE -> listOf("creator")
EventType.STATE_ROOM_JOIN_RULES -> listOf("join_rule")
EventType.STATE_ROOM_POWER_LEVELS -> listOf("users",
EventType.STATE_ROOM_MEMBER -> listOf("membership")
EventType.STATE_ROOM_CREATE -> listOf("creator")
EventType.STATE_ROOM_JOIN_RULES -> listOf("join_rule")
EventType.STATE_ROOM_POWER_LEVELS -> listOf("users",
"users_default",
"events",
"events_default",
@ -117,10 +119,10 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M
"kick",
"redact",
"invite")
EventType.STATE_ROOM_ALIASES -> listOf("aliases")
EventType.STATE_CANONICAL_ALIAS -> listOf("alias")
EventType.FEEDBACK -> listOf("type", "target_event_id")
else -> emptyList()
EventType.STATE_ROOM_ALIASES -> listOf("aliases")
EventType.STATE_ROOM_CANONICAL_ALIAS -> listOf("alias")
EventType.FEEDBACK -> listOf("type", "target_event_id")
else -> emptyList()
}
}
}

View File

@ -215,7 +215,16 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
return TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)
}
override fun getEventSummaryLive(eventId: String): LiveData<Optional<EventAnnotationsSummary>> {
override fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary? {
return monarchy.fetchCopyMap(
{ EventAnnotationsSummaryEntity.where(it, eventId).findFirst() },
{ entity, _ ->
entity.asDomain()
}
)
}
override fun getEventAnnotationsSummaryLive(eventId: String): LiveData<Optional<EventAnnotationsSummary>> {
val liveData = monarchy.findAllMappedWithChanges(
{ EventAnnotationsSummaryEntity.where(it, eventId) },
{ it.asDomain() }

View File

@ -16,10 +16,10 @@
package im.vector.matrix.android.internal.session.room.send.pills
import im.vector.matrix.android.api.session.room.send.UserMentionSpan
import im.vector.matrix.android.api.session.room.send.MatrixItemSpan
internal data class MentionLinkSpec(
val span: UserMentionSpan,
val span: MatrixItemSpan,
val start: Int,
val end: Int
)

View File

@ -16,15 +16,13 @@
package im.vector.matrix.android.internal.session.room.send.pills
import android.text.SpannableString
import im.vector.matrix.android.api.session.room.send.UserMentionSpan
import im.vector.matrix.android.api.session.room.send.MatrixItemSpan
import java.util.*
import javax.inject.Inject
/**
* Utility class to detect special span in CharSequence and turn them into
* formatted text to send them as a Matrix messages.
*
* For now only support UserMentionSpans (TODO rooms, room aliases, etc...)
*/
internal class TextPillsUtils @Inject constructor(
private val mentionLinkSpecComparator: MentionLinkSpecComparator
@ -49,7 +47,7 @@ internal class TextPillsUtils @Inject constructor(
private fun transformPills(text: CharSequence, template: String): String? {
val spannableString = SpannableString.valueOf(text)
val pills = spannableString
?.getSpans(0, text.length, UserMentionSpan::class.java)
?.getSpans(0, text.length, MatrixItemSpan::class.java)
?.map { MentionLinkSpec(it, spannableString.getSpanStart(it), spannableString.getSpanEnd(it)) }
?.toMutableList()
?.takeIf { it.isNotEmpty() }
@ -65,7 +63,7 @@ internal class TextPillsUtils @Inject constructor(
// append text before pill
append(text, currIndex, start)
// append the pill
append(String.format(template, urlSpan.matrixItem.id, urlSpan.matrixItem.displayName))
append(String.format(template, urlSpan.matrixItem.id, urlSpan.matrixItem.getBestName()))
currIndex = end
}
// append text after the last pill

View File

@ -21,7 +21,6 @@ import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.database.helper.deleteOnCascade
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.ChunkEntityFields
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.awaitTransaction
@ -38,7 +37,7 @@ internal class DefaultClearUnlinkedEventsTask @Inject constructor(private val mo
monarchy.awaitTransaction { localRealm ->
val unlinkedChunks = ChunkEntity
.where(localRealm, roomId = params.roomId)
.equalTo("${ChunkEntityFields.TIMELINE_EVENTS.ROOT}.${EventEntityFields.IS_UNLINKED}", true)
.equalTo(ChunkEntityFields.IS_UNLINKED, true)
.findAll()
unlinkedChunks.forEach {
it.deleteOnCascade()

View File

@ -27,13 +27,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
import im.vector.matrix.android.api.util.CancelableBag
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.ChunkEntityFields
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
import im.vector.matrix.android.internal.database.model.*
import im.vector.matrix.android.internal.database.query.FilterContent
import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates
import im.vector.matrix.android.internal.database.query.where
@ -44,16 +38,10 @@ import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.Debouncer
import im.vector.matrix.android.internal.util.createBackgroundHandler
import im.vector.matrix.android.internal.util.createUIHandler
import io.realm.OrderedCollectionChangeSet
import io.realm.OrderedRealmCollectionChangeListener
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.RealmQuery
import io.realm.RealmResults
import io.realm.Sort
import io.realm.*
import timber.log.Timber
import java.util.Collections
import java.util.UUID
import java.util.*
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
import kotlin.collections.ArrayList
@ -77,11 +65,11 @@ internal class DefaultTimeline(
private val hiddenReadReceipts: TimelineHiddenReadReceipts
) : Timeline, TimelineHiddenReadReceipts.Delegate {
private companion object {
companion object {
val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD")
}
private val listeners = ArrayList<Timeline.Listener>()
private val listeners = CopyOnWriteArrayList<Timeline.Listener>()
private val isStarted = AtomicBoolean(false)
private val isReady = AtomicBoolean(false)
private val mainHandler = createUIHandler()
@ -113,11 +101,7 @@ internal class DefaultTimeline(
if (!results.isLoaded || !results.isValid) {
return@OrderedRealmCollectionChangeListener
}
if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL) {
handleInitialLoad()
} else {
handleUpdates(changeSet)
}
handleUpdates(changeSet)
}
private val relationsListener = OrderedRealmCollectionChangeListener<RealmResults<EventAnnotationsSummaryEntity>> { collection, changeSet ->
@ -179,8 +163,9 @@ internal class DefaultTimeline(
nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING).findAll()
filteredEvents = nonFilteredEvents.where()
.filterEventsWithSettings()
.findAllAsync()
.also { it.addChangeListener(eventsChangeListener) }
.findAll()
handleInitialLoad()
filteredEvents.addChangeListener(eventsChangeListener)
eventRelations = EventAnnotationsSummaryEntity.whereInRoom(realm, roomId)
.findAllAsync()
@ -288,20 +273,20 @@ internal class DefaultTimeline(
return hasMoreInCache(direction) || !hasReachedEnd(direction)
}
override fun addListener(listener: Timeline.Listener) = synchronized(listeners) {
override fun addListener(listener: Timeline.Listener): Boolean {
if (listeners.contains(listener)) {
return false
}
listeners.add(listener).also {
return listeners.add(listener).also {
postSnapshot()
}
}
override fun removeListener(listener: Timeline.Listener) = synchronized(listeners) {
listeners.remove(listener)
override fun removeListener(listener: Timeline.Listener): Boolean {
return listeners.remove(listener)
}
override fun removeAllListeners() = synchronized(listeners) {
override fun removeAllListeners() {
listeners.clear()
}
@ -402,14 +387,14 @@ internal class DefaultTimeline(
private fun getState(direction: Timeline.Direction): State {
return when (direction) {
Timeline.Direction.FORWARDS -> forwardsState.get()
Timeline.Direction.FORWARDS -> forwardsState.get()
Timeline.Direction.BACKWARDS -> backwardsState.get()
}
}
private fun updateState(direction: Timeline.Direction, update: (State) -> State) {
val stateReference = when (direction) {
Timeline.Direction.FORWARDS -> forwardsState
Timeline.Direction.FORWARDS -> forwardsState
Timeline.Direction.BACKWARDS -> backwardsState
}
val currentValue = stateReference.get()
@ -504,15 +489,14 @@ internal class DefaultTimeline(
Timber.v("Should fetch $limit items $direction")
cancelableBag += paginationTask
.configureWith(params) {
this.retryCount = Int.MAX_VALUE
this.constraints = TaskConstraints(connectedToNetwork = true)
this.callback = object : MatrixCallback<TokenChunkEventPersistor.Result> {
override fun onSuccess(data: TokenChunkEventPersistor.Result) {
when (data) {
TokenChunkEventPersistor.Result.SUCCESS -> {
TokenChunkEventPersistor.Result.SUCCESS -> {
Timber.v("Success fetching $limit items $direction from pagination request")
}
TokenChunkEventPersistor.Result.REACHED_END -> {
TokenChunkEventPersistor.Result.REACHED_END -> {
postSnapshot()
}
TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE ->
@ -524,6 +508,8 @@ internal class DefaultTimeline(
}
override fun onFailure(failure: Throwable) {
updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
postSnapshot()
Timber.v("Failure fetching $limit items $direction from pagination request")
}
}
@ -637,7 +623,14 @@ internal class DefaultTimeline(
private fun fetchEvent(eventId: String) {
val params = GetContextOfEventTask.Params(roomId, eventId, settings.initialSize)
cancelableBag += contextOfEventTask.configureWith(params).executeBy(taskExecutor)
cancelableBag += contextOfEventTask.configureWith(params) {
callback = object : MatrixCallback<TokenChunkEventPersistor.Result> {
override fun onFailure(failure: Throwable) {
postFailure(failure)
}
}
}
.executeBy(taskExecutor)
}
private fun postSnapshot() {
@ -648,16 +641,26 @@ internal class DefaultTimeline(
updateLoadingStates(filteredEvents)
val snapshot = createSnapshot()
val runnable = Runnable {
synchronized(listeners) {
listeners.forEach {
it.onUpdated(snapshot)
}
listeners.forEach {
it.onTimelineUpdated(snapshot)
}
}
debouncer.debounce("post_snapshot", runnable, 50)
}
}
private fun postFailure(throwable: Throwable) {
if (isReady.get().not()) {
return
}
val runnable = Runnable {
listeners.forEach {
it.onTimelineFailure(throwable)
}
}
mainHandler.post(runnable)
}
private fun clearAllValues() {
prevDisplayIndex = null
nextDisplayIndex = null

View File

@ -24,7 +24,6 @@ import im.vector.matrix.android.internal.database.query.create
import im.vector.matrix.android.internal.database.query.find
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.user.UserEntityFactory
import im.vector.matrix.android.internal.util.awaitTransaction
import io.realm.kotlin.createObject
import timber.log.Timber
@ -33,7 +32,8 @@ import javax.inject.Inject
/**
* Insert Chunk in DB, and eventually merge with existing chunk event
*/
internal class TokenChunkEventPersistor @Inject constructor(private val monarchy: Monarchy) {
internal class TokenChunkEventPersistor @Inject constructor(private val monarchy: Monarchy,
private val timelineEventSenderVisitor: TimelineEventSenderVisitor) {
/**
* <pre>
@ -112,7 +112,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction")
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId)
?: realm.createObject(roomId)
val nextToken: String?
val prevToken: String?
@ -125,34 +125,29 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
}
val shouldSkip = ChunkEntity.find(realm, roomId, nextToken = nextToken) != null
|| ChunkEntity.find(realm, roomId, prevToken = prevToken) != null
|| ChunkEntity.find(realm, roomId, prevToken = prevToken) != null
val prevChunk = ChunkEntity.find(realm, roomId, nextToken = prevToken)
val nextChunk = ChunkEntity.find(realm, roomId, prevToken = nextToken)
// The current chunk is the one we will keep all along the merge processChanges.
// We try to look for a chunk next to the token,
// otherwise we create a whole new one
// otherwise we create a whole new one which is unlinked (not live)
var currentChunk = if (direction == PaginationDirection.FORWARDS) {
prevChunk?.apply { this.nextToken = nextToken }
} else {
nextChunk?.apply { this.prevToken = prevToken }
}
?: ChunkEntity.create(realm, prevToken, nextToken)
?: ChunkEntity.create(realm, prevToken, nextToken, isUnlinked = true)
if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) {
Timber.v("Reach end of $roomId")
currentChunk.isLastBackward = true
} else if (!shouldSkip) {
Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}")
val eventIds = ArrayList<String>(receivedChunk.events.size)
for (event in receivedChunk.events) {
event.eventId?.also { eventIds.add(it) }
currentChunk.add(roomId, event, direction, isUnlinked = currentChunk.isUnlinked())
UserEntityFactory.createOrNull(event)?.also {
realm.insertOrUpdate(it)
}
val timelineEvents = receivedChunk.events.mapNotNull {
currentChunk.add(roomId, it, direction)
}
// Then we merge chunks if needed
if (currentChunk != prevChunk && prevChunk != null) {
@ -170,12 +165,9 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
}
roomEntity.addOrUpdate(currentChunk)
for (stateEvent in receivedChunk.stateEvents) {
roomEntity.addStateEvent(stateEvent, isUnlinked = currentChunk.isUnlinked())
UserEntityFactory.createOrNull(stateEvent)?.also {
realm.insertOrUpdate(it)
}
roomEntity.addStateEvent(stateEvent, isUnlinked = currentChunk.isUnlinked)
}
currentChunk.updateSenderDataFor(eventIds)
timelineEventSenderVisitor.visit(timelineEvents)
}
}
return if (receivedChunk.events.isEmpty()) {
@ -196,11 +188,13 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
// We always merge the bottom chunk into top chunk, so we are always merging backwards
Timber.v("Merge ${currentChunk.prevToken} | ${currentChunk.nextToken} with ${otherChunk.prevToken} | ${otherChunk.nextToken}")
return if (direction == PaginationDirection.BACKWARDS && !otherChunk.isLastForward) {
currentChunk.merge(roomEntity.roomId, otherChunk, PaginationDirection.BACKWARDS)
val events = currentChunk.merge(roomEntity.roomId, otherChunk, PaginationDirection.BACKWARDS)
timelineEventSenderVisitor.visit(events)
roomEntity.deleteOnCascade(otherChunk)
currentChunk
} else {
otherChunk.merge(roomEntity.roomId, currentChunk, PaginationDirection.BACKWARDS)
val events = otherChunk.merge(roomEntity.roomId, currentChunk, PaginationDirection.BACKWARDS)
timelineEventSenderVisitor.visit(events)
roomEntity.deleteOnCascade(currentChunk)
otherChunk
}

View File

@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.VersioningState
import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
import im.vector.matrix.android.internal.database.awaitTransaction
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
@ -30,9 +31,9 @@ import im.vector.matrix.android.internal.database.query.types
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.SessionDatabase
import io.realm.OrderedCollectionChangeSet
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.RealmResults
import kotlinx.coroutines.launch
import javax.inject.Inject
internal class RoomTombstoneEventLiveObserver @Inject constructor(@SessionDatabase
@ -51,24 +52,24 @@ internal class RoomTombstoneEventLiveObserver @Inject constructor(@SessionDataba
}
.toList()
.also {
handleRoomTombstoneEvents(it)
observerScope.launch {
handleRoomTombstoneEvents(it)
}
}
}
private fun handleRoomTombstoneEvents(tombstoneEvents: List<Event>) = Realm.getInstance(realmConfiguration).use {
it.executeTransactionAsync { realm ->
for (event in tombstoneEvents) {
if (event.roomId == null) continue
val createRoomContent = event.getClearContent().toModel<RoomTombstoneContent>()
if (createRoomContent?.replacementRoom == null) continue
private suspend fun handleRoomTombstoneEvents(tombstoneEvents: List<Event>) = awaitTransaction(realmConfiguration) { realm ->
for (event in tombstoneEvents) {
if (event.roomId == null) continue
val createRoomContent = event.getClearContent().toModel<RoomTombstoneContent>()
if (createRoomContent?.replacementRoom == null) continue
val predecessorRoomSummary = RoomSummaryEntity.where(realm, event.roomId).findFirst()
?: RoomSummaryEntity(event.roomId)
if (predecessorRoomSummary.versioningState == VersioningState.NONE) {
predecessorRoomSummary.versioningState = VersioningState.UPGRADED_ROOM_NOT_JOINED
}
realm.insertOrUpdate(predecessorRoomSummary)
val predecessorRoomSummary = RoomSummaryEntity.where(realm, event.roomId).findFirst()
?: RoomSummaryEntity(event.roomId)
if (predecessorRoomSummary.versioningState == VersioningState.NONE) {
predecessorRoomSummary.versioningState = VersioningState.UPGRADED_ROOM_NOT_JOINED
}
realm.insertOrUpdate(predecessorRoomSummary)
}
}
}

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