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 🙌: 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: 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 🐛: 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 🗣: Translations 🗣:
- -

View File

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

View File

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

View File

@ -10,7 +10,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { 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 "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
implementation "androidx.appcompat:appcompat:1.1.0" implementation "androidx.appcompat:appcompat:1.1.0"
implementation "androidx.recyclerview:recyclerview:1.1.0-beta05"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version" kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
@ -119,14 +118,14 @@ dependencies {
implementation "ru.noties.markwon:core:$markwon_version" implementation "ru.noties.markwon:core:$markwon_version"
// Image // Image
implementation 'androidx.exifinterface:exifinterface:1.0.0' implementation 'androidx.exifinterface:exifinterface:1.1.0'
// Database // Database
implementation 'com.github.Zhuinden:realm-monarchy:0.5.1' implementation 'com.github.Zhuinden:realm-monarchy:0.5.1'
kapt 'dk.ilios:realmfieldnameshelper:1.1.1' kapt 'dk.ilios:realmfieldnameshelper:1.1.1'
// Work // Work
implementation "androidx.work:work-runtime-ktx:2.3.0-alpha01" implementation "androidx.work:work-runtime-ktx:2.3.0-beta02"
// FP // FP
implementation "io.arrow-kt:arrow-core:$arrow_version" 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 im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.Dispatchers.Main 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 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.model.OlmSessionWrapper
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import io.realm.Realm
import org.junit.Assert.* import org.junit.Assert.*
import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith
import org.matrix.olm.OlmAccount import org.matrix.olm.OlmAccount
import org.matrix.olm.OlmManager import org.matrix.olm.OlmManager
import org.matrix.olm.OlmSession import org.matrix.olm.OlmSession
private const val DUMMY_DEVICE_KEY = "DeviceKey" private const val DUMMY_DEVICE_KEY = "DeviceKey"
class CryptoStoreTest { @RunWith(AndroidJUnit4::class)
class CryptoStoreTest : InstrumentedTest {
private val cryptoStoreHelper = CryptoStoreHelper() private val cryptoStoreHelper = CryptoStoreHelper()
@Before
fun setup() {
Realm.init(context())
}
@Test @Test
fun test_metadata_realm_ok() { fun test_metadata_realm_ok() {
val cryptoStore: IMXCryptoStore = cryptoStoreHelper.createStore() 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 androidx.test.ext.junit.runners.AndroidJUnit4
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.InstrumentedTest 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.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.internal.session.room.timeline.PaginationDirection
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeListOfEvents import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeListOfEvents
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeMessageEvent 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.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import io.realm.kotlin.createObject import io.realm.kotlin.createObject
import org.amshove.kluent.shouldBeFalse
import org.amshove.kluent.shouldBeTrue import org.amshove.kluent.shouldBeTrue
import org.amshove.kluent.shouldEqual import org.amshove.kluent.shouldEqual
import org.junit.Before import org.junit.Before
@ -43,7 +46,11 @@ internal class ChunkEntityTest : InstrumentedTest {
@Before @Before
fun setup() { fun setup() {
Realm.init(context()) 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() 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 @Test
fun merge_shouldPrevTokenMerged_whenMergingForwards() { fun merge_shouldPrevTokenMerged_whenMergingForwards() {
monarchy.runTransactionSync { realm -> monarchy.runTransactionSync { realm ->
@ -172,8 +155,8 @@ internal class ChunkEntityTest : InstrumentedTest {
val chunk2: ChunkEntity = realm.createObject() val chunk2: ChunkEntity = realm.createObject()
val prevToken = "prev_token" val prevToken = "prev_token"
chunk1.prevToken = prevToken chunk1.prevToken = prevToken
chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk1.merge("roomId", chunk2, PaginationDirection.FORWARDS) chunk1.merge("roomId", chunk2, PaginationDirection.FORWARDS)
chunk1.prevToken shouldEqual prevToken chunk1.prevToken shouldEqual prevToken
} }
@ -186,10 +169,19 @@ internal class ChunkEntityTest : InstrumentedTest {
val chunk2: ChunkEntity = realm.createObject() val chunk2: ChunkEntity = realm.createObject()
val nextToken = "next_token" val nextToken = "next_token"
chunk1.nextToken = nextToken chunk1.nextToken = nextToken
chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS) chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS)
chunk1.nextToken shouldEqual nextToken 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 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.Content
import im.vector.matrix.android.api.session.events.model.Event 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.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.RoomMember
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent 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.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 import kotlin.random.Random
object RoomDataHelper { object RoomDataHelper {
@ -73,19 +66,4 @@ object RoomDataHelper {
val roomMember = RoomMember(Membership.JOIN, "Fake name #${Random.nextLong()}").toContent() val roomMember = RoomMember(Membership.JOIN, "Fake name #${Random.nextLong()}").toContent()
return createFakeEvent(EventType.STATE_ROOM_MEMBER, roomMember) 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) // val latch = CountDownLatch(2)
// var timelineEvents: List<TimelineEvent> = emptyList() // var timelineEvents: List<TimelineEvent> = emptyList()
// timeline.listener = object : Timeline.Listener { // timeline.listener = object : Timeline.Listener {
// override fun onUpdated(snapshot: List<TimelineEvent>) { // override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
// if (snapshot.isNotEmpty()) { // if (snapshot.isNotEmpty()) {
// if (initialLoad == 0) { // if (initialLoad == 0) {
// initialLoad = snapshot.size // initialLoad = snapshot.size

View File

@ -28,6 +28,12 @@ fun MXDeviceInfo.getFingerprintHumanReadable() = fingerprint()
?.chunked(4) ?.chunked(4)
?.joinToString(separator = " ") ?.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. * 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 import javax.net.ssl.HttpsURLConnection
fun Throwable.is401(): Boolean { fun Throwable.is401() =
return (this is Failure.ServerError && httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */ this is Failure.ServerError
&& error.code == MatrixError.M_UNAUTHORIZED) && 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 package im.vector.matrix.android.api.permalinks
import android.text.Spannable import android.text.Spannable
import im.vector.matrix.android.api.MatrixPatterns
/** /**
* MatrixLinkify take a piece of text and turns all of the * 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. * @param spannable the text in which the matrix items has to be clickable.
*/ */
@Suppress("UNUSED_PARAMETER")
fun addLinks(spannable: Spannable, callback: MatrixPermalinkSpan.Callback?): Boolean { 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 // sanity checks
if (spannable.isEmpty()) { if (spannable.isEmpty()) {
return false return false
@ -50,5 +55,7 @@ object MatrixLinkify {
} }
} }
return hasMatch return hasMatch
*/
return false
} }
} }

View File

@ -56,23 +56,23 @@ object PermalinkParser {
val identifier = params.getOrNull(0) val identifier = params.getOrNull(0)
val extraParameter = params.getOrNull(1) val extraParameter = params.getOrNull(1)
if (identifier.isNullOrEmpty()) {
return PermalinkData.FallbackLink(uri)
}
return when { return when {
identifier.isNullOrEmpty() -> PermalinkData.FallbackLink(uri)
MatrixPatterns.isUserId(identifier) -> PermalinkData.UserLink(userId = identifier) MatrixPatterns.isUserId(identifier) -> PermalinkData.UserLink(userId = identifier)
MatrixPatterns.isGroupId(identifier) -> PermalinkData.GroupLink(groupId = identifier) MatrixPatterns.isGroupId(identifier) -> PermalinkData.GroupLink(groupId = identifier)
MatrixPatterns.isRoomId(identifier) -> { MatrixPatterns.isRoomId(identifier) -> {
val eventId = extraParameter.takeIf { PermalinkData.RoomLink(
!it.isNullOrEmpty() && MatrixPatterns.isEventId(it) roomIdOrAlias = identifier,
} isRoomAlias = false,
PermalinkData.RoomLink(roomIdOrAlias = identifier, isRoomAlias = false, eventId = eventId) eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) }
)
} }
MatrixPatterns.isRoomAlias(identifier) -> { MatrixPatterns.isRoomAlias(identifier) -> {
val eventId = extraParameter.takeIf { PermalinkData.RoomLink(
!it.isNullOrEmpty() && MatrixPatterns.isEventId(it) roomIdOrAlias = identifier,
} isRoomAlias = true,
PermalinkData.RoomLink(roomIdOrAlias = identifier, isRoomAlias = true, eventId = eventId) eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) }
)
} }
else -> PermalinkData.FallbackLink(uri) 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. * This method allows to listen the sync state.
* @return a [LiveData] of [SyncState]. * @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. * 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 { 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>) 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.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult 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.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.DevicesListResponse
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
@ -89,6 +90,8 @@ interface CryptoService {
fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) fun getDevicesList(callback: MatrixCallback<DevicesListResponse>)
fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>)
fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int
fun isRoomEncrypted(roomId: String): Boolean 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_POWER_LEVELS = "m.room.power_levels"
const val STATE_ROOM_ALIASES = "m.room.aliases" const val STATE_ROOM_ALIASES = "m.room.aliases"
const val STATE_ROOM_TOMBSTONE = "m.room.tombstone" const val STATE_ROOM_TOMBSTONE = "m.room.tombstone"
const val STATE_CANONICAL_ALIAS = "m.room.canonical_alias" const val STATE_ROOM_CANONICAL_ALIAS = "m.room.canonical_alias"
const val STATE_HISTORY_VISIBILITY = "m.room.history_visibility" const val STATE_ROOM_HISTORY_VISIBILITY = "m.room.history_visibility"
const val STATE_RELATED_GROUPS = "m.room.related_groups" const val STATE_ROOM_RELATED_GROUPS = "m.room.related_groups"
const val STATE_PINNED_EVENT = "m.room.pinned_events" const val STATE_ROOM_PINNED_EVENT = "m.room.pinned_events"
// Call Events // Call Events
@ -86,10 +86,12 @@ object EventType {
STATE_ROOM_JOIN_RULES, STATE_ROOM_JOIN_RULES,
STATE_ROOM_GUEST_ACCESS, STATE_ROOM_GUEST_ACCESS,
STATE_ROOM_POWER_LEVELS, STATE_ROOM_POWER_LEVELS,
STATE_ROOM_ALIASES,
STATE_ROOM_TOMBSTONE, STATE_ROOM_TOMBSTONE,
STATE_HISTORY_VISIBILITY, STATE_ROOM_CANONICAL_ALIAS,
STATE_RELATED_GROUPS, STATE_ROOM_HISTORY_VISIBILITY,
STATE_PINNED_EVENT STATE_ROOM_RELATED_GROUPS,
STATE_ROOM_PINNED_EVENT
) )
fun isStateEvent(type: String): Boolean { fun isStateEvent(type: String): Boolean {

View File

@ -31,9 +31,22 @@ interface GroupService {
*/ */
fun getGroup(groupId: String): Group? 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. * Get a live list of group summaries. This list is refreshed as soon as the data changes.
* @return the [LiveData] of [GroupSummary] * @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" const val EVENT_ID_ONLY = "event_id_only"
} }
fun livePushers(): LiveData<List<Pusher>> fun getPushersLive(): LiveData<List<Pusher>>
fun pushers() : List<Pusher> fun pushers() : List<Pusher>
} }

View File

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

View File

@ -53,16 +53,35 @@ interface RoomService {
fun getRoom(roomId: String): Room? fun getRoom(roomId: String): Room?
/** /**
* Get a live list of room summaries. This list is refreshed as soon as the data changes. * Get a roomSummary from a roomId or a room alias
* @return the [LiveData] of [RoomSummary] * @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 * Get a live list of Breadcrumbs
* @return the [LiveData] of [RoomSummary] * @return the [LiveData] of [RoomSummary]
*/ */
fun liveBreadcrumbs(): LiveData<List<RoomSummary>> fun getBreadcrumbsLive(): LiveData<List<RoomSummary>>
/** /**
* Inform the Matrix SDK that a room is displayed. * 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? 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. * @return a [LiveData] of roomMember list.
*/ */
fun getRoomMemberIdsLive(): LiveData<List<String>> fun getRoomMembersLive(queryParams: RoomMemberQueryParams): LiveData<List<RoomMember>>
fun getNumberOfJoinedMembers(): Int 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 { fun isLeft(): Boolean {
return this == KNOCK || this == LEAVE || this == BAN 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 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) @JsonClass(generateAdapter = true)
data class RoomCanonicalAliasContent( data class RoomCanonicalAliasContent(

View File

@ -16,23 +16,12 @@
package im.vector.matrix.android.api.session.room.model 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( data class RoomMember(
@Json(name = "membership") val membership: Membership, val membership: Membership,
@Json(name = "reason") val reason: String? = null, val userId: String,
@Json(name = "displayname") val displayName: String? = null, val displayName: String? = null,
@Json(name = "avatar_url") val avatarUrl: String? = null, 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

@ -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 membership: Membership = Membership.NONE,
val versioningState: VersioningState = VersioningState.NONE, val versioningState: VersioningState = VersioningState.NONE,
val readMarkerId: String? = null, val readMarkerId: String? = null,
val userDrafts: List<UserDraft> = emptyList() val userDrafts: List<UserDraft> = emptyList(),
var isEncrypted: Boolean
) { ) {
val isVersioned: Boolean val isVersioned: Boolean

View File

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

View File

@ -98,7 +98,7 @@ interface RelationService {
/** /**
* Reply to an event in the timeline (must be in same room) * Reply to an event in the timeline (must be in same room)
* https://matrix.org/docs/spec/client_server/r0.4.0.html#id350 * 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. * by the sdk into pills.
* @param eventReplied the event referenced by the reply * @param eventReplied the event referenced by the reply
* @param replyText the reply text * @param replyText the reply text
@ -108,5 +108,17 @@ interface RelationService {
replyText: CharSequence, replyText: CharSequence,
autoMarkdown: Boolean = false): Cancelable? 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 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 * These Spans will be transformed into pills when detected in message to send
*/ */
interface UserMentionSpan { interface MatrixItemSpan {
val matrixItem: MatrixItem val matrixItem: MatrixItem
} }

View File

@ -29,7 +29,7 @@ interface SendService {
/** /**
* Method to send a text message asynchronously. * 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. * by the sdk into pills.
* @param text the text message to send * @param text the text message to send
* @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE * @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. * 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. * 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) fun paginate(direction: Direction, count: Int)
@ -106,7 +106,12 @@ interface Timeline {
* Call when the timeline has been updated through pagination or sync. * Call when the timeline has been updated through pagination or sync.
* @param snapshot the most up to date snapshot * @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. * @param userId the userId to look for.
* @return a LiveData of user with userId * @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 * Observe a live list of users sorted alphabetically
* @return a Livedata of users * @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. * Observe a live [PagedList] of users sorted alphabetically. You can filter the users.
* @param filter the filter. It will look into userId and displayName. * @param filter the filter. It will look into userId and displayName.
* @return a Livedata of users * @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 * Get list of ignored users
*/ */
fun liveIgnoredUsers(): LiveData<List<User>> fun getIgnoredUsersLive(): LiveData<List<User>>
/** /**
* Ignore users * 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.BuildConfig
import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.RoomSummary 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.room.model.roomdirectory.PublicRoom
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
@ -62,6 +63,9 @@ sealed class MatrixItem(
init { init {
if (BuildConfig.DEBUG) checkId() 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, data class GroupItem(override val id: String,
@ -71,9 +75,12 @@ sealed class MatrixItem(
init { init {
if (BuildConfig.DEBUG) checkId() 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 return displayName?.takeIf { it.isNotBlank() } ?: id
} }
@ -95,7 +102,7 @@ sealed class MatrixItem(
} }
fun firstLetterOfDisplayName(): String { fun firstLetterOfDisplayName(): String {
return getBestName() return (displayName?.takeIf { it.isNotBlank() } ?: id)
.let { dn -> .let { dn ->
var startIndex = 0 var startIndex = 0
val initial = dn[startIndex] val initial = dn[startIndex]
@ -138,4 +145,6 @@ sealed class MatrixItem(
fun User.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) fun User.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)
fun GroupSummary.toMatrixItem() = MatrixItem.GroupItem(groupId, displayName, avatarUrl) fun GroupSummary.toMatrixItem() = MatrixItem.GroupItem(groupId, displayName, avatarUrl)
fun RoomSummary.toMatrixItem() = MatrixItem.RoomItem(roomId, 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 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 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.DynamicRealm
import io.realm.RealmMigration import io.realm.RealmMigration
import timber.log.Timber import timber.log.Timber
@ -23,12 +26,17 @@ import timber.log.Timber
internal object AuthRealmMigration : RealmMigration { internal object AuthRealmMigration : RealmMigration {
// Current schema version // Current schema version
const val SCHEMA_VERSION = 2L const val SCHEMA_VERSION = 3L
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.d("Migrating Auth Realm from $oldVersion to $newVersion") Timber.d("Migrating Auth Realm from $oldVersion to $newVersion")
if (oldVersion <= 0) { if (oldVersion <= 0) migrateTo1(realm)
if (oldVersion <= 1) migrateTo2(realm)
if (oldVersion <= 2) migrateTo3(realm)
}
private fun migrateTo1(realm: DynamicRealm) {
Timber.d("Step 0 -> 1") Timber.d("Step 0 -> 1")
Timber.d("Create PendingSessionEntity") Timber.d("Create PendingSessionEntity")
@ -45,7 +53,7 @@ internal object AuthRealmMigration : RealmMigration {
.addField(PendingSessionEntityFields.CURRENT_THREE_PID_DATA_JSON, String::class.java) .addField(PendingSessionEntityFields.CURRENT_THREE_PID_DATA_JSON, String::class.java)
} }
if (oldVersion <= 1) { private fun migrateTo2(realm: DynamicRealm) {
Timber.d("Step 1 -> 2") Timber.d("Step 1 -> 2")
Timber.d("Add boolean isTokenValid in SessionParamsEntity, with value true") Timber.d("Add boolean isTokenValid in SessionParamsEntity, with value true")
@ -53,5 +61,25 @@ internal object AuthRealmMigration : RealmMigration {
?.addField(SessionParamsEntityFields.IS_TOKEN_VALID, Boolean::class.java) ?.addField(SessionParamsEntityFields.IS_TOKEN_VALID, Boolean::class.java)
?.transform { it.set(SessionParamsEntityFields.IS_TOKEN_VALID, true) } ?.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 import io.realm.annotations.PrimaryKey
internal open class SessionParamsEntity( internal open class SessionParamsEntity(
@PrimaryKey var userId: String = "", @PrimaryKey var sessionId: String = "",
var userId: String = "",
var credentialsJson: String = "", var credentialsJson: String = "",
var homeServerConnectionConfigJson: String = "", var homeServerConnectionConfigJson: String = "",
// Set to false when the token is invalid and the user has been soft logged out // 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.Credentials
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.internal.auth.createSessionId
import javax.inject.Inject import javax.inject.Inject
internal class SessionParamsMapper @Inject constructor(moshi: Moshi) { internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
@ -49,6 +50,7 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
return null return null
} }
return SessionParamsEntity( return SessionParamsEntity(
createSessionId(sessionParams.credentials.userId, sessionParams.credentials.deviceId),
sessionParams.credentials.userId, sessionParams.credentials.userId,
credentialsJson, credentialsJson,
homeServerConnectionConfigJson, homeServerConnectionConfigJson,

View File

@ -47,7 +47,7 @@ internal abstract class CryptoModule {
@Module @Module
companion object { companion object {
internal const val DB_ALIAS_PREFIX = "crypto_module_" internal fun getKeyAlias(userMd5: String) = "crypto_module_$userMd5"
@JvmStatic @JvmStatic
@Provides @Provides
@ -59,7 +59,7 @@ internal abstract class CryptoModule {
return RealmConfiguration.Builder() return RealmConfiguration.Builder()
.directory(directory) .directory(directory)
.apply { .apply {
realmKeysUtils.configureEncryption(this, "$DB_ALIAS_PREFIX$userMd5") realmKeysUtils.configureEncryption(this, getKeyAlias(userMd5))
} }
.name("crypto_store.realm") .name("crypto_store.realm")
.modules(RealmCryptoStoreModule()) .modules(RealmCryptoStoreModule())
@ -123,6 +123,9 @@ internal abstract class CryptoModule {
@Binds @Binds
abstract fun bindGetDevicesTask(getDevicesTask: DefaultGetDevicesTask): GetDevicesTask abstract fun bindGetDevicesTask(getDevicesTask: DefaultGetDevicesTask): GetDevicesTask
@Binds
abstract fun bindGetDeviceInfoTask(task: DefaultGetDeviceInfoTask): GetDeviceInfoTask
@Binds @Binds
abstract fun bindSetDeviceNameTask(setDeviceNameTask: DefaultSetDeviceNameTask): SetDeviceNameTask 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.MXEncryptEventContentResult
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent 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.DevicesListResponse
import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
@ -127,6 +128,7 @@ internal class DefaultCryptoService @Inject constructor(
private val deleteDeviceWithUserPasswordTask: DeleteDeviceWithUserPasswordTask, private val deleteDeviceWithUserPasswordTask: DeleteDeviceWithUserPasswordTask,
// Tasks // Tasks
private val getDevicesTask: GetDevicesTask, private val getDevicesTask: GetDevicesTask,
private val getDeviceInfoTask: GetDeviceInfoTask,
private val setDeviceNameTask: SetDeviceNameTask, private val setDeviceNameTask: SetDeviceNameTask,
private val uploadKeysTask: UploadKeysTask, private val uploadKeysTask: UploadKeysTask,
private val loadRoomMembersTask: LoadRoomMembersTask, private val loadRoomMembersTask: LoadRoomMembersTask,
@ -147,7 +149,7 @@ internal class DefaultCryptoService @Inject constructor(
when { when {
event.getClearType() == EventType.ENCRYPTION -> onRoomEncryptionEvent(roomId, event) event.getClearType() == EventType.ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
event.getClearType() == EventType.STATE_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) event.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
} }
} }
@ -155,7 +157,7 @@ internal class DefaultCryptoService @Inject constructor(
when { when {
event.getClearType() == EventType.ENCRYPTION -> onRoomEncryptionEvent(roomId, event) event.getClearType() == EventType.ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
event.getClearType() == EventType.STATE_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) event.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
} }
} }
@ -199,6 +201,14 @@ internal class DefaultCryptoService @Inject constructor(
.executeBy(taskExecutor) .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 { override fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int {
return cryptoStore.inboundGroupSessionsCount(onlyBackedUp) return cryptoStore.inboundGroupSessionsCount(onlyBackedUp)
} }

View File

@ -25,11 +25,18 @@ internal interface CryptoApi {
/** /**
* Get the devices list * 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") @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices")
fun getDevices(): Call<DevicesListResponse> 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. * 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 * 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 com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.util.createBackgroundHandler import im.vector.matrix.android.internal.util.createBackgroundHandler
import io.realm.* 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.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
internal interface LiveEntityObserver { internal interface LiveEntityObserver {
fun start() fun start()
fun dispose() fun dispose()
fun cancelProcess()
fun isStarted(): Boolean fun isStarted(): Boolean
} }
@ -35,6 +39,7 @@ internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val r
val BACKGROUND_HANDLER = createBackgroundHandler("LIVE_ENTITY_BACKGROUND") val BACKGROUND_HANDLER = createBackgroundHandler("LIVE_ENTITY_BACKGROUND")
} }
protected val observerScope = CoroutineScope(SupervisorJob())
protected abstract val query: Monarchy.Query<T> protected abstract val query: Monarchy.Query<T>
private val isStarted = AtomicBoolean(false) private val isStarted = AtomicBoolean(false)
private val backgroundRealm = AtomicReference<Realm>() private val backgroundRealm = AtomicReference<Realm>()
@ -59,10 +64,15 @@ internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val r
backgroundRealm.getAndSet(null).also { backgroundRealm.getAndSet(null).also {
it.close() it.close()
} }
observerScope.coroutineContext.cancelChildren()
} }
} }
} }
override fun cancelProcess() {
observerScope.coroutineContext.cancelChildren()
}
override fun isStarted(): Boolean { override fun isStarted(): Boolean {
return isStarted.get() return isStarted.get()
} }

View File

@ -16,43 +16,36 @@
package im.vector.matrix.android.internal.database package im.vector.matrix.android.internal.database
import im.vector.matrix.android.internal.util.createBackgroundHandler
import io.realm.* import io.realm.*
import java.util.concurrent.CountDownLatch import kotlinx.coroutines.*
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicReference
class RealmQueryLatch<E : RealmObject>(private val realmConfiguration: RealmConfiguration, internal suspend fun <T> awaitNotEmptyResult(realmConfiguration: RealmConfiguration,
private val realmQueryBuilder: (Realm) -> RealmQuery<E>) { 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 { Realm.getInstance(realmConfiguration).use { realm ->
val QUERY_LATCH_HANDLER = createBackgroundHandler("REALM_QUERY_LATCH") val result = builder(realm).findAllAsync()
}
@Throws(InterruptedException::class) val listener = object : RealmChangeListener<RealmResults<T>> {
fun await(timeout: Long, timeUnit: TimeUnit) { override fun onChange(it: RealmResults<T>) {
val realmRef = AtomicReference<Realm>() if (it.isNotEmpty()) {
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) result.removeChangeListener(this)
latch.countDown() latch.complete(Unit)
} }
} }
})
} }
result.addChangeListener(listener)
try { try {
latch.await(timeout, timeUnit) latch.await()
} catch (exception: InterruptedException) { } catch (e: CancellationException) {
throw exception result.removeChangeListener(listener)
} finally { throw e
QUERY_LATCH_HANDLER.post { }
realmRef.getAndSet(null).close()
} }
} }
} }

View File

@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.database
import android.content.Context import android.content.Context
import im.vector.matrix.android.internal.database.model.SessionRealmModule 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.UserCacheDirectory
import im.vector.matrix.android.internal.di.UserMd5 import im.vector.matrix.android.internal.di.UserMd5
import im.vector.matrix.android.internal.session.SessionModule 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, internal class SessionRealmConfigurationFactory @Inject constructor(private val realmKeysUtils: RealmKeysUtils,
@UserCacheDirectory val directory: File, @UserCacheDirectory val directory: File,
@SessionId val sessionId: String,
@UserMd5 val userMd5: String, @UserMd5 val userMd5: String,
context: Context) { context: Context) {
private val sharedPreferences = context.getSharedPreferences("im.vector.matrix.android.realm", Context.MODE_PRIVATE) private val sharedPreferences = context.getSharedPreferences("im.vector.matrix.android.realm", Context.MODE_PRIVATE)
fun create(): RealmConfiguration { 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) { if (shouldClearRealm) {
Timber.v("************************************************************") Timber.v("************************************************************")
Timber.v("The realm file session was corrupted and couldn't be loaded.") 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 sharedPreferences
.edit() .edit()
.putBoolean("$REALM_SHOULD_CLEAR_FLAG_$userMd5", true) .putBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", true)
.apply() .apply()
val realmConfiguration = RealmConfiguration.Builder() val realmConfiguration = RealmConfiguration.Builder()
.compactOnLaunch()
.directory(directory) .directory(directory)
.name(REALM_NAME) .name(REALM_NAME)
.apply { .apply {
realmKeysUtils.configureEncryption(this, "${SessionModule.DB_ALIAS_PREFIX}$userMd5") realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5))
} }
.modules(SessionRealmModule()) .modules(SessionRealmModule())
.deleteRealmIfMigrationNeeded() .deleteRealmIfMigrationNeeded()
@ -71,7 +74,7 @@ internal class SessionRealmConfigurationFactory @Inject constructor(private val
Timber.v("Successfully create realm instance") Timber.v("Successfully create realm instance")
sharedPreferences sharedPreferences
.edit() .edit()
.putBoolean("$REALM_SHOULD_CLEAR_FLAG_$userMd5", false) .putBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", false)
.apply() .apply()
} }
return realmConfiguration 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.api.session.room.send.SendState
import im.vector.matrix.android.internal.database.mapper.asDomain 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.mapper.toEntity
import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.*
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.query.find 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.getOrCreate
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.extensions.assertIsManaged import im.vector.matrix.android.internal.extensions.assertIsManaged
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import io.realm.Sort import io.realm.Sort
import io.realm.kotlin.createObject
// 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()
}
internal fun ChunkEntity.deleteOnCascade() { internal fun ChunkEntity.deleteOnCascade() {
assertIsManaged() assertIsManaged()
@ -51,11 +38,10 @@ internal fun ChunkEntity.deleteOnCascade() {
internal fun ChunkEntity.merge(roomId: String, internal fun ChunkEntity.merge(roomId: String,
chunkToMerge: ChunkEntity, chunkToMerge: ChunkEntity,
direction: PaginationDirection) { direction: PaginationDirection): List<TimelineEventEntity> {
assertIsManaged() assertIsManaged()
val isChunkToMergeUnlinked = chunkToMerge.isUnlinked() val isChunkToMergeUnlinked = chunkToMerge.isUnlinked
val isCurrentChunkUnlinked = this.isUnlinked() val isCurrentChunkUnlinked = isUnlinked
val isUnlinked = isCurrentChunkUnlinked && isChunkToMergeUnlinked
if (isCurrentChunkUnlinked && !isChunkToMergeUnlinked) { if (isCurrentChunkUnlinked && !isChunkToMergeUnlinked) {
this.timelineEvents.forEach { it.root?.isUnlinked = false } this.timelineEvents.forEach { it.root?.isUnlinked = false }
@ -70,49 +56,21 @@ internal fun ChunkEntity.merge(roomId: String,
this.isLastBackward = chunkToMerge.isLastBackward this.isLastBackward = chunkToMerge.isLastBackward
eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING) eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING)
} }
val events = eventsToMerge.mapNotNull { it.root?.asDomain() } return eventsToMerge
val eventIds = ArrayList<String>() .mapNotNull {
events.forEach { event -> val event = it.root?.asDomain() ?: return@mapNotNull null
add(roomId, event, direction, isUnlinked = isUnlinked) add(roomId, event, direction)
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()
} }
} }
internal fun ChunkEntity.add(roomId: String, internal fun ChunkEntity.add(roomId: String,
event: Event, event: Event,
direction: PaginationDirection, direction: PaginationDirection,
stateIndexOffset: Int = 0, stateIndexOffset: Int = 0
isUnlinked: Boolean = false) { ): TimelineEventEntity? {
assertIsManaged() assertIsManaged()
if (event.eventId != null && timelineEvents.find(event.eventId) != null) { if (event.eventId != null && timelineEvents.find(event.eventId) != null) {
return return null
} }
var currentDisplayIndex = lastDisplayIndex(direction, 0) var currentDisplayIndex = lastDisplayIndex(direction, 0)
if (direction == PaginationDirection.FORWARDS) { if (direction == PaginationDirection.FORWARDS) {
@ -134,12 +92,15 @@ internal fun ChunkEntity.add(roomId: String,
} }
} }
val isChunkUnlinked = isUnlinked
val localId = TimelineEventEntity.nextId(realm) val localId = TimelineEventEntity.nextId(realm)
val eventId = event.eventId ?: "" val eventId = event.eventId ?: ""
val senderId = event.senderId ?: "" val senderId = event.senderId ?: ""
val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst() 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 // 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 { val rootEvent = event.toEntity(roomId).apply {
it.root = event.toEntity(roomId).apply {
this.stateIndex = currentStateIndex this.stateIndex = currentStateIndex
this.isUnlinked = isUnlinked
this.displayIndex = currentDisplayIndex this.displayIndex = currentDisplayIndex
this.sendState = SendState.SYNCED 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.eventId = eventId
it.roomId = roomId it.roomId = roomId
it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() 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 val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size
timelineEvents.add(position, eventEntity) timelineEvents.add(position, eventEntity)
return eventEntity
} }
internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { 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 this.sendState = SendState.UNSENT
} }
val roomMembers = RoomMembers(realm, roomId) val roomMembers = RoomMembers(realm, roomId)
val myUser = roomMembers.get(senderId) val myUser = roomMembers.getLastRoomMember(senderId)
val localId = TimelineEventEntity.nextId(realm) val localId = TimelineEventEntity.nextId(realm)
val timelineEventEntity = TimelineEventEntity(localId).also { val timelineEventEntity = TimelineEventEntity(localId).also {
it.root = eventEntity it.root = eventEntity
@ -69,7 +69,6 @@ internal fun RoomEntity.addSendingEvent(event: Event) {
it.senderName = myUser?.displayName it.senderName = myUser?.displayName
it.senderAvatar = myUser?.avatarUrl it.senderAvatar = myUser?.avatarUrl
it.isUniqueDisplayName = roomMembers.isUniqueDisplayName(myUser?.displayName) it.isUniqueDisplayName = roomMembers.isUniqueDisplayName(myUser?.displayName)
it.senderMembershipEvent = roomMembers.queryRoomMemberEvent(senderId).findFirst()
} }
sendingTimelineEvents.add(0, timelineEventEntity) sendingTimelineEvents.add(0, timelineEventEntity)
} }

View File

@ -16,74 +16,9 @@
package im.vector.matrix.android.internal.database.helper package im.vector.matrix.android.internal.database.helper
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
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 io.realm.Realm 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 { internal fun TimelineEventEntity.Companion.nextId(realm: Realm): Long {
val currentIdNum = realm.where(TimelineEventEntity::class.java).max(TimelineEventEntityFields.LOCAL_ID) 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 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, readMarkerId = roomSummaryEntity.readMarkerId,
userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList(), userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList(),
canonicalAlias = roomSummaryEntity.canonicalAlias, 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 backwardsDisplayIndex: Int? = null,
var forwardsDisplayIndex: Int? = null, var forwardsDisplayIndex: Int? = null,
var backwardsStateIndex: Int? = null, var backwardsStateIndex: Int? = null,
var forwardsStateIndex: Int? = null var forwardsStateIndex: Int? = null,
var isUnlinked: Boolean = false
) : RealmObject() { ) : RealmObject() {
fun identifier() = "${prevToken}_$nextToken" 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 breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS,
var canonicalAlias: String? = null, var canonicalAlias: String? = null,
var aliases: RealmList<String> = RealmList(), var aliases: RealmList<String> = RealmList(),
var flatAliases: String = "" // this is required for querying
var flatAliases: String = "",
var isEncrypted: Boolean = false
) : RealmObject() { ) : RealmObject() {
private var membershipStr: String = Membership.NONE.name private var membershipStr: String = Membership.NONE.name

View File

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

View File

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

View File

@ -57,9 +57,15 @@ internal fun ChunkEntity.Companion.findIncludingEvent(realm: Realm, eventId: Str
return findAllIncludingEvents(realm, listOf(eventId)).firstOrNull() 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 { return realm.createObject<ChunkEntity>().apply {
this.prevToken = prevToken this.prevToken = prevToken
this.nextToken = nextToken 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> { internal fun TimelineEventEntity.Companion.findWithSenderMembershipEvent(realm: Realm, senderMembershipEventId: String): List<TimelineEventEntity> {
return realm.where<TimelineEventEntity>() return realm.where<TimelineEventEntity>()
.equalTo(TimelineEventEntityFields.SENDER_MEMBERSHIP_EVENT.EVENT_ID, senderMembershipEventId) .equalTo(TimelineEventEntityFields.SENDER_MEMBERSHIP_EVENT_ID, senderMembershipEventId)
.findAll() .findAll()
} }

View File

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

View File

@ -31,3 +31,10 @@ internal annotation class UserId
@Qualifier @Qualifier
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
internal annotation class UserMd5 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 package im.vector.matrix.android.internal.network
import android.content.Context import android.content.Context
import androidx.annotation.WorkerThread
import com.novoda.merlin.Merlin import com.novoda.merlin.Merlin
import com.novoda.merlin.MerlinsBeard import com.novoda.merlin.MerlinsBeard
import im.vector.matrix.android.internal.di.MatrixScope import im.vector.matrix.android.internal.di.MatrixScope
@ -28,8 +29,8 @@ import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
@MatrixScope @MatrixScope
internal class NetworkConnectivityChecker @Inject constructor(context: Context, internal class NetworkConnectivityChecker @Inject constructor(private val context: Context,
backgroundDetectionObserver: BackgroundDetectionObserver) private val backgroundDetectionObserver: BackgroundDetectionObserver)
: BackgroundDetectionObserver.Listener { : BackgroundDetectionObserver.Listener {
private val merlin = Merlin.Builder() private val merlin = Merlin.Builder()
@ -37,19 +38,33 @@ internal class NetworkConnectivityChecker @Inject constructor(context: Context,
.withDisconnectableCallbacks() .withDisconnectableCallbacks()
.build(context) .build(context)
private val listeners = Collections.synchronizedSet(LinkedHashSet<Listener>()) private val merlinsBeard = MerlinsBeard.Builder().build(context)
// True when internet is available private val listeners = Collections.synchronizedSet(LinkedHashSet<Listener>())
var hasInternetAccess = MerlinsBeard.Builder().build(context).isConnected private var hasInternetAccess = merlinsBeard.isConnected
private set
init { init {
backgroundDetectionObserver.register(this) 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() { override fun onMoveToForeground() {
merlin.bind() merlin.bind()
merlinsBeard.hasInternetAccess {
hasInternetAccess = it
}
merlin.registerDisconnectable { merlin.registerDisconnectable {
if (hasInternetAccess) { if (hasInternetAccess) {
Timber.v("On Disconnect") Timber.v("On Disconnect")
@ -76,14 +91,17 @@ internal class NetworkConnectivityChecker @Inject constructor(context: Context,
merlin.unbind() merlin.unbind()
} }
// In background you won't get notification as merlin is unbound
suspend fun waitUntilConnected() { suspend fun waitUntilConnected() {
if (hasInternetAccess) { if (hasInternetAccess) {
return return
} else { } else {
Timber.v("Waiting for network...")
suspendCoroutine<Unit> { continuation -> suspendCoroutine<Unit> { continuation ->
register(object : Listener { register(object : Listener {
override fun onConnect() { override fun onConnect() {
unregister(this) unregister(this)
Timber.v("Connected to network...")
continuation.resume(Unit) 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.api.util.Cancelable
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments 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.extensions.foldToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.md5 import im.vector.matrix.android.internal.util.md5
@ -42,7 +42,7 @@ import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
internal class DefaultFileService @Inject constructor(private val context: Context, 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 contentUrlResolver: ContentUrlResolver,
private val coroutineDispatchers: MatrixCoroutineDispatchers) : FileService { private val coroutineDispatchers: MatrixCoroutineDispatchers) : FileService {
@ -103,9 +103,9 @@ internal class DefaultFileService @Inject constructor(private val context: Conte
return when (downloadMode) { return when (downloadMode) {
FileService.DownloadMode.FOR_INTERNAL_USE -> { FileService.DownloadMode.FOR_INTERNAL_USE -> {
// Create dir tree (MF stands for Matrix File): // 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 tmpFolderRoot = File(context.cacheDir, "MF")
val tmpFolderUser = File(tmpFolderRoot, userMd5) val tmpFolderUser = File(tmpFolderRoot, sessionId)
File(tmpFolderUser, id.md5()) File(tmpFolderUser, id.md5())
} }
FileService.DownloadMode.TO_EXPORT -> { 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.auth.SessionParamsStore
import im.vector.matrix.android.internal.crypto.DefaultCryptoService import im.vector.matrix.android.internal.crypto.DefaultCryptoService
import im.vector.matrix.android.internal.database.LiveEntityObserver 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.SyncThread
import im.vector.matrix.android.internal.session.sync.job.SyncWorker import im.vector.matrix.android.internal.session.sync.job.SyncWorker
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -76,6 +78,8 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
private val secureStorageService: Lazy<SecureStorageService>, private val secureStorageService: Lazy<SecureStorageService>,
private val syncThreadProvider: Provider<SyncThread>, private val syncThreadProvider: Provider<SyncThread>,
private val contentUrlResolver: ContentUrlResolver, private val contentUrlResolver: ContentUrlResolver,
private val syncTokenStore: SyncTokenStore,
private val syncTaskSequencer: SyncTaskSequencer,
private val sessionParamsStore: SessionParamsStore, private val sessionParamsStore: SessionParamsStore,
private val contentUploadProgressTracker: ContentUploadStateTracker, private val contentUploadProgressTracker: ContentUploadStateTracker,
private val initialSyncProgressService: Lazy<InitialSyncProgressService>, private val initialSyncProgressService: Lazy<InitialSyncProgressService>,
@ -149,12 +153,17 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
cryptoService.get().close() cryptoService.get().close()
isOpen = false isOpen = false
EventBus.getDefault().unregister(this) EventBus.getDefault().unregister(this)
syncTaskSequencer.close()
} }
override fun syncState(): LiveData<SyncState> { override fun getSyncStateLive(): LiveData<SyncState> {
return getSyncThread().liveState() return getSyncThread().liveState()
} }
override fun hasAlreadySynced(): Boolean {
return syncTokenStore.getLastToken() != null
}
private fun getSyncThread(): SyncThread { private fun getSyncThread(): SyncThread {
return syncThread ?: syncThreadProvider.get().also { return syncThread ?: syncThreadProvider.get().also {
syncThread = it syncThread = it
@ -164,17 +173,8 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
override fun clearCache(callback: MatrixCallback<Unit>) { override fun clearCache(callback: MatrixCallback<Unit>) {
stopSync() stopSync()
stopAnyBackgroundSync() stopAnyBackgroundSync()
cacheService.get().clearCache(object : MatrixCallback<Unit> { liveEntityObservers.forEach { it.cancelProcess() }
override fun onSuccess(data: Unit) { cacheService.get().clearCache(callback)
startSync(true)
callback.onSuccess(data)
}
override fun onFailure(failure: Throwable) {
startSync(true)
callback.onFailure(failure)
}
})
} }
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)

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.UserModule
import im.vector.matrix.android.internal.session.user.accountdata.AccountDataModule import im.vector.matrix.android.internal.session.user.accountdata.AccountDataModule
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
@Component(dependencies = [MatrixComponent::class], @Component(dependencies = [MatrixComponent::class],
modules = [ modules = [
@ -69,6 +70,8 @@ import im.vector.matrix.android.internal.task.TaskExecutor
@SessionScope @SessionScope
internal interface SessionComponent { internal interface SessionComponent {
fun coroutineDispatchers(): MatrixCoroutineDispatchers
fun session(): Session fun session(): Session
fun syncTask(): SyncTask 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.Session
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService
import im.vector.matrix.android.api.session.securestorage.SecureStorageService 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.LiveEntityObserver
import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory
import im.vector.matrix.android.internal.di.* import im.vector.matrix.android.internal.di.*
@ -54,8 +55,7 @@ internal abstract class SessionModule {
@Module @Module
companion object { companion object {
internal fun getKeyAlias(userMd5: String) = "session_db_$userMd5"
internal const val DB_ALIAS_PREFIX = "session_db_"
@JvmStatic @JvmStatic
@Provides @Provides
@ -83,11 +83,26 @@ internal abstract class SessionModule {
return userId.md5() return userId.md5()
} }
@JvmStatic
@SessionId
@Provides
fun providesSessionId(credentials: Credentials): String {
return createSessionId(credentials.userId, credentials.deviceId)
}
@JvmStatic @JvmStatic
@Provides @Provides
@UserCacheDirectory @UserCacheDirectory
fun providesFilesDir(@UserMd5 userMd5: String, context: Context): File { fun providesFilesDir(@UserMd5 userMd5: String,
return File(context.filesDir, userMd5) @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 @JvmStatic

View File

@ -20,11 +20,16 @@ import androidx.lifecycle.LiveData
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.group.Group 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.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.api.session.group.model.GroupSummary
import im.vector.matrix.android.internal.database.mapper.asDomain 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.GroupSummaryEntity
import im.vector.matrix.android.internal.database.model.GroupSummaryEntityFields import im.vector.matrix.android.internal.database.model.GroupSummaryEntityFields
import im.vector.matrix.android.internal.database.query.where 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 import javax.inject.Inject
internal class DefaultGroupService @Inject constructor(private val monarchy: Monarchy) : GroupService { 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 return null
} }
override fun liveGroupSummaries(): LiveData<List<GroupSummary>> { override fun getGroupSummary(groupId: String): GroupSummary? {
return monarchy.findAllMappedWithChanges( return monarchy.fetchCopyMap(
{ realm -> GroupSummaryEntity.where(realm).isNotEmpty(GroupSummaryEntityFields.DISPLAY_NAME) }, { realm -> GroupSummaryEntity.where(realm, groupId).findFirst() },
{ it, _ -> it.asDomain() }
)
}
override fun getGroupSummaries(groupSummaryQueryParams: GroupSummaryQueryParams): List<GroupSummary> {
return monarchy.fetchAllMappedSync(
{ groupSummariesQuery(it, groupSummaryQueryParams) },
{ it.asDomain() } { 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 com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.room.model.Membership 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.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.GroupEntity
import im.vector.matrix.android.internal.database.model.GroupSummaryEntity import im.vector.matrix.android.internal.database.model.GroupSummaryEntity
import im.vector.matrix.android.internal.database.query.where 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 im.vector.matrix.android.internal.worker.WorkerParamsFactory
import io.realm.OrderedCollectionChangeSet import io.realm.OrderedCollectionChangeSet
import io.realm.RealmResults import io.realm.RealmResults
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
private const val GET_GROUP_DATA_WORKER = "GET_GROUP_DATA_WORKER" private const val GET_GROUP_DATA_WORKER = "GET_GROUP_DATA_WORKER"
@ -53,10 +55,15 @@ internal class GroupSummaryUpdater @Inject constructor(private val context: Cont
.map { it.groupId } .map { it.groupId }
.toList()) .toList())
deleteGroups(modifiedGroupEntity modifiedGroupEntity
.filter { it.membership == Membership.LEAVE } .filter { it.membership == Membership.LEAVE }
.map { it.groupId } .map { it.groupId }
.toList()) .toList()
.also {
observerScope.launch {
deleteGroups(it)
}
}
} }
private fun fetchGroupsData(groupIds: List<String>) { 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 * Delete the GroupSummaryEntity of left groups
*/ */
private fun deleteGroups(groupIds: List<String>) { private suspend fun deleteGroups(groupIds: List<String>) = awaitTransaction(monarchy.realmConfiguration) { realm ->
monarchy
.writeAsync { realm ->
GroupSummaryEntity.where(realm, groupIds) GroupSummaryEntity.where(realm, groupIds)
.findAll() .findAll()
.deleteAllFromRealm() .deleteAllFromRealm()
} }
}
} }

View File

@ -86,7 +86,7 @@ internal class DefaultPusherService @Inject constructor(private val context: Con
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun livePushers(): LiveData<List<Pusher>> { override fun getPushersLive(): LiveData<List<Pusher>> {
return monarchy.findAllMappedWithChanges( return monarchy.findAllMappedWithChanges(
{ realm -> PusherEntity.where(realm) }, { realm -> PusherEntity.where(realm) },
{ it.asDomain() } { 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.MatrixCallback
import im.vector.matrix.android.api.session.room.Room 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.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.RoomSummary
import im.vector.matrix.android.api.session.room.model.VersioningState import im.vector.matrix.android.api.session.room.model.VersioningState
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams 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.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity 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.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.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.alias.GetRoomIdByAliasTask
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask 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.session.user.accountdata.UpdateBreadcrumbsTask
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.fetchCopyMap
import io.realm.Realm import io.realm.Realm
import io.realm.RealmQuery
import javax.inject.Inject import javax.inject.Inject
internal class DefaultRoomService @Inject constructor(private val monarchy: Monarchy, internal class DefaultRoomService @Inject constructor(private val monarchy: Monarchy,
@ -69,28 +74,64 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
} }
} }
override fun liveRoomSummaries(): LiveData<List<RoomSummary>> { override fun getRoomSummary(roomIdOrAlias: String): RoomSummary? {
return monarchy.findAllMappedWithChanges( return monarchy
{ realm -> .fetchCopyMap({
RoomSummaryEntity.where(realm) if (roomIdOrAlias.startsWith("!")) {
.isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) // It's a roomId
.notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name) 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) } { roomSummaryMapper.map(it) }
) )
} }
override fun liveBreadcrumbs(): LiveData<List<RoomSummary>> { override fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams): LiveData<List<RoomSummary>> {
return monarchy.findAllMappedWithChanges( return monarchy.findAllMappedWithChanges(
{ realm -> { roomSummariesQuery(it, queryParams) },
RoomSummaryEntity.where(realm) { 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) .isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME)
.notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name) .notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name)
.greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummaryEntity.NOT_IN_BREADCRUMBS) .greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummaryEntity.NOT_IN_BREADCRUMBS)
.sort(RoomSummaryEntityFields.BREADCRUMBS_INDEX) .sort(RoomSummaryEntityFields.BREADCRUMBS_INDEX)
},
{ roomSummaryMapper.map(it) }
)
} }
override fun onRoomDisplayed(roomId: String): Cancelable { override fun onRoomDisplayed(roomId: String): Cancelable {

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

View File

@ -212,11 +212,12 @@ internal interface RoomAPI {
/** /**
* Join the given room. * 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 * @param params the request body
*/ */
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/join") @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "join/{roomIdOrAlias}")
fun join(@Path("roomId") roomId: String, fun join(@Path("roomIdOrAlias") roomIdOrAlias: String,
@Query("server_name") viaServers: List<String>, @Query("server_name") viaServers: List<String>,
@Body params: Map<String, String?>): Call<Unit> @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.EventType
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomAvatarContent 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.mapper.ContentMapper
import im.vector.matrix.android.internal.database.model.EventEntity 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.prev
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.UserId
@ -47,19 +46,15 @@ internal class RoomAvatarResolver @Inject constructor(private val monarchy: Mona
return@doWithRealm return@doWithRealm
} }
val roomMembers = RoomMembers(realm, roomId) 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) // 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) { if (members.size == 1) {
res = members.firstOrNull()?.toRoomMember()?.avatarUrl res = members.firstOrNull()?.avatarUrl
} else if (members.size == 2) { } else if (members.size == 2) {
val firstOtherMember = members.where().notEqualTo(EventEntityFields.STATE_KEY, userId).findFirst() val firstOtherMember = members.where().notEqualTo(RoomMemberEntityFields.USER_ID, userId).findFirst()
res = firstOtherMember?.toRoomMember()?.avatarUrl res = firstOtherMember?.avatarUrl
} }
} }
return res 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.crypto.CryptoService
import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper 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.draft.DefaultDraftService
import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService
import im.vector.matrix.android.internal.session.room.notification.DefaultRoomPushRuleService import im.vector.matrix.android.internal.session.room.notification.DefaultRoomPushRuleService
@ -35,6 +36,7 @@ internal interface RoomFactory {
fun create(roomId: String): Room fun create(roomId: String): Room
} }
@SessionScope
internal class DefaultRoomFactory @Inject constructor(private val monarchy: Monarchy, internal class DefaultRoomFactory @Inject constructor(private val monarchy: Monarchy,
private val roomSummaryMapper: RoomSummaryMapper, private val roomSummaryMapper: RoomSummaryMapper,
private val cryptoService: CryptoService, 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.RoomCanonicalAliasContent
import im.vector.matrix.android.api.session.room.model.RoomTopicContent 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.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.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.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity 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.isEventRead
import im.vector.matrix.android.internal.database.query.latestEvent import im.vector.matrix.android.internal.database.query.latestEvent
import im.vector.matrix.android.internal.database.query.prev 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.RoomSyncSummary
import im.vector.matrix.android.internal.session.sync.model.RoomSyncUnreadNotifications import im.vector.matrix.android.internal.session.sync.model.RoomSyncUnreadNotifications
import io.realm.Realm import io.realm.Realm
import io.realm.kotlin.createObject
import javax.inject.Inject import javax.inject.Inject
internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId: String, 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_NAME,
EventType.STATE_ROOM_TOPIC, EventType.STATE_ROOM_TOPIC,
EventType.STATE_ROOM_MEMBER, EventType.STATE_ROOM_MEMBER,
EventType.STATE_HISTORY_VISIBILITY, EventType.STATE_ROOM_HISTORY_VISIBILITY,
EventType.CALL_INVITE, EventType.CALL_INVITE,
EventType.CALL_HANGUP, EventType.CALL_HANGUP,
EventType.CALL_ANSWER, EventType.CALL_ANSWER,
@ -69,9 +69,7 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId
roomSummary: RoomSyncSummary? = null, roomSummary: RoomSyncSummary? = null,
unreadNotifications: RoomSyncUnreadNotifications? = null, unreadNotifications: RoomSyncUnreadNotifications? = null,
updateMembers: Boolean = false) { updateMembers: Boolean = false) {
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
?: realm.createObject(roomId)
if (roomSummary != null) { if (roomSummary != null) {
if (roomSummary.heroes.isNotEmpty()) { if (roomSummary.heroes.isNotEmpty()) {
roomSummaryEntity.heroes.clear() roomSummaryEntity.heroes.clear()
@ -93,8 +91,9 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId
val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = PREVIEWABLE_TYPES) val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = PREVIEWABLE_TYPES)
val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev() 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 lastAliasesEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).prev()
val encryptionEvent = EventEntity.where(realm, roomId, EventType.ENCRYPTION).prev()
roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0
// avoid this call if we are sure there are unread events // avoid this call if we are sure there are unread events
@ -107,18 +106,20 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId
roomSummaryEntity.canonicalAlias = ContentMapper.map(lastCanonicalAliasEvent?.content).toModel<RoomCanonicalAliasContent>() roomSummaryEntity.canonicalAlias = ContentMapper.map(lastCanonicalAliasEvent?.content).toModel<RoomCanonicalAliasContent>()
?.canonicalAlias ?.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.clear()
roomSummaryEntity.aliases.addAll(roomAliases) roomSummaryEntity.aliases.addAll(roomAliases)
roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|") roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|")
roomSummaryEntity.isEncrypted = encryptionEvent != null
if (updateMembers) { if (updateMembers) {
val otherRoomMembers = RoomMembers(realm, roomId) val otherRoomMembers = RoomMembers(realm, roomId)
.queryRoomMembersEvent() .queryRoomMembersEvent()
.notEqualTo(EventEntityFields.STATE_KEY, userId) .notEqualTo(RoomMemberEntityFields.USER_ID, userId)
.findAll() .findAll()
.asSequence() .asSequence()
.map { it.stateKey } .map { it.userId }
roomSummaryEntity.otherMemberIds.clear() roomSummaryEntity.otherMemberIds.clear()
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers) 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.failure.CreateRoomFailure
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams 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.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.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomEntityFields import im.vector.matrix.android.internal.database.model.RoomEntityFields
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity 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.task.Task
import im.vector.matrix.android.internal.util.awaitTransaction import im.vector.matrix.android.internal.util.awaitTransaction
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import kotlinx.coroutines.TimeoutCancellationException
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@ -53,13 +54,12 @@ internal class DefaultCreateRoomTask @Inject constructor(private val roomAPI: Ro
} }
val roomId = createRoomResponse.roomId!! 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) // 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 -> try {
awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->
realm.where(RoomEntity::class.java) realm.where(RoomEntity::class.java)
.equalTo(RoomEntityFields.ROOM_ID, roomId) .equalTo(RoomEntityFields.ROOM_ID, roomId)
} }
try { } catch (exception: TimeoutCancellationException) {
rql.await(timeout = 1L, timeUnit = TimeUnit.MINUTES)
} catch (exception: Exception) {
throw CreateRoomFailure.CreatedWithTimeout throw CreateRoomFailure.CreatedWithTimeout
} }
if (params.isDirect()) { 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.VersioningState
import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent 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.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.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity 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.database.query.where
import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.di.SessionDatabase
import io.realm.OrderedCollectionChangeSet import io.realm.OrderedCollectionChangeSet
import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import io.realm.RealmResults import io.realm.RealmResults
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
internal class RoomCreateEventLiveObserver @Inject constructor(@SessionDatabase internal class RoomCreateEventLiveObserver @Inject constructor(@SessionDatabase
@ -51,12 +52,13 @@ internal class RoomCreateEventLiveObserver @Inject constructor(@SessionDatabase
} }
.toList() .toList()
.also { .also {
observerScope.launch {
handleRoomCreateEvents(it) handleRoomCreateEvents(it)
} }
} }
}
private fun handleRoomCreateEvents(createEvents: List<Event>) = Realm.getInstance(realmConfiguration).use { private suspend fun handleRoomCreateEvents(createEvents: List<Event>) = awaitTransaction(realmConfiguration) { realm ->
it.executeTransactionAsync { realm ->
for (event in createEvents) { for (event in createEvents) {
val createRoomContent = event.getClearContent().toModel<RoomCreateContent>() val createRoomContent = event.getClearContent().toModel<RoomCreateContent>()
val predecessorRoomId = createRoomContent?.predecessor?.roomId ?: continue val predecessorRoomId = createRoomContent?.predecessor?.roomId ?: continue
@ -67,5 +69,4 @@ internal class RoomCreateEventLiveObserver @Inject constructor(@SessionDatabase
realm.insertOrUpdate(predecessorRoomSummary) realm.insertOrUpdate(predecessorRoomSummary)
} }
} }
}
} }

View File

@ -21,18 +21,23 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback 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.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.Membership
import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.database.mapper.asDomain 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.InviteTask
import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask 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.session.room.membership.leaving.LeaveRoomTask
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.fetchCopied 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, internal class DefaultMembershipService @AssistedInject constructor(@Assisted private val roomId: String,
private val monarchy: Monarchy, private val monarchy: Monarchy,
@ -58,29 +63,44 @@ internal class DefaultMembershipService @AssistedInject constructor(@Assisted pr
} }
override fun getRoomMember(userId: String): RoomMember? { override fun getRoomMember(userId: String): RoomMember? {
val eventEntity = monarchy.fetchCopied { val roomMemberEntity = monarchy.fetchCopied {
RoomMembers(it, roomId).queryRoomMemberEvent(userId).findFirst() RoomMembers(it, roomId).getLastRoomMember(userId)
} }
return eventEntity?.asDomain()?.content.toModel() return roomMemberEntity?.asDomain()
} }
override fun getRoomMemberIdsLive(): LiveData<List<String>> { override fun getRoomMembers(queryParams: RoomMemberQueryParams): List<RoomMember> {
return monarchy.findAllMappedWithChanges( return monarchy.fetchAllMappedSync(
{ {
RoomMembers(it, roomId).queryRoomMembersEvent() roomMembersQuery(it, queryParams)
}, },
{ {
it.stateKey!! it.asDomain()
} }
) )
} }
override fun getNumberOfJoinedMembers(): Int { override fun getRoomMembersLive(queryParams: RoomMemberQueryParams): LiveData<List<RoomMember>> {
var result = 0 return monarchy.findAllMappedWithChanges(
monarchy.runTransactionSync { {
result = RoomMembers(it, roomId).getNumberOfJoinedMembers() 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 {
return Realm.getInstance(monarchy.realmConfiguration).use {
RoomMembers(it, roomId).getNumberOfJoinedMembers()
} }
return result
} }
override fun invite(userId: String, reason: String?, callback: MatrixCallback<Unit>): Cancelable { 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 com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.room.model.Membership 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.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.model.RoomEntity
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.network.executeRequest 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.RoomAPI
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.session.sync.SyncTokenStore 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.task.Task
import im.vector.matrix.android.internal.util.awaitTransaction import im.vector.matrix.android.internal.util.awaitTransaction
import io.realm.Realm import io.realm.Realm
@ -44,7 +43,9 @@ internal interface LoadRoomMembersTask : Task<LoadRoomMembersTask.Params, Unit>
internal class DefaultLoadRoomMembersTask @Inject constructor(private val roomAPI: RoomAPI, internal class DefaultLoadRoomMembersTask @Inject constructor(private val roomAPI: RoomAPI,
private val monarchy: Monarchy, private val monarchy: Monarchy,
private val syncTokenStore: SyncTokenStore, private val syncTokenStore: SyncTokenStore,
private val roomSummaryUpdater: RoomSummaryUpdater private val roomSummaryUpdater: RoomSummaryUpdater,
private val roomMemberEventHandler: RoomMemberEventHandler,
private val timelineEventSenderVisitor: TimelineEventSenderVisitor
) : LoadRoomMembersTask { ) : LoadRoomMembersTask {
override suspend fun execute(params: LoadRoomMembersTask.Params) { override suspend fun execute(params: LoadRoomMembersTask.Params) {
@ -66,12 +67,11 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(private val roomAP
for (roomMemberEvent in response.roomMemberEvents) { for (roomMemberEvent in response.roomMemberEvents) {
roomEntity.addStateEvent(roomMemberEvent) roomEntity.addStateEvent(roomMemberEvent)
UserEntityFactory.createOrNull(roomMemberEvent)?.also { roomMemberEventHandler.handle(realm, roomId, roomMemberEvent)
realm.insertOrUpdate(it)
}
} }
timelineEventSenderVisitor.clear()
roomEntity.chunks.flatMap { it.timelineEvents }.forEach { roomEntity.chunks.flatMap { it.timelineEvents }.forEach {
it.updateSenderData() timelineEventSenderVisitor.visit(it)
} }
roomEntity.areAllMembersLoaded = true roomEntity.areAllMembersLoaded = true
roomSummaryUpdater.update(realm, roomId, updateMembers = 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.events.model.toModel
import im.vector.matrix.android.api.session.room.model.* 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.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.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.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.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.prev
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
@ -62,7 +63,7 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context:
return@doWithRealm 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 name = ContentMapper.map(canonicalAlias?.content).toModel<RoomCanonicalAliasContent>()?.canonicalAlias
if (!name.isNullOrEmpty()) { if (!name.isNullOrEmpty()) {
return@doWithRealm return@doWithRealm
@ -75,32 +76,35 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context:
} }
val roomMembers = RoomMembers(realm, roomId) val roomMembers = RoomMembers(realm, roomId)
val loadedMembers = roomMembers.queryRoomMembersEvent().findAll() val activeMembers = roomMembers.queryActiveRoomMembersEvent().findAll()
if (roomEntity?.membership == Membership.INVITE) { if (roomEntity?.membership == Membership.INVITE) {
val inviteMeEvent = roomMembers.queryRoomMemberEvent(userId).findFirst() val inviteMeEvent = roomMembers.getLastStateEvent(userId)
val inviterId = inviteMeEvent?.sender val inviterId = inviteMeEvent?.sender
name = if (inviterId != null) { name = if (inviterId != null) {
val inviterMemberEvent = loadedMembers.where() activeMembers.where()
.equalTo(EventEntityFields.STATE_KEY, inviterId) .equalTo(RoomMemberEntityFields.USER_ID, inviterId)
.findFirst() .findFirst()
inviterMemberEvent?.toRoomMember()?.displayName ?.displayName
} else { } else {
context.getString(R.string.room_displayname_room_invite) context.getString(R.string.room_displayname_room_invite)
} }
} else if (roomEntity?.membership == Membership.JOIN) { } else if (roomEntity?.membership == Membership.JOIN) {
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
val otherMembersSubset: List<EventEntity> = if (roomSummary?.heroes?.isNotEmpty() == true) { val otherMembersSubset: List<RoomMemberEntity> = if (roomSummary?.heroes?.isNotEmpty() == true) {
roomSummary.heroes.mapNotNull { roomSummary.heroes.mapNotNull { userId ->
roomMembers.getStateEvent(it) roomMembers.getLastRoomMember(userId)?.takeIf {
it.membership == Membership.INVITE || it.membership == Membership.JOIN
}
} }
} else { } else {
loadedMembers.where() activeMembers.where()
.notEqualTo(EventEntityFields.STATE_KEY, userId) .notEqualTo(RoomMemberEntityFields.USER_ID, userId)
.limit(3) .limit(3)
.findAll() .findAll()
.createSnapshot()
} }
val otherMembersCount = roomMembers.getNumberOfMembers() - 1 val otherMembersCount = otherMembersSubset.count()
name = when (otherMembersCount) { name = when (otherMembersCount) {
0 -> context.getString(R.string.room_displayname_empty_room) 0 -> context.getString(R.string.room_displayname_empty_room)
1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers) 1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers)
@ -119,19 +123,14 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context:
return name ?: roomId return name ?: roomId
} }
private fun resolveRoomMemberName(eventEntity: EventEntity?, private fun resolveRoomMemberName(roomMember: RoomMemberEntity?,
roomMembers: RoomMembers): String? { roomMembers: RoomMembers): String? {
if (eventEntity == null) return null if (roomMember == null) return null
val roomMember = eventEntity.toRoomMember() ?: return null
val isUnique = roomMembers.isUniqueDisplayName(roomMember.displayName) val isUnique = roomMembers.isUniqueDisplayName(roomMember.displayName)
return if (isUnique) { return if (isUnique) {
roomMember.displayName roomMember.displayName
} else { } 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 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.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.Membership
import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.internal.database.model.*
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.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.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import io.realm.Realm import io.realm.Realm
@ -42,19 +40,18 @@ internal class RoomMembers(private val realm: Realm,
RoomSummaryEntity.where(realm, roomId).findFirst() RoomSummaryEntity.where(realm, roomId).findFirst()
} }
fun getStateEvent(userId: String): EventEntity? { fun getLastStateEvent(userId: String): EventEntity? {
return EventEntity return EventEntity
.where(realm, roomId, EventType.STATE_ROOM_MEMBER) .where(realm, roomId, EventType.STATE_ROOM_MEMBER)
.sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING)
.equalTo(EventEntityFields.STATE_KEY, userId) .equalTo(EventEntityFields.STATE_KEY, userId)
.sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING)
.findFirst() .findFirst()
} }
fun get(userId: String): RoomMember? { fun getLastRoomMember(userId: String): RoomMemberEntity? {
return getStateEvent(userId) return RoomMemberEntity
?.let { .where(realm, roomId, userId)
it.asDomain().content?.toModel<RoomMember>() .findFirst()
}
} }
fun isUniqueDisplayName(displayName: String?): Boolean { fun isUniqueDisplayName(displayName: String?): Boolean {
@ -69,26 +66,27 @@ internal class RoomMembers(private val realm: Realm,
.size == 1 .size == 1
} }
fun queryRoomMembersEvent(): RealmQuery<EventEntity> { fun queryRoomMembersEvent(): RealmQuery<RoomMemberEntity> {
return EventEntity return RoomMemberEntity.where(realm, roomId)
.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 queryJoinedRoomMembersEvent(): RealmQuery<EventEntity> { fun queryJoinedRoomMembersEvent(): RealmQuery<RoomMemberEntity> {
return queryRoomMembersEvent().contains(EventEntityFields.CONTENT, "\"membership\":\"join\"")
}
fun queryInvitedRoomMembersEvent(): RealmQuery<EventEntity> {
return queryRoomMembersEvent().contains(EventEntityFields.CONTENT, "\"membership\":\"invite\"")
}
fun queryRoomMemberEvent(userId: String): RealmQuery<EventEntity> {
return queryRoomMembersEvent() 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 { fun getNumberOfJoinedMembers(): Int {
@ -111,7 +109,7 @@ internal class RoomMembers(private val realm: Realm,
* @return a roomMember id list of joined or invited members. * @return a roomMember id list of joined or invited members.
*/ */
fun getActiveRoomMemberIds(): List<String> { 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. * @return a roomMember id list of joined members.
*/ */
fun getJoinedRoomMemberIds(): List<String> { fun getJoinedRoomMemberIds(): List<String> {
return getRoomMemberIdsFiltered { it.membership == Membership.JOIN } return queryJoinedRoomMembersEvent().findAll().map { it.userId }
}
/* ==========================================================================================
* 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()
} }
} }

View File

@ -17,7 +17,7 @@
package im.vector.matrix.android.internal.session.room.membership.joining 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.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.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomEntityFields import im.vector.matrix.android.internal.database.model.RoomEntityFields
import im.vector.matrix.android.internal.di.SessionDatabase 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.session.room.read.SetReadMarkersTask
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import kotlinx.coroutines.TimeoutCancellationException
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@ -46,18 +47,16 @@ internal class DefaultJoinRoomTask @Inject constructor(private val roomAPI: Room
executeRequest<Unit> { executeRequest<Unit> {
apiCall = roomAPI.join(params.roomId, params.viaServers, mapOf("reason" to params.reason)) 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) // 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 { try {
rql.await(timeout = 1L, timeUnit = TimeUnit.MINUTES) awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->
} catch (exception: Exception) { realm.where(RoomEntity::class.java)
.equalTo(RoomEntityFields.ROOM_ID, params.roomId)
}
} catch (exception: TimeoutCancellationException) {
throw JoinRoomFailure.JoinedWithTimeout throw JoinRoomFailure.JoinedWithTimeout
} }
setReadMarkers(roomId) setReadMarkers(params.roomId)
} }
private suspend fun setReadMarkers(roomId: String) { 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.model.EventEntity
import im.vector.matrix.android.internal.database.query.types import im.vector.matrix.android.internal.database.query.types
import im.vector.matrix.android.internal.di.SessionDatabase 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.OrderedCollectionChangeSet
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import io.realm.RealmResults import io.realm.RealmResults
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject 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. * 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, internal class EventsPruner @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration,
private val pruneEventTask: PruneEventTask, private val pruneEventTask: PruneEventTask) :
private val taskExecutor: TaskExecutor) :
RealmLiveEntityObserver<EventEntity>(realmConfiguration) { RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
override val query = Monarchy.Query<EventEntity> { EventEntity.types(it, listOf(EventType.REDACTION)) } 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() } .mapNotNull { results[it]?.asDomain() }
.toList() .toList()
observerScope.launch {
val params = PruneEventTask.Params(insertedDomains) val params = PruneEventTask.Params(insertedDomains)
pruneEventTask.configureWith(params).executeBy(taskExecutor) 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.EventType
import im.vector.matrix.android.api.session.events.model.LocalEcho 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.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.ContentMapper
import im.vector.matrix.android.internal.database.mapper.EventMapper import im.vector.matrix.android.internal.database.mapper.EventMapper
import im.vector.matrix.android.internal.database.model.EventEntity 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) { override suspend fun execute(params: PruneEventTask.Params) {
monarchy.awaitTransaction { realm -> 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() val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst()
?: return ?: return
val allowedKeys = computeAllowedKeys(eventToPrune.type) val typeToPrune = eventToPrune.type
val stateKey = eventToPrune.stateKey
val allowedKeys = computeAllowedKeys(typeToPrune)
if (allowedKeys.isNotEmpty()) { if (allowedKeys.isNotEmpty()) {
val prunedContent = ContentMapper.map(eventToPrune.content)?.filterKeys { key -> allowedKeys.contains(key) } val prunedContent = ContentMapper.map(eventToPrune.content)?.filterKeys { key -> allowedKeys.contains(key) }
eventToPrune.content = ContentMapper.map(prunedContent) eventToPrune.content = ContentMapper.map(prunedContent)
} else { } else {
when (eventToPrune.type) { when (typeToPrune) {
EventType.ENCRYPTED, EventType.ENCRYPTED,
EventType.MESSAGE -> { EventType.MESSAGE -> {
Timber.d("REDACTION for message ${eventToPrune.eventId}") Timber.d("REDACTION for message ${eventToPrune.eventId}")
@ -94,11 +97,10 @@ 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) val timelineEventsToUpdate = TimelineEventEntity.findWithSenderMembershipEvent(realm, eventToPrune.eventId)
for (timelineEvent in timelineEventsToUpdate) { timelineEventSenderVisitor.visit(timelineEventsToUpdate)
timelineEvent.updateSenderData()
}
} }
} }
@ -118,7 +120,7 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M
"redact", "redact",
"invite") "invite")
EventType.STATE_ROOM_ALIASES -> listOf("aliases") EventType.STATE_ROOM_ALIASES -> listOf("aliases")
EventType.STATE_CANONICAL_ALIAS -> listOf("alias") EventType.STATE_ROOM_CANONICAL_ALIAS -> listOf("alias")
EventType.FEEDBACK -> listOf("type", "target_event_id") EventType.FEEDBACK -> listOf("type", "target_event_id")
else -> emptyList() else -> emptyList()
} }

View File

@ -215,7 +215,16 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
return TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain) 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( val liveData = monarchy.findAllMappedWithChanges(
{ EventAnnotationsSummaryEntity.where(it, eventId) }, { EventAnnotationsSummaryEntity.where(it, eventId) },
{ it.asDomain() } { it.asDomain() }

View File

@ -16,10 +16,10 @@
package im.vector.matrix.android.internal.session.room.send.pills 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( internal data class MentionLinkSpec(
val span: UserMentionSpan, val span: MatrixItemSpan,
val start: Int, val start: Int,
val end: Int val end: Int
) )

View File

@ -16,15 +16,13 @@
package im.vector.matrix.android.internal.session.room.send.pills package im.vector.matrix.android.internal.session.room.send.pills
import android.text.SpannableString 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 java.util.*
import javax.inject.Inject import javax.inject.Inject
/** /**
* Utility class to detect special span in CharSequence and turn them into * Utility class to detect special span in CharSequence and turn them into
* formatted text to send them as a Matrix messages. * formatted text to send them as a Matrix messages.
*
* For now only support UserMentionSpans (TODO rooms, room aliases, etc...)
*/ */
internal class TextPillsUtils @Inject constructor( internal class TextPillsUtils @Inject constructor(
private val mentionLinkSpecComparator: MentionLinkSpecComparator private val mentionLinkSpecComparator: MentionLinkSpecComparator
@ -49,7 +47,7 @@ internal class TextPillsUtils @Inject constructor(
private fun transformPills(text: CharSequence, template: String): String? { private fun transformPills(text: CharSequence, template: String): String? {
val spannableString = SpannableString.valueOf(text) val spannableString = SpannableString.valueOf(text)
val pills = spannableString 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)) } ?.map { MentionLinkSpec(it, spannableString.getSpanStart(it), spannableString.getSpanEnd(it)) }
?.toMutableList() ?.toMutableList()
?.takeIf { it.isNotEmpty() } ?.takeIf { it.isNotEmpty() }
@ -65,7 +63,7 @@ internal class TextPillsUtils @Inject constructor(
// append text before pill // append text before pill
append(text, currIndex, start) append(text, currIndex, start)
// append the pill // 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 currIndex = end
} }
// append text after the last pill // 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.helper.deleteOnCascade
import im.vector.matrix.android.internal.database.model.ChunkEntity 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.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.database.query.where
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.awaitTransaction import im.vector.matrix.android.internal.util.awaitTransaction
@ -38,7 +37,7 @@ internal class DefaultClearUnlinkedEventsTask @Inject constructor(private val mo
monarchy.awaitTransaction { localRealm -> monarchy.awaitTransaction { localRealm ->
val unlinkedChunks = ChunkEntity val unlinkedChunks = ChunkEntity
.where(localRealm, roomId = params.roomId) .where(localRealm, roomId = params.roomId)
.equalTo("${ChunkEntityFields.TIMELINE_EVENTS.ROOT}.${EventEntityFields.IS_UNLINKED}", true) .equalTo(ChunkEntityFields.IS_UNLINKED, true)
.findAll() .findAll()
unlinkedChunks.forEach { unlinkedChunks.forEach {
it.deleteOnCascade() 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.api.util.CancelableBag
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper 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.mapper.asDomain
import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.*
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.query.FilterContent 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.findAllInRoomWithSendStates
import im.vector.matrix.android.internal.database.query.where 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.Debouncer
import im.vector.matrix.android.internal.util.createBackgroundHandler import im.vector.matrix.android.internal.util.createBackgroundHandler
import im.vector.matrix.android.internal.util.createUIHandler import im.vector.matrix.android.internal.util.createUIHandler
import io.realm.OrderedCollectionChangeSet import io.realm.*
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 timber.log.Timber import timber.log.Timber
import java.util.Collections import java.util.*
import java.util.UUID import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@ -77,11 +65,11 @@ internal class DefaultTimeline(
private val hiddenReadReceipts: TimelineHiddenReadReceipts private val hiddenReadReceipts: TimelineHiddenReadReceipts
) : Timeline, TimelineHiddenReadReceipts.Delegate { ) : Timeline, TimelineHiddenReadReceipts.Delegate {
private companion object { companion object {
val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD") 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 isStarted = AtomicBoolean(false)
private val isReady = AtomicBoolean(false) private val isReady = AtomicBoolean(false)
private val mainHandler = createUIHandler() private val mainHandler = createUIHandler()
@ -113,12 +101,8 @@ internal class DefaultTimeline(
if (!results.isLoaded || !results.isValid) { if (!results.isLoaded || !results.isValid) {
return@OrderedRealmCollectionChangeListener return@OrderedRealmCollectionChangeListener
} }
if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL) {
handleInitialLoad()
} else {
handleUpdates(changeSet) handleUpdates(changeSet)
} }
}
private val relationsListener = OrderedRealmCollectionChangeListener<RealmResults<EventAnnotationsSummaryEntity>> { collection, changeSet -> private val relationsListener = OrderedRealmCollectionChangeListener<RealmResults<EventAnnotationsSummaryEntity>> { collection, changeSet ->
var hasChange = false var hasChange = false
@ -179,8 +163,9 @@ internal class DefaultTimeline(
nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING).findAll() nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING).findAll()
filteredEvents = nonFilteredEvents.where() filteredEvents = nonFilteredEvents.where()
.filterEventsWithSettings() .filterEventsWithSettings()
.findAllAsync() .findAll()
.also { it.addChangeListener(eventsChangeListener) } handleInitialLoad()
filteredEvents.addChangeListener(eventsChangeListener)
eventRelations = EventAnnotationsSummaryEntity.whereInRoom(realm, roomId) eventRelations = EventAnnotationsSummaryEntity.whereInRoom(realm, roomId)
.findAllAsync() .findAllAsync()
@ -288,20 +273,20 @@ internal class DefaultTimeline(
return hasMoreInCache(direction) || !hasReachedEnd(direction) return hasMoreInCache(direction) || !hasReachedEnd(direction)
} }
override fun addListener(listener: Timeline.Listener) = synchronized(listeners) { override fun addListener(listener: Timeline.Listener): Boolean {
if (listeners.contains(listener)) { if (listeners.contains(listener)) {
return false return false
} }
listeners.add(listener).also { return listeners.add(listener).also {
postSnapshot() postSnapshot()
} }
} }
override fun removeListener(listener: Timeline.Listener) = synchronized(listeners) { override fun removeListener(listener: Timeline.Listener): Boolean {
listeners.remove(listener) return listeners.remove(listener)
} }
override fun removeAllListeners() = synchronized(listeners) { override fun removeAllListeners() {
listeners.clear() listeners.clear()
} }
@ -504,7 +489,6 @@ internal class DefaultTimeline(
Timber.v("Should fetch $limit items $direction") Timber.v("Should fetch $limit items $direction")
cancelableBag += paginationTask cancelableBag += paginationTask
.configureWith(params) { .configureWith(params) {
this.retryCount = Int.MAX_VALUE
this.constraints = TaskConstraints(connectedToNetwork = true) this.constraints = TaskConstraints(connectedToNetwork = true)
this.callback = object : MatrixCallback<TokenChunkEventPersistor.Result> { this.callback = object : MatrixCallback<TokenChunkEventPersistor.Result> {
override fun onSuccess(data: TokenChunkEventPersistor.Result) { override fun onSuccess(data: TokenChunkEventPersistor.Result) {
@ -524,6 +508,8 @@ internal class DefaultTimeline(
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
postSnapshot()
Timber.v("Failure fetching $limit items $direction from pagination request") Timber.v("Failure fetching $limit items $direction from pagination request")
} }
} }
@ -637,7 +623,14 @@ internal class DefaultTimeline(
private fun fetchEvent(eventId: String) { private fun fetchEvent(eventId: String) {
val params = GetContextOfEventTask.Params(roomId, eventId, settings.initialSize) 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() { private fun postSnapshot() {
@ -648,16 +641,26 @@ internal class DefaultTimeline(
updateLoadingStates(filteredEvents) updateLoadingStates(filteredEvents)
val snapshot = createSnapshot() val snapshot = createSnapshot()
val runnable = Runnable { val runnable = Runnable {
synchronized(listeners) {
listeners.forEach { listeners.forEach {
it.onUpdated(snapshot) it.onTimelineUpdated(snapshot)
}
} }
} }
debouncer.debounce("post_snapshot", runnable, 50) 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() { private fun clearAllValues() {
prevDisplayIndex = null prevDisplayIndex = null
nextDisplayIndex = 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.find
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
import im.vector.matrix.android.internal.database.query.where 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 im.vector.matrix.android.internal.util.awaitTransaction
import io.realm.kotlin.createObject import io.realm.kotlin.createObject
import timber.log.Timber import timber.log.Timber
@ -33,7 +32,8 @@ import javax.inject.Inject
/** /**
* Insert Chunk in DB, and eventually merge with existing chunk event * 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> * <pre>
@ -132,27 +132,22 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
// The current chunk is the one we will keep all along the merge processChanges. // 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, // 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) { var currentChunk = if (direction == PaginationDirection.FORWARDS) {
prevChunk?.apply { this.nextToken = nextToken } prevChunk?.apply { this.nextToken = nextToken }
} else { } else {
nextChunk?.apply { this.prevToken = prevToken } 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) { if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) {
Timber.v("Reach end of $roomId") Timber.v("Reach end of $roomId")
currentChunk.isLastBackward = true currentChunk.isLastBackward = true
} else if (!shouldSkip) { } else if (!shouldSkip) {
Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}") Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}")
val eventIds = ArrayList<String>(receivedChunk.events.size) val timelineEvents = receivedChunk.events.mapNotNull {
for (event in receivedChunk.events) { currentChunk.add(roomId, it, direction)
event.eventId?.also { eventIds.add(it) }
currentChunk.add(roomId, event, direction, isUnlinked = currentChunk.isUnlinked())
UserEntityFactory.createOrNull(event)?.also {
realm.insertOrUpdate(it)
}
} }
// Then we merge chunks if needed // Then we merge chunks if needed
if (currentChunk != prevChunk && prevChunk != null) { if (currentChunk != prevChunk && prevChunk != null) {
@ -170,12 +165,9 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
} }
roomEntity.addOrUpdate(currentChunk) roomEntity.addOrUpdate(currentChunk)
for (stateEvent in receivedChunk.stateEvents) { for (stateEvent in receivedChunk.stateEvents) {
roomEntity.addStateEvent(stateEvent, isUnlinked = currentChunk.isUnlinked()) roomEntity.addStateEvent(stateEvent, isUnlinked = currentChunk.isUnlinked)
UserEntityFactory.createOrNull(stateEvent)?.also {
realm.insertOrUpdate(it)
} }
} timelineEventSenderVisitor.visit(timelineEvents)
currentChunk.updateSenderDataFor(eventIds)
} }
} }
return if (receivedChunk.events.isEmpty()) { 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 // 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}") Timber.v("Merge ${currentChunk.prevToken} | ${currentChunk.nextToken} with ${otherChunk.prevToken} | ${otherChunk.nextToken}")
return if (direction == PaginationDirection.BACKWARDS && !otherChunk.isLastForward) { 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) roomEntity.deleteOnCascade(otherChunk)
currentChunk currentChunk
} else { } else {
otherChunk.merge(roomEntity.roomId, currentChunk, PaginationDirection.BACKWARDS) val events = otherChunk.merge(roomEntity.roomId, currentChunk, PaginationDirection.BACKWARDS)
timelineEventSenderVisitor.visit(events)
roomEntity.deleteOnCascade(currentChunk) roomEntity.deleteOnCascade(currentChunk)
otherChunk 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.VersioningState
import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent 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.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.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity 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.database.query.where
import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.di.SessionDatabase
import io.realm.OrderedCollectionChangeSet import io.realm.OrderedCollectionChangeSet
import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import io.realm.RealmResults import io.realm.RealmResults
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
internal class RoomTombstoneEventLiveObserver @Inject constructor(@SessionDatabase internal class RoomTombstoneEventLiveObserver @Inject constructor(@SessionDatabase
@ -51,12 +52,13 @@ internal class RoomTombstoneEventLiveObserver @Inject constructor(@SessionDataba
} }
.toList() .toList()
.also { .also {
observerScope.launch {
handleRoomTombstoneEvents(it) handleRoomTombstoneEvents(it)
} }
} }
}
private fun handleRoomTombstoneEvents(tombstoneEvents: List<Event>) = Realm.getInstance(realmConfiguration).use { private suspend fun handleRoomTombstoneEvents(tombstoneEvents: List<Event>) = awaitTransaction(realmConfiguration) { realm ->
it.executeTransactionAsync { realm ->
for (event in tombstoneEvents) { for (event in tombstoneEvents) {
if (event.roomId == null) continue if (event.roomId == null) continue
val createRoomContent = event.getClearContent().toModel<RoomTombstoneContent>() val createRoomContent = event.getClearContent().toModel<RoomTombstoneContent>()
@ -70,5 +72,4 @@ internal class RoomTombstoneEventLiveObserver @Inject constructor(@SessionDataba
realm.insertOrUpdate(predecessorRoomSummary) realm.insertOrUpdate(predecessorRoomSummary)
} }
} }
}
} }

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