Merge branch 'release/0.15.0'

This commit is contained in:
Benoit Marty 2020-02-10 21:40:36 +01:00
commit c498416075
273 changed files with 6899 additions and 3374 deletions

View file

@ -86,3 +86,10 @@ steps:
plugins:
- docker#v3.1.0:
image: "openjdk"
# Check that indonesians files are identical.
# Due to Android issue, the resource folder must be values-in/, and Weblate export data into values-id/.
# If this step fails, it means that Weblate has updated the file in value-id/ so to fix it, copy the file to values-in/
- label: "Indonesian"
command:
- "diff ./vector/src/main/res/values-id/strings.xml ./vector/src/main/res/values-in/strings.xml"

5
.gitignore vendored
View file

@ -4,6 +4,7 @@
# idea files: exclude everything except dictionnaries
.idea/caches
.idea/libraries
.idea/inspectionProfiles
.idea/*.xml
.DS_Store
/build
@ -13,7 +14,3 @@
/tmp
ktlint
.idea/copyright/New_vector.xml
.idea/copyright/profiles_settings.xml
.idea/copyright/New_Vector_Ltd.xml

View file

@ -1,5 +1,6 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="RIGHT_MARGIN" value="160" />
<AndroidXmlCodeStyleSettings>
<option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" />
</AndroidXmlCodeStyleSettings>

View file

@ -0,0 +1,6 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value="Copyright (c) &amp;#36;today.year New Vector Ltd&#10;&#10;Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);&#10;you may not use this file except in compliance with the License.&#10;You may obtain a copy of the License at&#10;&#10; http://www.apache.org/licenses/LICENSE-2.0&#10;&#10;Unless required by applicable law or agreed to in writing, software&#10;distributed under the License is distributed on an &quot;AS IS&quot; BASIS,&#10;WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.&#10;See the License for the specific language governing permissions and&#10;limitations under the License." />
<option name="myName" value="NewVector" />
</copyright>
</component>

View file

@ -0,0 +1,8 @@
<component name="CopyrightManager">
<settings default="NewVector">
<LanguageOptions name="XML">
<option name="fileTypeOverride" value="1" />
<option name="prefixLines" value="false" />
</LanguageOptions>
</settings>
</component>

View file

@ -1,3 +1,17 @@
Changes in RiotX 0.15.0 (2020-02-10)
===================================================
Improvements 🙌:
- Improve navigation to the timeline (#789, #862)
- Improve network detection. It is now based on the sync request status (#873, #882)
Other changes:
- Support SSO login with Firefox account (#606)
Bugfix 🐛:
- Ask for permission before opening the camera (#934)
- Encrypt for invited users by default, if the room state allows it (#803)
Changes in RiotX 0.14.3 (2020-02-03)
===================================================

View file

@ -47,23 +47,11 @@ allprojects {
jcenter()
}
tasks.withType(JavaCompile).all {
options.compilerArgs += [
'-Adagger.gradle.incremental=enabled'
]
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
// Warnings are potential errors, so stop ignoring them
kotlinOptions.allWarningsAsErrors = true
}
afterEvaluate {
extensions.findByName("kapt")?.arguments {
arg("dagger.gradle.incremental", "enabled")
}
}
}
task clean(type: Delete) {

View file

@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import io.reactivex.Observable
import io.reactivex.android.MainThreadDisposable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
private class LiveDataObservable<T>(
@ -60,3 +61,11 @@ private class LiveDataObservable<T>(
fun <T> LiveData<T>.asObservable(): Observable<T> {
return LiveDataObservable(this).observeOn(Schedulers.computation())
}
internal fun <T> Observable<T>.startWithCallable(supplier: () -> T): Observable<T> {
val startObservable = Observable
.fromCallable(supplier)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
return startWith(startObservable)
}

View file

@ -16,8 +16,6 @@
package im.vector.matrix.rx
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams
@ -30,114 +28,43 @@ import im.vector.matrix.android.api.session.room.send.UserDraft
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.functions.BiFunction
import timber.log.Timber
class RxRoom(private val room: Room, private val session: Session) {
class RxRoom(private val room: Room) {
fun liveRoomSummary(): Observable<Optional<RoomSummary>> {
val summaryObservable = room.getRoomSummaryLive()
return room.getRoomSummaryLive()
.asObservable()
.startWith(room.roomSummary().toOptional())
.doOnNext { Timber.v("RX: summary emitted for: ${it.getOrNull()?.roomId}") }
val memberIdsChangeObservable = summaryObservable
.map {
it.getOrNull()?.let { roomSummary ->
if (roomSummary.isEncrypted) {
// Return the list of other users
roomSummary.otherMemberIds + listOf(session.myUserId)
} else {
// Return an empty list, the room is not encrypted
emptyList()
}
}.orEmpty()
}.distinctUntilChanged()
.doOnNext { Timber.v("RX: memberIds emitted. Size: ${it.size}") }
// Observe the device info of the users in the room
val cryptoDeviceInfoObservable = memberIdsChangeObservable
.switchMap { membersIds ->
session.getLiveCryptoDeviceInfo(membersIds)
.asObservable()
.map {
// If any key change, emit the userIds list
membersIds
}
.startWith(membersIds)
.doOnNext { Timber.v("RX: CryptoDeviceInfo emitted. Size: ${it.size}") }
}
.doOnNext { Timber.v("RX: cryptoDeviceInfo emitted 2. Size: ${it.size}") }
val roomEncryptionTrustLevelObservable = cryptoDeviceInfoObservable
.map { userIds ->
if (userIds.isEmpty()) {
Optional<RoomEncryptionTrustLevel>(null)
} else {
session.getCrossSigningService().getTrustLevelForUsers(userIds).toOptional()
}
}
.doOnNext { Timber.v("RX: roomEncryptionTrustLevel emitted: ${it.getOrNull()?.name}") }
return Observable
.combineLatest<Optional<RoomSummary>, Optional<RoomEncryptionTrustLevel>, Optional<RoomSummary>>(
summaryObservable,
roomEncryptionTrustLevelObservable,
BiFunction { summary, level ->
summary.getOrNull()?.copy(
roomEncryptionTrustLevel = level.getOrNull()
).toOptional()
}
)
.doOnNext { Timber.v("RX: final room summary emitted for ${it.getOrNull()?.roomId}") }
.startWithCallable { room.roomSummary().toOptional() }
}
fun liveRoomMembers(queryParams: RoomMemberQueryParams): Observable<List<RoomMemberSummary>> {
val roomMembersObservable = room.getRoomMembersLive(queryParams).asObservable()
.startWith(room.getRoomMembers(queryParams))
.doOnNext { Timber.v("RX: room members emitted. Size: ${it.size}") }
// TODO Do it only for room members of the room (switchMap)
val cryptoDeviceInfoObservable = session.getLiveCryptoDeviceInfo().asObservable()
.startWith(emptyList<CryptoDeviceInfo>())
.doOnNext { Timber.v("RX: cryptoDeviceInfo emitted. Size: ${it.size}") }
return Observable
.combineLatest<List<RoomMemberSummary>, List<CryptoDeviceInfo>, List<RoomMemberSummary>>(
roomMembersObservable,
cryptoDeviceInfoObservable,
BiFunction { summaries, _ ->
summaries.map {
if (room.isEncrypted()) {
it.copy(
// Get the trust level of a virtual room with only this user
userEncryptionTrustLevel = session.getCrossSigningService().getTrustLevelForUsers(listOf(it.userId))
)
} else {
it
}
}
}
)
.doOnNext { Timber.v("RX: final room members emitted. Size: ${it.size}") }
return room.getRoomMembersLive(queryParams).asObservable()
.startWithCallable {
room.getRoomMembers(queryParams)
}
}
fun liveAnnotationSummary(eventId: String): Observable<Optional<EventAnnotationsSummary>> {
return room.getEventAnnotationsSummaryLive(eventId).asObservable()
.startWith(room.getEventAnnotationsSummary(eventId).toOptional())
.startWithCallable {
room.getEventAnnotationsSummary(eventId).toOptional()
}
}
fun liveTimelineEvent(eventId: String): Observable<Optional<TimelineEvent>> {
return room.getTimeLineEventLive(eventId).asObservable()
.startWith(room.getTimeLineEvent(eventId).toOptional())
.startWithCallable {
room.getTimeLineEvent(eventId).toOptional()
}
}
fun liveStateEvent(eventType: String): Observable<Optional<Event>> {
return room.getStateEventLive(eventType).asObservable()
.startWith(room.getStateEvent(eventType).toOptional())
fun liveStateEvent(eventType: String, stateKey: String): Observable<Optional<Event>> {
return room.getStateEventLive(eventType, stateKey).asObservable()
.startWithCallable {
room.getStateEvent(eventType, stateKey).toOptional()
}
}
fun liveReadMarker(): Observable<Optional<String>> {
@ -170,6 +97,6 @@ class RxRoom(private val room: Room, private val session: Session) {
}
}
fun Room.rx(session: Session): RxRoom {
return RxRoom(this, session)
fun Room.rx(): RxRoom {
return RxRoom(this)
}

View file

@ -33,48 +33,28 @@ import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.functions.BiFunction
import timber.log.Timber
class RxSession(private val session: Session) {
fun liveRoomSummaries(queryParams: RoomSummaryQueryParams): Observable<List<RoomSummary>> {
val summariesObservable = session.getRoomSummariesLive(queryParams).asObservable()
.startWith(session.getRoomSummaries(queryParams))
.doOnNext { Timber.v("RX: summaries emitted: size: ${it.size}") }
val cryptoDeviceInfoObservable = session.getLiveCryptoDeviceInfo().asObservable()
.startWith(emptyList<CryptoDeviceInfo>())
.doOnNext { Timber.v("RX: crypto device info emitted: size: ${it.size}") }
return Observable
.combineLatest<List<RoomSummary>, List<CryptoDeviceInfo>, List<RoomSummary>>(
summariesObservable,
cryptoDeviceInfoObservable,
BiFunction { summaries, _ ->
summaries.map {
if (it.isEncrypted) {
it.copy(
roomEncryptionTrustLevel = session.getCrossSigningService()
.getTrustLevelForUsers(it.otherMemberIds + session.myUserId)
)
} else {
it
}
}
}
)
.doOnNext { Timber.d("RX: final summaries emitted: size: ${it.size}") }
return session.getRoomSummariesLive(queryParams).asObservable()
.startWithCallable {
session.getRoomSummaries(queryParams)
}
}
fun liveGroupSummaries(queryParams: GroupSummaryQueryParams): Observable<List<GroupSummary>> {
return session.getGroupSummariesLive(queryParams).asObservable()
.startWith(session.getGroupSummaries(queryParams))
.startWithCallable {
session.getGroupSummaries(queryParams)
}
}
fun liveBreadcrumbs(): Observable<List<RoomSummary>> {
return session.getBreadcrumbsLive().asObservable()
.startWith(session.getBreadcrumbs())
.startWithCallable {
session.getBreadcrumbs()
}
}
fun liveSyncState(): Observable<SyncState> {
@ -87,7 +67,9 @@ class RxSession(private val session: Session) {
fun liveUser(userId: String): Observable<Optional<User>> {
return session.getUserLive(userId).asObservable()
.startWith(session.getUser(userId).toOptional())
.startWithCallable {
session.getUser(userId).toOptional()
}
}
fun liveUsers(): Observable<List<User>> {
@ -128,12 +110,16 @@ class RxSession(private val session: Session) {
}
fun liveUserCryptoDevices(userId: String): Observable<List<CryptoDeviceInfo>> {
return session.getLiveCryptoDeviceInfo(userId).asObservable()
return session.getLiveCryptoDeviceInfo(userId).asObservable().startWithCallable {
session.getCryptoDeviceInfo(userId)
}
}
fun liveCrossSigningInfo(userId: String): Observable<Optional<MXCrossSigningInfo>> {
return session.getCrossSigningService().getLiveCrossSigningKeys(userId).asObservable()
.startWith(session.getCrossSigningService().getUserCrossSigningKeys(userId).toOptional())
.startWithCallable {
session.getCrossSigningService().getUserCrossSigningKeys(userId).toOptional()
}
}
}

View file

@ -92,10 +92,11 @@ dependencies {
def arrow_version = "0.8.2"
def moshi_version = '1.8.0'
def lifecycle_version = '2.1.0'
def lifecycle_version = '2.2.0'
def arch_version = '2.1.0'
def coroutines_version = "1.3.2"
def markwon_version = '3.1.0'
def daggerVersion = '2.24'
def daggerVersion = '2.25.4'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
@ -104,14 +105,13 @@ dependencies {
implementation "androidx.appcompat:appcompat:1.1.0"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
// Network
implementation 'com.squareup.retrofit2:retrofit:2.6.2'
implementation 'com.squareup.retrofit2:converter-moshi:2.6.2'
implementation 'com.squareup.okhttp3:okhttp:4.2.2'
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2'
implementation 'com.novoda:merlin:1.2.0'
implementation "com.squareup.moshi:moshi-adapters:$moshi_version"
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
@ -125,7 +125,7 @@ dependencies {
kapt 'dk.ilios:realmfieldnameshelper:1.1.1'
// Work
implementation "androidx.work:work-runtime-ktx:2.3.0-beta02"
implementation "androidx.work:work-runtime-ktx:2.3.0"
// FP
implementation "io.arrow-kt:arrow-core:$arrow_version"
@ -167,7 +167,7 @@ dependencies {
androidTestImplementation 'org.amshove.kluent:kluent-android:1.44'
// Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281
androidTestImplementation 'io.mockk:mockk-android:1.9.2.kotlin12'
androidTestImplementation "androidx.arch.core:core-testing:$lifecycle_version"
androidTestImplementation "androidx.arch.core:core-testing:$arch_version"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
}

View file

@ -117,6 +117,10 @@ class CommonTestHelper(context: Context) {
override fun onTimelineFailure(throwable: Throwable) {
}
override fun onNewTimelineEvents(eventIds: List<String>) {
// noop
}
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
// TODO Count only new messages?
if (snapshot.count { it.root.type == EventType.MESSAGE } == nbOfMessages) {

View file

@ -74,7 +74,7 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) {
val room = aliceSession.getRoom(roomId!!)!!
val lock2 = CountDownLatch(1)
room.enableEncryptionWithAlgorithm(MXCRYPTO_ALGORITHM_MEGOLM, object : TestMatrixCallback<Unit>(lock2) {})
room.enableEncryption(callback = TestMatrixCallback(lock2))
mTestHelper.await(lock2)
return CryptoTestData(aliceSession, roomId!!)
@ -241,6 +241,11 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) {
val bobEventsListener = object : Timeline.Listener {
override fun onTimelineFailure(throwable: Throwable) {
// noop
}
override fun onNewTimelineEvents(eventIds: List<String>) {
// noop
}
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {

View file

@ -20,15 +20,15 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.InstrumentedTest
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.api.session.room.send.SendState
import im.vector.matrix.android.internal.database.helper.addTimelineEvent
import im.vector.matrix.android.internal.database.helper.merge
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.SessionRealmModule
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeListOfEvents
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeMessageEvent
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeRoomMemberEvent
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.kotlin.createObject
@ -58,8 +58,11 @@ internal class ChunkEntityTest : InstrumentedTest {
fun add_shouldAdd_whenNotAlreadyIncluded() {
monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject()
val fakeEvent = createFakeMessageEvent()
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED).let {
realm.copyToRealmOrUpdate(it)
}
chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
chunk.timelineEvents.size shouldEqual 1
}
}
@ -68,65 +71,23 @@ internal class ChunkEntityTest : InstrumentedTest {
fun add_shouldNotAdd_whenAlreadyIncluded() {
monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject()
val fakeEvent = createFakeMessageEvent()
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED).let {
realm.copyToRealmOrUpdate(it)
}
chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
chunk.timelineEvents.size shouldEqual 1
}
}
@Test
fun add_shouldStateIndexIncremented_whenStateEventIsAddedForward() {
monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject()
val fakeEvent = createFakeRoomMemberEvent()
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 1
}
}
@Test
fun add_shouldStateIndexNotIncremented_whenNoStateEventIsAdded() {
monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject()
val fakeEvent = createFakeMessageEvent()
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 0
}
}
@Test
fun addAll_shouldStateIndexIncremented_whenStateEventsAreAddedForward() {
monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject()
val fakeEvents = createFakeListOfEvents(30)
val numberOfStateEvents = fakeEvents.filter { it.isStateEvent() }.size
chunk.addAll("roomId", fakeEvents, PaginationDirection.FORWARDS)
chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual numberOfStateEvents
}
}
@Test
fun addAll_shouldStateIndexDecremented_whenStateEventsAreAddedBackward() {
monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject()
val fakeEvents = createFakeListOfEvents(30)
val numberOfStateEvents = fakeEvents.filter { it.isStateEvent() }.size
val lastIsState = fakeEvents.last().isStateEvent()
val expectedStateIndex = if (lastIsState) -numberOfStateEvents + 1 else -numberOfStateEvents
chunk.addAll("roomId", fakeEvents, PaginationDirection.BACKWARDS)
chunk.lastStateIndex(PaginationDirection.BACKWARDS) shouldEqual expectedStateIndex
}
}
@Test
fun merge_shouldAddEvents_whenMergingBackward() {
monarchy.runTransactionSync { realm ->
val chunk1: ChunkEntity = realm.createObject()
val chunk2: ChunkEntity = realm.createObject()
chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS)
chunk1.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk2.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk1.merge(ROOM_ID, chunk2, PaginationDirection.BACKWARDS)
chunk1.timelineEvents.size shouldEqual 60
}
}
@ -140,9 +101,9 @@ internal class ChunkEntityTest : InstrumentedTest {
val eventsForChunk2 = eventsForChunk1 + createFakeListOfEvents(10)
chunk1.isLastForward = true
chunk2.isLastForward = false
chunk1.addAll("roomId", eventsForChunk1, PaginationDirection.FORWARDS)
chunk2.addAll("roomId", eventsForChunk2, PaginationDirection.BACKWARDS)
chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS)
chunk1.addAll(ROOM_ID, eventsForChunk1, PaginationDirection.FORWARDS)
chunk2.addAll(ROOM_ID, eventsForChunk2, PaginationDirection.BACKWARDS)
chunk1.merge(ROOM_ID, chunk2, PaginationDirection.BACKWARDS)
chunk1.timelineEvents.size shouldEqual 40
chunk1.isLastForward.shouldBeTrue()
}
@ -155,9 +116,9 @@ internal class ChunkEntityTest : InstrumentedTest {
val chunk2: ChunkEntity = realm.createObject()
val prevToken = "prev_token"
chunk1.prevToken = prevToken
chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk1.merge("roomId", chunk2, PaginationDirection.FORWARDS)
chunk1.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk2.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk1.merge(ROOM_ID, chunk2, PaginationDirection.FORWARDS)
chunk1.prevToken shouldEqual prevToken
}
}
@ -169,19 +130,25 @@ internal class ChunkEntityTest : InstrumentedTest {
val chunk2: ChunkEntity = realm.createObject()
val nextToken = "next_token"
chunk1.nextToken = nextToken
chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS)
chunk1.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk2.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk1.merge(ROOM_ID, chunk2, PaginationDirection.BACKWARDS)
chunk1.nextToken shouldEqual nextToken
}
}
private fun ChunkEntity.addAll(roomId: String,
events: List<Event>,
direction: PaginationDirection,
stateIndexOffset: Int = 0) {
direction: PaginationDirection) {
events.forEach { event ->
add(roomId, event, direction, stateIndexOffset)
val fakeEvent = event.toEntity(roomId, SendState.SYNCED).let {
realm.copyToRealmOrUpdate(it)
}
addTimelineEvent(roomId, fakeEvent, direction, emptyMap())
}
}
companion object {
private const val ROOM_ID = "roomId"
}
}

View file

@ -23,6 +23,7 @@ import androidx.work.WorkManager
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.api.crypto.MXCryptoConfig
import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
@ -35,7 +36,8 @@ import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
data class MatrixConfiguration(
val applicationFlavor: String = "Default-application-flavor"
val applicationFlavor: String = "Default-application-flavor",
val cryptoConfig: MXCryptoConfig = MXCryptoConfig()
) {
interface Provider {
@ -57,12 +59,11 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
init {
Monarchy.init(context)
DaggerMatrixComponent.factory().create(context).inject(this)
DaggerMatrixComponent.factory().create(context, matrixConfiguration).inject(this)
if (context.applicationContext !is Configuration.Provider) {
WorkManager.initialize(context, Configuration.Builder().build())
}
ProcessLifecycleOwner.get().lifecycle.addObserver(backgroundDetectionObserver)
userAgentHolder.setApplicationFlavor(matrixConfiguration.applicationFlavor)
}
fun getUserAgent() = userAgentHolder.userAgent

View file

@ -38,3 +38,8 @@ interface MatrixCallback<in T> {
// no-op
}
}
/**
* Basic no op implementation
*/
class NoOpMatrixCallback<T>: MatrixCallback<T>

View file

@ -14,14 +14,14 @@
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto
package im.vector.matrix.android.api.crypto
/**
* Class to define the parameters used to customize or configure the end-to-end crypto.
*/
data class MXCryptoConfig(
// Tell whether the encryption of the event content is enabled for the invited members.
// By default, we encrypt messages only for the joined members.
// The encryption for the invited members will be blocked if the history visibility is "joined".
var enableEncryptionForInvitedMembers: Boolean = false
// SDK clients can disable this by settings it to false.
// Note that the encryption for the invited members will be blocked if the history visibility is "joined".
var enableEncryptionForInvitedMembers: Boolean = true
)

View file

@ -15,30 +15,32 @@
*/
package im.vector.matrix.android.api.pushrules
import im.vector.matrix.android.api.session.events.model.Event
abstract class Condition(val kind: Kind) {
enum class Kind(val value: String) {
event_match("event_match"),
contains_display_name("contains_display_name"),
room_member_count("room_member_count"),
sender_notification_permission("sender_notification_permission"),
UNRECOGNIZE("");
EventMatch("event_match"),
ContainsDisplayName("contains_display_name"),
RoomMemberCount("room_member_count"),
SenderNotificationPermission("sender_notification_permission"),
Unrecognised("");
companion object {
fun fromString(value: String): Kind {
return when (value) {
"event_match" -> event_match
"contains_display_name" -> contains_display_name
"room_member_count" -> room_member_count
"sender_notification_permission" -> sender_notification_permission
else -> UNRECOGNIZE
"event_match" -> EventMatch
"contains_display_name" -> ContainsDisplayName
"room_member_count" -> RoomMemberCount
"sender_notification_permission" -> SenderNotificationPermission
else -> Unrecognised
}
}
}
}
abstract fun isSatisfied(conditionResolver: ConditionResolver): Boolean
abstract fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean
open fun technicalDescription(): String {
return "Kind: $kind"

View file

@ -15,14 +15,22 @@
*/
package im.vector.matrix.android.api.pushrules
import im.vector.matrix.android.api.session.events.model.Event
/**
* Acts like a visitor on Conditions.
* This class as all required context needed to evaluate rules
*/
interface ConditionResolver {
fun resolveEventMatchCondition(event: Event,
condition: EventMatchCondition): Boolean
fun resolveEventMatchCondition(eventMatchCondition: EventMatchCondition): Boolean
fun resolveRoomMemberCountCondition(roomMemberCountCondition: RoomMemberCountCondition): Boolean
fun resolveSenderNotificationPermissionCondition(senderNotificationPermissionCondition: SenderNotificationPermissionCondition): Boolean
fun resolveContainsDisplayNameCondition(containsDisplayNameCondition: ContainsDisplayNameCondition) : Boolean
fun resolveRoomMemberCountCondition(event: Event,
condition: RoomMemberCountCondition): Boolean
fun resolveSenderNotificationPermissionCondition(event: Event,
condition: SenderNotificationPermissionCondition): Boolean
fun resolveContainsDisplayNameCondition(event: Event,
condition: ContainsDisplayNameCondition): Boolean
}

View file

@ -21,10 +21,10 @@ import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import timber.log.Timber
class ContainsDisplayNameCondition : Condition(Kind.contains_display_name) {
class ContainsDisplayNameCondition : Condition(Kind.ContainsDisplayName) {
override fun isSatisfied(conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveContainsDisplayNameCondition(this)
override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveContainsDisplayNameCondition(event, this)
}
override fun technicalDescription(): String {

View file

@ -19,10 +19,20 @@ import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.internal.di.MoshiProvider
import timber.log.Timber
class EventMatchCondition(val key: String, val pattern: String) : Condition(Kind.event_match) {
class EventMatchCondition(
/**
* The dot-separated field of the event to match, e.g. content.body
*/
val key: String,
/**
* The glob-style pattern to match against. Patterns with no special glob characters should
* be treated as having asterisks prepended and appended when testing the condition.
*/
val pattern: String
) : Condition(Kind.EventMatch) {
override fun isSatisfied(conditionResolver: ConditionResolver) : Boolean {
return conditionResolver.resolveEventMatchCondition(this)
override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveEventMatchCondition(event, this)
}
override fun technicalDescription(): String {

View file

@ -16,25 +16,32 @@
package im.vector.matrix.android.api.pushrules
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.internal.session.room.RoomGetter
import timber.log.Timber
private val regex = Regex("^(==|<=|>=|<|>)?(\\d*)$")
class RoomMemberCountCondition(val iz: String) : Condition(Kind.room_member_count) {
class RoomMemberCountCondition(
/**
* A decimal integer optionally prefixed by one of ==, <, >, >= or <=.
* A prefix of < matches rooms where the member count is strictly less than the given number and so forth.
* If no prefix is present, this parameter defaults to ==.
*/
val iz: String
) : Condition(Kind.RoomMemberCount) {
override fun isSatisfied(conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveRoomMemberCountCondition(this)
override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveRoomMemberCountCondition(event, this)
}
override fun technicalDescription(): String {
return "Room member count is $iz"
}
fun isSatisfied(event: Event, session: RoomService?): Boolean {
// sanity check^
internal fun isSatisfied(event: Event, roomGetter: RoomGetter): Boolean {
// sanity checks
val roomId = event.roomId ?: return false
val room = session?.getRoom(roomId) ?: return false
val room = roomGetter.getRoom(roomId) ?: return false
// Parse the is field into prefix and number the first time
val (prefix, count) = parseIsField() ?: return false
@ -59,7 +66,7 @@ class RoomMemberCountCondition(val iz: String) : Condition(Kind.room_member_coun
val (prefix, count) = match.destructured
return prefix to count.toInt()
} catch (t: Throwable) {
Timber.d(t)
Timber.e(t, "Unable to parse 'is' field")
}
return null
}

View file

@ -19,10 +19,18 @@ import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper
class SenderNotificationPermissionCondition(val key: String) : Condition(Kind.sender_notification_permission) {
class SenderNotificationPermissionCondition(
/**
* A string that determines the power level the sender must have to trigger notifications of a given type,
* such as room. Refer to the m.room.power_levels event schema for information about what the defaults are
* and how to interpret the event. The key is used to look up the power level required to send a notification
* type from the notifications object in the power level event content.
*/
val key: String
) : Condition(Kind.SenderNotificationPermission) {
override fun isSatisfied(conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveSenderNotificationPermissionCondition(this)
override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveSenderNotificationPermissionCondition(event, this)
}
override fun technicalDescription(): String {

View file

@ -22,7 +22,7 @@ import com.squareup.moshi.JsonClass
* All push rulesets for a user.
*/
@JsonClass(generateAdapter = true)
data class GetPushRulesResponse(
internal data class GetPushRulesResponse(
/**
* Global rules, account level applying to all devices
*/

View file

@ -17,7 +17,11 @@ package im.vector.matrix.android.api.pushrules.rest
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.pushrules.*
import im.vector.matrix.android.api.pushrules.Condition
import im.vector.matrix.android.api.pushrules.ContainsDisplayNameCondition
import im.vector.matrix.android.api.pushrules.EventMatchCondition
import im.vector.matrix.android.api.pushrules.RoomMemberCountCondition
import im.vector.matrix.android.api.pushrules.SenderNotificationPermissionCondition
import timber.log.Timber
@JsonClass(generateAdapter = true)
@ -30,13 +34,13 @@ data class PushCondition(
/**
* Required for event_match conditions. The dot- separated field of the event to match.
*/
val key: String? = null,
/**
*Required for event_match conditions.
*/
/**
* Required for event_match conditions.
*/
val pattern: String? = null,
/**
* Required for room_member_count conditions.
* A decimal integer optionally prefixed by one of, ==, <, >, >= or <=.
@ -47,30 +51,35 @@ data class PushCondition(
) {
fun asExecutableCondition(): Condition? {
return when (Condition.Kind.fromString(this.kind)) {
Condition.Kind.event_match -> {
if (this.key != null && this.pattern != null) {
return when (Condition.Kind.fromString(kind)) {
Condition.Kind.EventMatch -> {
if (key != null && pattern != null) {
EventMatchCondition(key, pattern)
} else {
Timber.e("Malformed Event match condition")
null
}
}
Condition.Kind.contains_display_name -> {
Condition.Kind.ContainsDisplayName -> {
ContainsDisplayNameCondition()
}
Condition.Kind.room_member_count -> {
if (this.iz.isNullOrBlank()) {
Condition.Kind.RoomMemberCount -> {
if (iz.isNullOrEmpty()) {
Timber.e("Malformed ROOM_MEMBER_COUNT condition")
null
} else {
RoomMemberCountCondition(this.iz)
RoomMemberCountCondition(iz)
}
}
Condition.Kind.sender_notification_permission -> {
this.key?.let { SenderNotificationPermissionCondition(it) }
Condition.Kind.SenderNotificationPermission -> {
if (key == null) {
Timber.e("Malformed Sender Notification Permission condition")
null
} else {
SenderNotificationPermissionCondition(key)
}
}
Condition.Kind.UNRECOGNIZE -> {
Condition.Kind.Unrecognised -> {
Timber.e("Unknown kind $kind")
null
}

View file

@ -18,7 +18,7 @@ package im.vector.matrix.android.api.pushrules.rest
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class Ruleset(
internal data class Ruleset(
val content: List<PushRule>? = null,
val override: List<PushRule>? = null,
val room: List<PushRule>? = null,

View file

@ -18,7 +18,6 @@ package im.vector.matrix.android.api.session.crypto.crosssigning
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustResult
import im.vector.matrix.android.internal.crypto.crosssigning.UserTrustResult
@ -63,6 +62,4 @@ interface CrossSigningService {
fun checkDeviceTrust(otherUserId: String,
otherDeviceId: String,
locallyTrusted: Boolean?): DeviceTrustResult
fun getTrustLevelForUsers(userIds: List<String>): RoomEncryptionTrustLevel
}

View file

@ -200,7 +200,7 @@ data class Event(
fun Event.isTextMessage(): Boolean {
return getClearType() == EventType.MESSAGE
&& when (getClearContent()?.toModel<MessageContent>()?.type) {
&& when (getClearContent()?.toModel<MessageContent>()?.msgType) {
MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_EMOTE,
MessageType.MSGTYPE_NOTICE -> true
@ -210,7 +210,7 @@ fun Event.isTextMessage(): Boolean {
fun Event.isImageMessage(): Boolean {
return getClearType() == EventType.MESSAGE
&& when (getClearContent()?.toModel<MessageContent>()?.type) {
&& when (getClearContent()?.toModel<MessageContent>()?.msgType) {
MessageType.MSGTYPE_IMAGE -> true
else -> false
}

View file

@ -17,6 +17,7 @@
package im.vector.matrix.android.api.session.room.crypto
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
interface RoomCryptoService {
@ -26,5 +27,9 @@ interface RoomCryptoService {
fun shouldEncryptForInvitedMembers(): Boolean
fun enableEncryptionWithAlgorithm(algorithm: String, callback: MatrixCallback<Unit>)
/**
* Enable encryption of the room
*/
fun enableEncryption(algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM,
callback: MatrixCallback<Unit>)
}

View file

@ -18,9 +18,28 @@ package im.vector.matrix.android.api.session.room.model
import com.squareup.moshi.Json
/**
* Ref: https://matrix.org/docs/spec/client_server/latest#room-history-visibility
*/
enum class RoomHistoryVisibility {
/**
* All events while this is the m.room.history_visibility value may be shared by any
* participating homeserver with anyone, regardless of whether they have ever joined the room.
*/
@Json(name = "world_readable") WORLD_READABLE,
/**
* Previous events are always accessible to newly joined members. All events in the
* room are accessible, even those sent when the member was not a part of the room.
*/
@Json(name = "shared") SHARED,
/**
* Events are accessible to newly joined members from the point they were invited onwards.
* Events stop being accessible when the member's state changes to something other than invite or join.
*/
@Json(name = "invited") INVITED,
@Json(name = "joined") JOINED,
@Json(name = "world_readable") WORLD_READABLE
/**
* Events are accessible to newly joined members from the point they joined the room onwards.
* Events stop being accessible when the member's state changes to something other than join.
*/
@Json(name = "joined") JOINED
}

View file

@ -16,8 +16,6 @@
package im.vector.matrix.android.api.session.room.model
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
/**
* Class representing a simplified version of EventType.STATE_ROOM_MEMBER state event content
*/
@ -25,7 +23,5 @@ data class RoomMemberSummary constructor(
val membership: Membership,
val userId: String,
val displayName: String? = null,
val avatarUrl: String? = null,
// TODO Warning: Will not be populated if not using RxRoom
val userEncryptionTrustLevel: RoomEncryptionTrustLevel? = null
val avatarUrl: String? = null
)

View file

@ -25,9 +25,9 @@ import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
@JsonClass(generateAdapter = true)
data class MessageAudioContent(
/**
* Not documented
* Required. Must be 'm.audio'.
*/
@Json(name = "msgtype") override val type: String,
@Json(name = "msgtype") override val msgType: String,
/**
* Required. A description of the audio e.g. 'Bee Gees - Stayin' Alive', or some kind of content description for accessibility e.g. 'audio attachment'.
@ -40,7 +40,7 @@ data class MessageAudioContent(
@Json(name = "info") val audioInfo: AudioInfo? = null,
/**
* Required. Required if the file is not encrypted. The URL (typically MXC URI) to the audio clip.
* Required if the file is not encrypted. The URL (typically MXC URI) to the audio clip.
*/
@Json(name = "url") override val url: String? = null,

View file

@ -20,8 +20,7 @@ import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
interface MessageContent {
// TODO Rename to msgType
val type: String
val msgType: String
val body: String
val relatesTo: RelationDefaultContent?
val newContent: Content?

View file

@ -23,7 +23,7 @@ import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultC
@JsonClass(generateAdapter = true)
data class MessageDefaultContent(
@Json(name = "msgtype") override val type: String,
@Json(name = "msgtype") override val msgType: String,
@Json(name = "body") override val body: String,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null

View file

@ -23,10 +23,26 @@ import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultC
@JsonClass(generateAdapter = true)
data class MessageEmoteContent(
@Json(name = "msgtype") override val type: String,
/**
* Required. Must be 'm.emote'.
*/
@Json(name = "msgtype") override val msgType: String,
/**
* Required. The emote action to perform.
*/
@Json(name = "body") override val body: String,
/**
* The format used in the formatted_body. Currently only org.matrix.custom.html is supported.
*/
@Json(name = "format") val format: String? = null,
/**
* The formatted version of the body. This is required if format is specified.
*/
@Json(name = "formatted_body") val formattedBody: String? = null,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null
) : MessageContent

View file

@ -23,10 +23,13 @@ import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
*/
interface MessageEncryptedContent : MessageContent {
/**
* Required. Required if the file is unencrypted. The URL (typically MXC URI) to the image.
* Required if the file is unencrypted. The URL (typically MXC URI) to the image.
*/
val url: String?
/**
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
*/
val encryptedFileInfo: EncryptedFileInfo?
}

View file

@ -26,9 +26,9 @@ import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
@JsonClass(generateAdapter = true)
data class MessageFileContent(
/**
* Not documented
* Required. Must be 'm.file'.
*/
@Json(name = "msgtype") override val type: String,
@Json(name = "msgtype") override val msgType: String,
/**
* Required. A human-readable description of the file. This is recommended to be the filename of the original upload.
@ -46,13 +46,16 @@ data class MessageFileContent(
@Json(name = "info") val info: FileInfo? = null,
/**
* Required. Required if the file is unencrypted. The URL (typically MXC URI) to the file.
* Required if the file is unencrypted. The URL (typically MXC URI) to the file.
*/
@Json(name = "url") override val url: String? = null,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null,
/**
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
*/
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
) : MessageEncryptedContent {

View file

@ -1,5 +1,5 @@
/*
* Copyright 2019 New Vector Ltd
* 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.
@ -14,10 +14,8 @@
* limitations under the License.
*/
package im.vector.riotx.features.home.room.detail
package im.vector.matrix.android.api.session.room.model.message
data class FileTooBigError(
val filename: String,
val fileSizeInBytes: Long,
val homeServerLimitInBytes: Long
)
object MessageFormat {
const val FORMAT_MATRIX_HTML = "org.matrix.custom.html"
}

View file

@ -27,7 +27,7 @@ data class MessageImageContent(
/**
* Required. Must be 'm.image'.
*/
@Json(name = "msgtype") override val type: String,
@Json(name = "msgtype") override val msgType: String,
/**
* Required. A textual representation of the image. This could be the alt text of the image, the filename of the image,
@ -41,7 +41,7 @@ data class MessageImageContent(
@Json(name = "info") override val info: ImageInfo? = null,
/**
* Required. Required if the file is unencrypted. The URL (typically MXC URI) to the image.
* Required if the file is unencrypted. The URL (typically MXC URI) to the image.
*/
@Json(name = "url") override val url: String? = null,

View file

@ -24,9 +24,9 @@ import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultC
@JsonClass(generateAdapter = true)
data class MessageLocationContent(
/**
* Not documented
* Required. Must be 'm.location'.
*/
@Json(name = "msgtype") override val type: String,
@Json(name = "msgtype") override val msgType: String,
/**
* Required. A description of the location e.g. 'Big Ben, London, UK', or some kind of content description for accessibility e.g. 'location attachment'.

View file

@ -23,10 +23,26 @@ import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultC
@JsonClass(generateAdapter = true)
data class MessageNoticeContent(
@Json(name = "msgtype") override val type: String,
/**
* Required. Must be 'm.notice'.
*/
@Json(name = "msgtype") override val msgType: String,
/**
* Required. The notice text to send.
*/
@Json(name = "body") override val body: String,
/**
* The format used in the formatted_body. Currently only org.matrix.custom.html is supported.
*/
@Json(name = "format") val format: String? = null,
/**
* The formatted version of the body. This is required if format is specified.
*/
@Json(name = "formatted_body") val formattedBody: String? = null,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null
) : MessageContent

View file

@ -12,7 +12,6 @@
* 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.message
@ -28,7 +27,7 @@ data class MessageStickerContent(
/**
* Set in local, not from server
*/
override val type: String = MessageType.MSGTYPE_STICKER_LOCAL,
override val msgType: String = MessageType.MSGTYPE_STICKER_LOCAL,
/**
* Required. A textual representation of the image. This could be the alt text of the image, the filename of the image,
@ -42,7 +41,7 @@ data class MessageStickerContent(
@Json(name = "info") override val info: ImageInfo? = null,
/**
* Required. Required if the file is unencrypted. The URL (typically MXC URI) to the image.
* Required if the file is unencrypted. The URL (typically MXC URI) to the image.
*/
@Json(name = "url") override val url: String? = null,

View file

@ -23,10 +23,26 @@ import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultC
@JsonClass(generateAdapter = true)
data class MessageTextContent(
@Json(name = "msgtype") override val type: String,
/**
* Required. Must be 'm.text'.
*/
@Json(name = "msgtype") override val msgType: String,
/**
* Required. The body of the message.
*/
@Json(name = "body") override val body: String,
/**
* The format used in the formatted_body. Currently only org.matrix.custom.html is supported.
*/
@Json(name = "format") val format: String? = null,
/**
* The formatted version of the body. This is required if format is specified.
*/
@Json(name = "formatted_body") val formattedBody: String? = null,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null
) : MessageContent

View file

@ -17,7 +17,6 @@
package im.vector.matrix.android.api.session.room.model.message
object MessageType {
const val MSGTYPE_TEXT = "m.text"
const val MSGTYPE_EMOTE = "m.emote"
const val MSGTYPE_NOTICE = "m.notice"
@ -27,7 +26,6 @@ object MessageType {
const val MSGTYPE_LOCATION = "m.location"
const val MSGTYPE_FILE = "m.file"
const val MSGTYPE_VERIFICATION_REQUEST = "m.key.verification.request"
const val FORMAT_MATRIX_HTML = "org.matrix.custom.html"
// Add, in local, a fake message type in order to StickerMessage can inherit Message class
// Because sticker isn't a message type but a event type without msgtype field
const val MSGTYPE_STICKER_LOCAL = "org.matrix.android.sdk.sticker"

View file

@ -24,7 +24,7 @@ import im.vector.matrix.android.internal.crypto.verification.VerificationInfoReq
@JsonClass(generateAdapter = true)
data class MessageVerificationRequestContent(
@Json(name = "msgtype") override val type: String = MessageType.MSGTYPE_VERIFICATION_REQUEST,
@Json(name = "msgtype") override val msgType: String = MessageType.MSGTYPE_VERIFICATION_REQUEST,
@Json(name = "body") override val body: String,
@Json(name = "from_device") override val fromDevice: String?,
@Json(name = "methods") override val methods: List<String>,

View file

@ -27,7 +27,7 @@ data class MessageVideoContent(
/**
* Required. Must be 'm.video'.
*/
@Json(name = "msgtype") override val type: String,
@Json(name = "msgtype") override val msgType: String,
/**
* Required. A description of the video e.g. 'Gangnam style', or some kind of content description for accessibility e.g. 'video attachment'.
@ -40,7 +40,7 @@ data class MessageVideoContent(
@Json(name = "info") val videoInfo: VideoInfo? = null,
/**
* Required. Required if the file is unencrypted. The URL (typically MXC URI) to the video clip.
* Required if the file is unencrypted. The URL (typically MXC URI) to the video clip.
*/
@Json(name = "url") override val url: String? = null,

View file

@ -26,10 +26,16 @@ import im.vector.matrix.android.api.util.Optional
*/
interface ReadService {
enum class MarkAsReadParams {
READ_RECEIPT,
READ_MARKER,
BOTH
}
/**
* Force the read marker to be set on the latest event.
*/
fun markAllAsRead(callback: MatrixCallback<Unit>)
fun markAsRead(params: MarkAsReadParams = MarkAsReadParams.BOTH, callback: MatrixCallback<Unit>)
/**
* Set the read receipt on the event with provided eventId.

View file

@ -17,18 +17,20 @@
package im.vector.matrix.android.api.session.room.send
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
interface DraftService {
/**
* Save or update a draft to the room
*/
fun saveDraft(draft: UserDraft)
fun saveDraft(draft: UserDraft, callback: MatrixCallback<Unit>): Cancelable
/**
* Delete the last draft, basically just after sending the message
*/
fun deleteDraft()
fun deleteDraft(callback: MatrixCallback<Unit>): Cancelable
/**
* Return the current drafts if any, as a live data

View file

@ -28,12 +28,7 @@ interface StateService {
*/
fun updateTopic(topic: String, callback: MatrixCallback<Unit>)
/**
* Enable encryption of the room
*/
fun enableEncryption(algorithm: String, callback: MatrixCallback<Unit>)
fun getStateEvent(eventType: String, stateKey: String): Event?
fun getStateEvent(eventType: String): Event?
fun getStateEventLive(eventType: String): LiveData<Optional<Event>>
fun getStateEventLive(eventType: String, stateKey: String): LiveData<Optional<Event>>
}

View file

@ -112,6 +112,11 @@ interface Timeline {
* Called whenever an error we can't recover from occurred
*/
fun onTimelineFailure(throwable: Throwable)
/**
* Called when new events come through the sync
*/
fun onNewTimelineEvents(eventIds: List<String>)
}
/**

View file

@ -36,6 +36,7 @@ import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventConten
data class TimelineEvent(
val root: Event,
val localId: Long,
val eventId: String,
val displayIndex: Int,
val senderName: String?,
val isUniqueDisplayName: Boolean,

View file

@ -19,10 +19,11 @@ package im.vector.matrix.android.internal.crypto
import dagger.Binds
import dagger.Module
import dagger.Provides
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
import im.vector.matrix.android.internal.crypto.api.CryptoApi
import im.vector.matrix.android.internal.crypto.crosssigning.ComputeTrustTask
import im.vector.matrix.android.internal.crypto.crosssigning.DefaultComputeTrustTask
import im.vector.matrix.android.internal.crypto.crosssigning.DefaultCrossSigningService
import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask
@ -137,15 +138,6 @@ internal abstract class CryptoModule {
return RealmClearCacheTask(realmConfiguration)
}
@JvmStatic
@Provides
fun providesCryptoStore(@CryptoDatabase
realmConfiguration: RealmConfiguration, credentials: Credentials): IMXCryptoStore {
return RealmCryptoStore(
realmConfiguration,
credentials)
}
@JvmStatic
@Provides
@SessionScope
@ -159,13 +151,6 @@ internal abstract class CryptoModule {
fun providesRoomKeysAPI(retrofit: Retrofit): RoomKeysApi {
return retrofit.create(RoomKeysApi::class.java)
}
@JvmStatic
@Provides
@SessionScope
fun providesCryptoConfig(): MXCryptoConfig {
return MXCryptoConfig()
}
}
@Binds
@ -256,4 +241,10 @@ internal abstract class CryptoModule {
@Binds
abstract fun bindCrossSigningService(crossSigningService: DefaultCrossSigningService): CrossSigningService
@Binds
abstract fun bindCryptoStore(realmCryptoStore: RealmCryptoStore): IMXCryptoStore
@Binds
abstract fun bindComputeShieldTrustTask(defaultShieldTrustUpdater: DefaultComputeTrustTask): ComputeTrustTask
}

View file

@ -26,7 +26,9 @@ import com.squareup.moshi.Types
import com.zhuinden.monarchy.Monarchy
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.NoOpMatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.crypto.MXCryptoConfig
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.listeners.ProgressListener
import im.vector.matrix.android.api.session.crypto.CryptoService
@ -70,7 +72,7 @@ import im.vector.matrix.android.internal.crypto.tasks.UploadKeysTask
import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationService
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.query.where
import im.vector.matrix.android.internal.database.query.whereType
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.session.SessionScope
@ -116,7 +118,7 @@ internal class DefaultCryptoService @Inject constructor(
// Olm device
private val olmDevice: MXOlmDevice,
// Set of parameters used to configure/customize the end-to-end crypto.
private val cryptoConfig: MXCryptoConfig = MXCryptoConfig(),
private val mxCryptoConfig: MXCryptoConfig,
// Device list manager
private val deviceListManager: DeviceListManager,
// The key backup service.
@ -189,7 +191,7 @@ internal class DefaultCryptoService @Inject constructor(
this.callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
// bg refresh of crypto device
downloadKeys(listOf(credentials.userId), true, object : MatrixCallback<MXUsersDevicesMap<CryptoDeviceInfo>> {})
downloadKeys(listOf(credentials.userId), true, NoOpMatrixCallback())
callback.onSuccess(data)
}
@ -399,6 +401,7 @@ internal class DefaultCryptoService @Inject constructor(
null
}
}
override fun getCryptoDeviceInfo(userId: String): List<CryptoDeviceInfo> {
return cryptoStore.getUserDevices(userId)?.map { it.value } ?: emptyList()
}
@ -531,7 +534,7 @@ internal class DefaultCryptoService @Inject constructor(
*/
override fun isRoomEncrypted(roomId: String): Boolean {
val encryptionEvent = monarchy.fetchCopied { realm ->
EventEntity.where(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
.contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
.findFirst()
}
@ -545,8 +548,8 @@ internal class DefaultCryptoService @Inject constructor(
return cryptoStore.getUserDevices(userId)?.values?.toMutableList() ?: ArrayList()
}
fun isEncryptionEnabledForInvitedUser(): Boolean {
return cryptoConfig.enableEncryptionForInvitedMembers
private fun isEncryptionEnabledForInvitedUser(): Boolean {
return mxCryptoConfig.enableEncryptionForInvitedMembers
}
override fun getEncryptionAlgorithm(roomId: String): String? {
@ -779,7 +782,7 @@ internal class DefaultCryptoService @Inject constructor(
deviceListManager.startTrackingDeviceList(listOf(userId))
} else if (membership == Membership.INVITE
&& shouldEncryptForInvitedMembers(roomId)
&& cryptoConfig.enableEncryptionForInvitedMembers) {
&& isEncryptionEnabledForInvitedUser()) {
// track the deviceList for this invited user.
// Caution: there's a big edge case here in that federated servers do not
// know what other servers are in the room at the time they've been invited.

View file

@ -27,6 +27,9 @@ import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.DownloadKeysForUsersTask
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ -36,10 +39,12 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
private val olmDevice: MXOlmDevice,
private val syncTokenStore: SyncTokenStore,
private val credentials: Credentials,
private val downloadKeysForUsersTask: DownloadKeysForUsersTask) {
private val downloadKeysForUsersTask: DownloadKeysForUsersTask,
coroutineDispatchers: MatrixCoroutineDispatchers,
taskExecutor: TaskExecutor) {
interface UserDevicesUpdateListener {
fun onUsersDeviceUpdate(users: List<String>)
fun onUsersDeviceUpdate(userIds: List<String>)
}
private val deviceChangeListeners = mutableListOf<UserDevicesUpdateListener>()
@ -72,17 +77,19 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
private val notReadyToRetryHS = mutableSetOf<String>()
init {
var isUpdated = false
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
for ((userId, status) in deviceTrackingStatuses) {
if (TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == status || TRACKING_STATUS_UNREACHABLE_SERVER == status) {
// if a download was in progress when we got shut down, it isn't any more.
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
isUpdated = true
taskExecutor.executorScope.launch(coroutineDispatchers.crypto) {
var isUpdated = false
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
for ((userId, status) in deviceTrackingStatuses) {
if (TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == status || TRACKING_STATUS_UNREACHABLE_SERVER == status) {
// if a download was in progress when we got shut down, it isn't any more.
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
isUpdated = true
}
}
if (isUpdated) {
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
}
}
if (isUpdated) {
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
}
}
@ -320,7 +327,6 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
// al devices =
val models = response.deviceKeys?.get(userId)?.mapValues { entry -> CryptoInfoMapper.map(entry.value) }
Timber.v("## doKeyDownloadForUsers() : Got keys for $userId : $models")
if (!models.isNullOrEmpty()) {
val workingCopy = models.toMutableMap()

View file

@ -0,0 +1,78 @@
/*
* 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.crypto.crosssigning
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
import im.vector.matrix.android.api.extensions.orFalse
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject
internal interface ComputeTrustTask : Task<ComputeTrustTask.Params, RoomEncryptionTrustLevel> {
data class Params(
val userIds: List<String>
)
}
internal class DefaultComputeTrustTask @Inject constructor(
val cryptoStore: IMXCryptoStore
) : ComputeTrustTask {
override suspend fun execute(params: ComputeTrustTask.Params): RoomEncryptionTrustLevel {
val allTrustedUserIds = params.userIds
.filter { userId -> getUserCrossSigningKeys(userId)?.isTrusted() == true }
return if (allTrustedUserIds.isEmpty()) {
RoomEncryptionTrustLevel.Default
} else {
// If one of the verified user as an untrusted device -> warning
// If all devices of all verified users are trusted -> green
// else -> black
allTrustedUserIds
.mapNotNull { cryptoStore.getUserDeviceList(it) }
.flatten()
.let { allDevices ->
if (getMyCrossSigningKeys() != null) {
allDevices.any { !it.trustLevel?.crossSigningVerified.orFalse() }
} else {
// Legacy method
allDevices.any { !it.isVerified }
}
}
.let { hasWarning ->
if (hasWarning) {
RoomEncryptionTrustLevel.Warning
} else {
if (params.userIds.size == allTrustedUserIds.size) {
// all users are trusted and all devices are verified
RoomEncryptionTrustLevel.Trusted
} else {
RoomEncryptionTrustLevel.Default
}
}
}
}
}
private fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? {
return cryptoStore.getCrossSigningInfo(otherUserId)
}
private fun getMyCrossSigningKeys(): MXCrossSigningInfo? {
return cryptoStore.getMyCrossSigningInfo()
}
}

View file

@ -19,8 +19,6 @@ package im.vector.matrix.android.internal.crypto.crosssigning
import androidx.lifecycle.LiveData
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
import im.vector.matrix.android.api.extensions.orFalse
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
import im.vector.matrix.android.api.util.Optional
@ -36,13 +34,15 @@ import im.vector.matrix.android.internal.crypto.tasks.UploadSignaturesTask
import im.vector.matrix.android.internal.crypto.tasks.UploadSigningKeysTask
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.task.TaskConstraints
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.TaskThread
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.JsonCanonicalizer
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.withoutPrefix
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.matrix.olm.OlmPkSigning
import org.matrix.olm.OlmUtility
import timber.log.Timber
@ -57,9 +57,11 @@ internal class DefaultCrossSigningService @Inject constructor(
private val deviceListManager: DeviceListManager,
private val uploadSigningKeysTask: UploadSigningKeysTask,
private val uploadSignaturesTask: UploadSignaturesTask,
private val cryptoCoroutineScope: CoroutineScope,
private val computeTrustTask: ComputeTrustTask,
private val taskExecutor: TaskExecutor,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val taskExecutor: TaskExecutor) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener {
private val cryptoCoroutineScope: CoroutineScope,
private val eventBus: EventBus) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener {
private var olmUtility: OlmUtility? = null
@ -211,7 +213,7 @@ internal class DefaultCrossSigningService @Inject constructor(
cryptoStore.storePrivateKeysInfo(masterKeyPrivateKey?.toBase64NoPadding(), uskPrivateKey?.toBase64NoPadding(), sskPrivateKey?.toBase64NoPadding())
uploadSigningKeysTask.configureWith(params) {
this.constraints = TaskConstraints(true)
this.executionThread = TaskThread.CRYPTO
this.callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.i("## CrossSigning - Keys successfully uploaded")
@ -247,7 +249,7 @@ internal class DefaultCrossSigningService @Inject constructor(
resetTrustOnKeyChange()
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadSignatureQueryBuilder.build())) {
// this.retryCount = 3
this.constraints = TaskConstraints(true)
this.executionThread = TaskThread.CRYPTO
this.callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.i("## CrossSigning - signatures successfully uploaded")
@ -396,7 +398,7 @@ internal class DefaultCrossSigningService @Inject constructor(
return@forEach
} catch (failure: Throwable) {
// log
Timber.v(failure)
Timber.w(failure, "Signature not valid?")
}
}
}
@ -500,6 +502,7 @@ internal class DefaultCrossSigningService @Inject constructor(
.withSigningKeyInfo(otherMasterKeys.copyForSignature(userId, userPubKey, newSignature))
.build()
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) {
this.executionThread = TaskThread.CRYPTO
this.callback = callback
}.executeBy(taskExecutor)
}
@ -546,6 +549,7 @@ internal class DefaultCrossSigningService @Inject constructor(
.withDeviceInfo(toUpload)
.build()
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) {
this.executionThread = TaskThread.CRYPTO
this.callback = callback
}.executeBy(taskExecutor)
}
@ -611,81 +615,52 @@ internal class DefaultCrossSigningService @Inject constructor(
}
}
override fun onUsersDeviceUpdate(users: List<String>) {
Timber.d("## CrossSigning - onUsersDeviceUpdate for ${users.size} users")
users.forEach { otherUserId ->
override fun onUsersDeviceUpdate(userIds: List<String>) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
Timber.d("## CrossSigning - onUsersDeviceUpdate for ${userIds.size} users")
userIds.forEach { otherUserId ->
checkUserTrust(otherUserId).let {
Timber.d("## CrossSigning - update trust for $otherUserId , verified=${it.isVerified()}")
setUserKeysAsTrusted(otherUserId, it.isVerified())
}
checkUserTrust(otherUserId).let {
Timber.d("## CrossSigning - update trust for $otherUserId , verified=${it.isVerified()}")
setUserKeysAsTrusted(otherUserId, it.isVerified())
}
// TODO if my keys have changes, i should recheck all devices of all users?
val devices = cryptoStore.getUserDeviceList(otherUserId)
devices?.forEach { device ->
val updatedTrust = checkDeviceTrust(otherUserId, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false)
Timber.d("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust")
cryptoStore.setDeviceTrust(otherUserId, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified())
}
// TODO if my keys have changes, i should recheck all devices of all users?
val devices = cryptoStore.getUserDeviceList(otherUserId)
devices?.forEach { device ->
val updatedTrust = checkDeviceTrust(otherUserId, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false)
Timber.d("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust")
cryptoStore.setDeviceTrust(otherUserId, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified())
}
if (otherUserId == userId) {
// It's me, i should check if a newly trusted device is signing my master key
// In this case it will change my MSK trust, and should then re-trigger a check of all other user trust
setUserKeysAsTrusted(otherUserId, checkSelfTrust().isVerified())
}
if (otherUserId == userId) {
// It's me, i should check if a newly trusted device is signing my master key
// In this case it will change my MSK trust, and should then re-trigger a check of all other user trust
setUserKeysAsTrusted(otherUserId, checkSelfTrust().isVerified())
eventBus.post(CryptoToSessionUserTrustChange(userIds))
}
}
}
private fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) {
val currentTrust = cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted()
cryptoStore.setUserKeysAsTrusted(otherUserId, trusted)
// If it's me, recheck trust of all users and devices?
val users = ArrayList<String>()
if (otherUserId == userId && currentTrust != trusted) {
cryptoStore.updateUsersTrust {
users.add(it)
checkUserTrust(it).isVerified()
}
users.forEach {
cryptoStore.getUserDeviceList(it)?.forEach { device ->
val updatedTrust = checkDeviceTrust(it, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false)
Timber.d("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust")
cryptoStore.setDeviceTrust(it, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified())
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
val currentTrust = cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted()
cryptoStore.setUserKeysAsTrusted(otherUserId, trusted)
// If it's me, recheck trust of all users and devices?
val users = ArrayList<String>()
if (otherUserId == userId && currentTrust != trusted) {
cryptoStore.updateUsersTrust {
users.add(it)
checkUserTrust(it).isVerified()
}
}
}
}
override fun getTrustLevelForUsers(userIds: List<String>): RoomEncryptionTrustLevel {
val allTrusted = userIds
.filter { getUserCrossSigningKeys(it)?.isTrusted() == true }
val allUsersAreVerified = userIds.size == allTrusted.size
return if (allTrusted.isEmpty()) {
RoomEncryptionTrustLevel.Default
} else {
// If one of the verified user as an untrusted device -> warning
// Green if all devices of all verified users are trusted -> green
// else black
val allDevices = allTrusted.mapNotNull {
cryptoStore.getUserDeviceList(it)
}.flatten()
if (getMyCrossSigningKeys() != null) {
val hasWarning = allDevices.any { !it.trustLevel?.crossSigningVerified.orFalse() }
if (hasWarning) {
RoomEncryptionTrustLevel.Warning
} else {
if (allUsersAreVerified) RoomEncryptionTrustLevel.Trusted else RoomEncryptionTrustLevel.Default
}
} else {
val hasWarningLegacy = allDevices.any { !it.isVerified }
if (hasWarningLegacy) {
RoomEncryptionTrustLevel.Warning
} else {
if (allUsersAreVerified) RoomEncryptionTrustLevel.Trusted else RoomEncryptionTrustLevel.Default
users.forEach {
cryptoStore.getUserDeviceList(it)?.forEach { device ->
val updatedTrust = checkDeviceTrust(it, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false)
Timber.d("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust")
cryptoStore.setDeviceTrust(it, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified())
}
}
}
}

View file

@ -5,7 +5,7 @@
* 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
* 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,
@ -13,13 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.crosssigning
package im.vector.riotx.features.home.room.detail
data class SessionToCryptoRoomMembersUpdate(
val roomId: String,
val userIds: List<String>
)
import java.io.File
data class DownloadFileState(
val mimeType: String,
val file: File?,
val throwable: Throwable?
)
data class CryptoToSessionUserTrustChange(
val userIds: List<String>
)

View file

@ -0,0 +1,134 @@
/*
* 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.crypto.crosssigning
import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.CryptoDatabase
import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.createBackgroundHandler
import io.realm.Realm
import io.realm.RealmConfiguration
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject
internal class ShieldTrustUpdater @Inject constructor(
private val eventBus: EventBus,
private val computeTrustTask: ComputeTrustTask,
private val taskExecutor: TaskExecutor,
@CryptoDatabase private val cryptoRealmConfiguration: RealmConfiguration,
@SessionDatabase private val sessionRealmConfiguration: RealmConfiguration,
private val roomSummaryUpdater: RoomSummaryUpdater
) {
companion object {
private val BACKGROUND_HANDLER = createBackgroundHandler("SHIELD_CRYPTO_DB_THREAD")
}
private val backgroundCryptoRealm = AtomicReference<Realm>()
private val backgroundSessionRealm = AtomicReference<Realm>()
// private var cryptoDevicesResult: RealmResults<DeviceInfoEntity>? = null
// private val cryptoDeviceChangeListener = object : OrderedRealmCollectionChangeListener<RealmResults<DeviceInfoEntity>> {
// override fun onChange(t: RealmResults<DeviceInfoEntity>, changeSet: OrderedCollectionChangeSet) {
// val grouped = t.groupBy { it.userId }
// onCryptoDevicesChange(grouped.keys.mapNotNull { it })
// }
// }
fun start() {
eventBus.register(this)
BACKGROUND_HANDLER.post {
val cryptoRealm = Realm.getInstance(cryptoRealmConfiguration)
backgroundCryptoRealm.set(cryptoRealm)
// cryptoDevicesResult = cryptoRealm.where<DeviceInfoEntity>().findAll()
// cryptoDevicesResult?.addChangeListener(cryptoDeviceChangeListener)
backgroundSessionRealm.set(Realm.getInstance(sessionRealmConfiguration))
}
}
fun stop() {
eventBus.unregister(this)
BACKGROUND_HANDLER.post {
// cryptoDevicesResult?.removeAllChangeListeners()
backgroundCryptoRealm.getAndSet(null).also {
it?.close()
}
backgroundSessionRealm.getAndSet(null).also {
it?.close()
}
}
}
@Subscribe
fun onRoomMemberChange(update: SessionToCryptoRoomMembersUpdate) {
taskExecutor.executorScope.launch {
val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(update.userIds))
// We need to send that back to session base
BACKGROUND_HANDLER.post {
backgroundSessionRealm.get().executeTransaction { realm ->
roomSummaryUpdater.updateShieldTrust(realm, update.roomId, updatedTrust)
}
}
}
}
@Subscribe
fun onTrustUpdate(update: CryptoToSessionUserTrustChange) {
onCryptoDevicesChange(update.userIds)
}
private fun onCryptoDevicesChange(users: List<String>) {
BACKGROUND_HANDLER.post {
val impactedRoomsId = backgroundSessionRealm.get().where(RoomMemberSummaryEntity::class.java)
.`in`(RoomMemberSummaryEntityFields.USER_ID, users.toTypedArray())
.findAll()
.map { it.roomId }
.distinct()
val map = HashMap<String, List<String>>()
impactedRoomsId.forEach { roomId ->
RoomMemberSummaryEntity.where(backgroundSessionRealm.get(), roomId)
.findAll()
.let { results ->
map[roomId] = results.map { it.userId }
}
}
map.forEach { entry ->
val roomId = entry.key
val userList = entry.value
taskExecutor.executorScope.launch {
val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(userList))
BACKGROUND_HANDLER.post {
backgroundSessionRealm.get().executeTransaction { realm ->
roomSummaryUpdater.updateShieldTrust(realm, roomId, updatedTrust)
}
}
}
}
}
}
}

View file

@ -414,7 +414,7 @@ internal class KeysBackup @Inject constructor(
olmDevice.verifySignature(fingerprint, authData.signalableJSONDictionary(), mySignature)
isSignatureValid = true
} catch (e: OlmException) {
Timber.v(e, "getKeysBackupTrust: Bad signature from device ${device.deviceId}")
Timber.w(e, "getKeysBackupTrust: Bad signature from device ${device.deviceId}")
}
}

View file

@ -61,6 +61,7 @@ import im.vector.matrix.android.internal.crypto.store.db.query.delete
import im.vector.matrix.android.internal.crypto.store.db.query.get
import im.vector.matrix.android.internal.crypto.store.db.query.getById
import im.vector.matrix.android.internal.crypto.store.db.query.getOrCreate
import im.vector.matrix.android.internal.di.CryptoDatabase
import im.vector.matrix.android.internal.session.SessionScope
import io.realm.Realm
import io.realm.RealmConfiguration
@ -70,11 +71,13 @@ import io.realm.kotlin.where
import org.matrix.olm.OlmAccount
import org.matrix.olm.OlmException
import timber.log.Timber
import javax.inject.Inject
import kotlin.collections.set
@SessionScope
internal class RealmCryptoStore(private val realmConfiguration: RealmConfiguration,
private val credentials: Credentials) : IMXCryptoStore {
internal class RealmCryptoStore @Inject constructor(
@CryptoDatabase private val realmConfiguration: RealmConfiguration,
private val credentials: Credentials) : IMXCryptoStore {
/* ==========================================================================================
* Memory cache, to correctly release JNI objects
@ -403,14 +406,14 @@ internal class RealmCryptoStore(private val realmConfiguration: RealmConfigurati
{ realm: Realm ->
realm
.where<UserEntity>()
.`in`(UserEntityFields.USER_ID, userIds.toTypedArray())
.`in`(UserEntityFields.USER_ID, userIds.distinct().toTypedArray())
},
{ entity ->
entity.devices.map { CryptoMapper.mapToModel(it) }
}
)
return Transformations.map(liveData) {
it.firstOrNull() ?: emptyList()
it.flatten()
}
}

View file

@ -88,7 +88,7 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
// done from another device of mine
if (EventType.MESSAGE == event.type) {
val msgType = event.getClearContent().toModel<MessageContent>()?.type
val msgType = event.getClearContent().toModel<MessageContent>()?.msgType
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) {
event.getClearContent().toModel<MessageVerificationRequestContent>()?.let {
if (it.fromDevice != deviceId) {
@ -144,7 +144,7 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
params.verificationService.onRoomEvent(event)
}
EventType.MESSAGE -> {
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.type) {
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.msgType) {
params.verificationService.onRoomRequestReceived(event)
}
}

View file

@ -37,7 +37,9 @@ internal class DefaultUploadSignaturesTask @Inject constructor(
override suspend fun execute(params: UploadSignaturesTask.Params) {
try {
val response = executeRequest<SignatureUploadResponse>(eventBus) {
apiCall = cryptoApi.uploadSignatures(params.signatures)
this.isRetryable = true
this.maxRetryCount = 10
this.apiCall = cryptoApi.uploadSignatures(params.signatures)
}
if (response.failures?.isNotEmpty() == true) {
throw Throwable(response.failures.toString())

View file

@ -165,7 +165,7 @@ internal class DefaultVerificationService @Inject constructor(
onRoomDoneReceived(event)
}
EventType.MESSAGE -> {
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.type) {
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.msgType) {
onRoomRequestReceived(event)
}
}

View file

@ -24,7 +24,7 @@ import im.vector.matrix.android.internal.crypto.tasks.RoomVerificationUpdateTask
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.query.types
import im.vector.matrix.android.internal.database.query.whereTypes
import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
@ -42,7 +42,7 @@ internal class VerificationMessageLiveObserver @Inject constructor(
) : RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
override val query = Monarchy.Query {
EventEntity.types(it, listOf(
EventEntity.whereTypes(it, listOf(
EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_KEY,

View file

@ -331,7 +331,7 @@ internal class VerificationTransportRoomMessage(
content = content,
unsignedData = UnsignedData(age = null, transactionId = localID)
).also {
localEchoEventFactory.saveLocalEcho(monarchy, it)
localEchoEventFactory.createLocalEcho(it)
}
}

View file

@ -16,19 +16,27 @@
package im.vector.matrix.android.internal.database.helper
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.room.send.SendState
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.mapper.toEntity
import im.vector.matrix.android.internal.database.model.*
import im.vector.matrix.android.api.session.room.model.RoomMemberContent
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntityFields
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.EventEntityFields
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.RoomMemberSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields
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.getOrCreate
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.extensions.assertIsManaged
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import io.realm.Realm
import io.realm.Sort
import io.realm.kotlin.createObject
import timber.log.Timber
internal fun ChunkEntity.deleteOnCascade() {
assertIsManaged()
@ -36,116 +44,154 @@ internal fun ChunkEntity.deleteOnCascade() {
this.deleteFromRealm()
}
internal fun ChunkEntity.merge(roomId: String,
chunkToMerge: ChunkEntity,
direction: PaginationDirection): List<TimelineEventEntity> {
internal fun ChunkEntity.merge(roomId: String, chunkToMerge: ChunkEntity, direction: PaginationDirection) {
assertIsManaged()
val isChunkToMergeUnlinked = chunkToMerge.isUnlinked
val isCurrentChunkUnlinked = isUnlinked
if (isCurrentChunkUnlinked && !isChunkToMergeUnlinked) {
this.timelineEvents.forEach { it.root?.isUnlinked = false }
}
val localRealm = this.realm
val eventsToMerge: List<TimelineEventEntity>
if (direction == PaginationDirection.FORWARDS) {
this.nextToken = chunkToMerge.nextToken
this.isLastForward = chunkToMerge.isLastForward
eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.ASCENDING)
eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
} else {
this.prevToken = chunkToMerge.prevToken
this.isLastBackward = chunkToMerge.isLastBackward
eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING)
eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
}
chunkToMerge.stateEvents.forEach { stateEvent ->
addStateEvent(roomId, stateEvent, direction)
}
return eventsToMerge
.mapNotNull {
val event = it.root?.asDomain() ?: return@mapNotNull null
add(roomId, event, direction)
.forEach {
addTimelineEventFromMerge(localRealm, it, direction)
}
}
internal fun ChunkEntity.add(roomId: String,
event: Event,
direction: PaginationDirection,
stateIndexOffset: Int = 0
): TimelineEventEntity? {
assertIsManaged()
if (event.eventId != null && timelineEvents.find(event.eventId) != null) {
return null
}
var currentDisplayIndex = lastDisplayIndex(direction, 0)
if (direction == PaginationDirection.FORWARDS) {
currentDisplayIndex += 1
forwardsDisplayIndex = currentDisplayIndex
internal fun ChunkEntity.addStateEvent(roomId: String, stateEvent: EventEntity, direction: PaginationDirection) {
if (direction == PaginationDirection.BACKWARDS) {
Timber.v("We don't keep chunk state events when paginating backward")
} else {
currentDisplayIndex -= 1
backwardsDisplayIndex = currentDisplayIndex
}
var currentStateIndex = lastStateIndex(direction, defaultValue = stateIndexOffset)
if (direction == PaginationDirection.FORWARDS && EventType.isStateEvent(event.type)) {
currentStateIndex += 1
forwardsStateIndex = currentStateIndex
} else if (direction == PaginationDirection.BACKWARDS && timelineEvents.isNotEmpty()) {
val lastEventType = timelineEvents.last()?.root?.type ?: ""
if (EventType.isStateEvent(lastEventType)) {
currentStateIndex -= 1
backwardsStateIndex = currentStateIndex
val stateKey = stateEvent.stateKey ?: return
val type = stateEvent.type
val pastStateEvent = stateEvents.where()
.equalTo(EventEntityFields.ROOM_ID, roomId)
.equalTo(EventEntityFields.STATE_KEY, stateKey)
.equalTo(CurrentStateEventEntityFields.TYPE, type)
.findFirst()
if (pastStateEvent != null) {
stateEvents.remove(pastStateEvent)
}
stateEvents.add(stateEvent)
}
}
val isChunkUnlinked = isUnlinked
internal fun ChunkEntity.addTimelineEvent(roomId: String,
eventEntity: EventEntity,
direction: PaginationDirection,
roomMemberContentsByUser: Map<String, RoomMemberContent?>) {
val eventId = eventEntity.eventId
if (timelineEvents.find(eventId) != null) {
return
}
val displayIndex = nextDisplayIndex(direction)
val localId = TimelineEventEntity.nextId(realm)
val eventId = event.eventId ?: ""
val senderId = event.senderId ?: ""
val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst()
?: realm.createObject<ReadReceiptsSummaryEntity>(eventId).apply {
this.roomId = roomId
}
val senderId = eventEntity.sender ?: ""
// Update RR for the sender of a new message with a dummy one
val readReceiptsSummaryEntity = handleReadReceipts(realm, roomId, eventEntity, senderId)
val timelineEventEntity = realm.createObject<TimelineEventEntity>().apply {
this.localId = localId
this.root = eventEntity
this.eventId = eventId
this.roomId = roomId
this.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
this.readReceipts = readReceiptsSummaryEntity
this.displayIndex = displayIndex
val roomMemberContent = roomMemberContentsByUser[senderId]
this.senderAvatar = roomMemberContent?.avatarUrl
this.senderName = roomMemberContent?.displayName
isUniqueDisplayName = if (roomMemberContent?.displayName != null) {
computeIsUnique(realm, roomId, isLastForward, roomMemberContent, roomMemberContentsByUser)
} else {
true
}
}
timelineEvents.add(timelineEventEntity)
}
if (event.originServerTs != null) {
val timestampOfEvent = event.originServerTs.toDouble()
private fun computeIsUnique(
realm: Realm,
roomId: String,
isLastForward: Boolean,
myRoomMemberContent: RoomMemberContent,
roomMemberContentsByUser: Map<String, RoomMemberContent?>
): Boolean {
val isHistoricalUnique = roomMemberContentsByUser.values.find {
it != myRoomMemberContent && it?.displayName == myRoomMemberContent.displayName
} == null
return if (isLastForward) {
val isLiveUnique = RoomMemberSummaryEntity
.where(realm, roomId)
.equalTo(RoomMemberSummaryEntityFields.DISPLAY_NAME, myRoomMemberContent.displayName)
.findAll().none {
!roomMemberContentsByUser.containsKey(it.userId)
}
isHistoricalUnique && isLiveUnique
} else {
isHistoricalUnique
}
}
private fun ChunkEntity.addTimelineEventFromMerge(realm: Realm, timelineEventEntity: TimelineEventEntity, direction: PaginationDirection) {
val eventId = timelineEventEntity.eventId
if (timelineEvents.find(eventId) != null) {
return
}
val displayIndex = nextDisplayIndex(direction)
val localId = TimelineEventEntity.nextId(realm)
val copied = realm.createObject<TimelineEventEntity>().apply {
this.localId = localId
this.root = timelineEventEntity.root
this.eventId = timelineEventEntity.eventId
this.roomId = timelineEventEntity.roomId
this.annotations = timelineEventEntity.annotations
this.readReceipts = timelineEventEntity.readReceipts
this.displayIndex = displayIndex
this.senderAvatar = timelineEventEntity.senderAvatar
this.senderName = timelineEventEntity.senderName
this.isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName
}
timelineEvents.add(copied)
}
private fun handleReadReceipts(realm: Realm, roomId: String, eventEntity: EventEntity, senderId: String): ReadReceiptsSummaryEntity {
val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventEntity.eventId).findFirst()
?: realm.createObject<ReadReceiptsSummaryEntity>(eventEntity.eventId).apply {
this.roomId = roomId
}
val originServerTs = eventEntity.originServerTs
if (originServerTs != null) {
val timestampOfEvent = originServerTs.toDouble()
val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId)
// If the synced RR is older, update
if (timestampOfEvent > readReceiptOfSender.originServerTs) {
val previousReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId = readReceiptOfSender.eventId).findFirst()
readReceiptOfSender.eventId = eventId
readReceiptOfSender.eventId = eventEntity.eventId
readReceiptOfSender.originServerTs = timestampOfEvent
previousReceiptsSummary?.readReceipts?.remove(readReceiptOfSender)
readReceiptsSummaryEntity.readReceipts.add(readReceiptOfSender)
}
}
val rootEvent = event.toEntity(roomId).apply {
this.stateIndex = currentStateIndex
this.displayIndex = currentDisplayIndex
this.sendState = SendState.SYNCED
this.isUnlinked = isChunkUnlinked
}
val eventEntity = realm.createObject<TimelineEventEntity>().also {
it.localId = localId
it.root = realm.copyToRealm(rootEvent)
it.eventId = eventId
it.roomId = roomId
it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
it.readReceipts = readReceiptsSummaryEntity
}
val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size
timelineEvents.add(position, eventEntity)
return eventEntity
return readReceiptsSummaryEntity
}
internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
internal fun ChunkEntity.nextDisplayIndex(direction: PaginationDirection): Int {
return when (direction) {
PaginationDirection.FORWARDS -> forwardsDisplayIndex
PaginationDirection.BACKWARDS -> backwardsDisplayIndex
} ?: defaultValue
}
internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
return when (direction) {
PaginationDirection.FORWARDS -> forwardsStateIndex
PaginationDirection.BACKWARDS -> backwardsStateIndex
} ?: defaultValue
PaginationDirection.FORWARDS -> {
(timelineEvents.where().max(TimelineEventEntityFields.DISPLAY_INDEX)?.toInt() ?: 0) + 1
}
PaginationDirection.BACKWARDS -> {
(timelineEvents.where().min(TimelineEventEntityFields.DISPLAY_INDEX)?.toInt() ?: 0) - 1
}
}
}

View file

@ -16,15 +16,8 @@
package im.vector.matrix.android.internal.database.helper
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.send.SendState
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.RoomEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.query.fastContains
import im.vector.matrix.android.internal.extensions.assertIsManaged
import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper
internal fun RoomEntity.deleteOnCascade(chunkEntity: ChunkEntity) {
chunks.remove(chunkEntity)
@ -36,39 +29,3 @@ internal fun RoomEntity.addOrUpdate(chunkEntity: ChunkEntity) {
chunks.add(chunkEntity)
}
}
internal fun RoomEntity.addStateEvent(stateEvent: Event,
stateIndex: Int = Int.MIN_VALUE,
filterDuplicates: Boolean = false,
isUnlinked: Boolean = false) {
assertIsManaged()
if (stateEvent.eventId == null || (filterDuplicates && fastContains(stateEvent.eventId))) {
return
} else {
val entity = stateEvent.toEntity(roomId).apply {
this.stateIndex = stateIndex
this.isUnlinked = isUnlinked
this.sendState = SendState.SYNCED
}
untimelinedStateEvents.add(entity)
}
}
internal fun RoomEntity.addSendingEvent(event: Event) {
assertIsManaged()
val senderId = event.senderId ?: return
val eventEntity = event.toEntity(roomId).apply {
this.sendState = SendState.UNSENT
}
val roomMembers = RoomMemberHelper(realm, roomId)
val myUser = roomMembers.getLastRoomMember(senderId)
val localId = TimelineEventEntity.nextId(realm)
val timelineEventEntity = TimelineEventEntity(localId).also {
it.root = eventEntity
it.eventId = event.eventId ?: ""
it.roomId = roomId
it.senderName = myUser?.displayName
it.senderAvatar = myUser?.avatarUrl
it.isUniqueDisplayName = roomMembers.isUniqueDisplayName(myUser?.displayName)
}
sendingTimelineEvents.add(0, timelineEventEntity)
}

View file

@ -1,148 +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.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.RoomMemberHelper
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 = RoomMemberHelper(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 = RoomMemberHelper(realm, roomId).isUniqueDisplayName(it.displayName)
}
}
}
result.senderMembershipEventId = senderMembershipEvent?.eventId
return result
}
}

View file

@ -20,6 +20,7 @@ import com.squareup.moshi.JsonDataException
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.UnsignedData
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.di.MoshiProvider
@ -43,7 +44,6 @@ internal object EventMapper {
eventEntity.redacts = event.redacts
eventEntity.age = event.unsignedData?.age ?: event.originServerTs
eventEntity.unsignedData = uds
eventEntity.ageLocalTs = event.ageLocalTs
return eventEntity
}
@ -92,6 +92,9 @@ internal fun EventEntity.asDomain(): Event {
return EventMapper.map(this)
}
internal fun Event.toEntity(roomId: String): EventEntity {
return EventMapper.map(this, roomId)
internal fun Event.toEntity(roomId: String, sendState: SendState, ageLocalTs: Long? = null): EventEntity {
return EventMapper.map(this, roomId).apply {
this.sendState = sendState
this.ageLocalTs = ageLocalTs
}
}

View file

@ -38,7 +38,7 @@ internal object PushRulesMapper {
enabled = pushrule.enabled,
ruleId = pushrule.ruleId,
conditions = listOf(
PushCondition(Condition.Kind.event_match.name, "content.body", pushrule.pattern)
PushCondition(Condition.Kind.EventMatch.value, "content.body", pushrule.pattern)
)
)
}
@ -59,7 +59,7 @@ internal object PushRulesMapper {
enabled = pushrule.enabled,
ruleId = pushrule.ruleId,
conditions = listOf(
PushCondition(Condition.Kind.event_match.name, "room_id", pushrule.ruleId)
PushCondition(Condition.Kind.EventMatch.value, "room_id", pushrule.ruleId)
)
)
}
@ -71,7 +71,7 @@ internal object PushRulesMapper {
enabled = pushrule.enabled,
ruleId = pushrule.ruleId,
conditions = listOf(
PushCondition(Condition.Kind.event_match.name, "user_id", pushrule.ruleId)
PushCondition(Condition.Kind.EventMatch.value, "user_id", pushrule.ruleId)
)
)
}

View file

@ -26,8 +26,7 @@ internal object RoomMemberSummaryMapper {
userId = roomMemberSummaryEntity.userId,
avatarUrl = roomMemberSummaryEntity.avatarUrl,
displayName = roomMemberSummaryEntity.displayName,
membership = roomMemberSummaryEntity.membership,
userEncryptionTrustLevel = null
membership = roomMemberSummaryEntity.membership
)
}
}

View file

@ -17,11 +17,11 @@
package im.vector.matrix.android.internal.database.mapper
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.tag.RoomTag
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
@ -49,7 +49,8 @@ internal class RoomSummaryMapper @Inject constructor(
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
} catch (e: MXCryptoError) {
} catch (e: Throwable) {
Timber.d(e)
}
}
@ -75,7 +76,8 @@ internal class RoomSummaryMapper @Inject constructor(
aliases = roomSummaryEntity.aliases.toList(),
isEncrypted = roomSummaryEntity.isEncrypted,
typingRoomMemberIds = roomSummaryEntity.typingUserIds.toList(),
breadcrumbsIndex = roomSummaryEntity.breadcrumbsIndex
breadcrumbsIndex = roomSummaryEntity.breadcrumbsIndex,
roomEncryptionTrustLevel = roomSummaryEntity.roomEncryptionTrustLevel
)
}
}

View file

@ -37,15 +37,19 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
return TimelineEvent(
root = timelineEventEntity.root?.asDomain()
?: Event("", timelineEventEntity.eventId),
eventId = timelineEventEntity.eventId,
annotations = timelineEventEntity.annotations?.asDomain(),
localId = timelineEventEntity.localId,
displayIndex = timelineEventEntity.root?.displayIndex ?: 0,
displayIndex = timelineEventEntity.displayIndex,
senderName = timelineEventEntity.senderName,
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
senderAvatar = timelineEventEntity.senderAvatar,
readReceipts = readReceipts?.sortedByDescending {
it.originServerTs
} ?: emptyList()
readReceipts = readReceipts
?.distinctBy {
it.user
}?.sortedByDescending {
it.originServerTs
} ?: emptyList()
)
}
}

View file

@ -24,14 +24,10 @@ import io.realm.annotations.LinkingObjects
internal open class ChunkEntity(@Index var prevToken: String? = null,
@Index var nextToken: String? = null,
var stateEvents: RealmList<EventEntity> = RealmList(),
var timelineEvents: RealmList<TimelineEventEntity> = RealmList(),
@Index var isLastForward: Boolean = false,
@Index var isLastBackward: Boolean = false,
var backwardsDisplayIndex: Int? = null,
var forwardsDisplayIndex: Int? = null,
var backwardsStateIndex: Int? = null,
var forwardsStateIndex: Int? = null,
var isUnlinked: Boolean = false
@Index var isLastBackward: Boolean = false
) : RealmObject() {
fun identifier() = "${prevToken}_$nextToken"

View file

@ -0,0 +1,30 @@
/*
* 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.database.model
import io.realm.RealmObject
import io.realm.annotations.Index
internal open class CurrentStateEventEntity(var eventId: String = "",
var root: EventEntity? = null,
@Index var roomId: String = "",
@Index var type: String = "",
@Index var stateKey: String = ""
) : RealmObject() {
companion object
}

View file

@ -21,11 +21,10 @@ import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.di.MoshiProvider
import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.Index
import io.realm.annotations.LinkingObjects
import io.realm.annotations.PrimaryKey
internal open class EventEntity(@Index var eventId: String = "",
internal open class EventEntity(@PrimaryKey var eventId: String = "",
@Index var roomId: String = "",
@Index var type: String = "",
var content: String? = null,
@ -36,20 +35,11 @@ internal open class EventEntity(@Index var eventId: String = "",
var age: Long? = 0,
var unsignedData: String? = null,
var redacts: String? = null,
@Index var stateIndex: Int = 0,
@Index var displayIndex: Int = 0,
@Index var isUnlinked: Boolean = false,
var decryptionResultJson: String? = null,
var decryptionErrorCode: String? = null,
var ageLocalTs: Long? = null
) : RealmObject() {
enum class LinkFilterMode {
LINKED_ONLY,
UNLINKED_ONLY,
BOTH
}
private var sendStateStr: String = SendState.UNKNOWN.name
var sendState: SendState
@ -62,12 +52,6 @@ internal open class EventEntity(@Index var eventId: String = "",
companion object
@LinkingObjects("untimelinedStateEvents")
val room: RealmResults<RoomEntity>? = null
@LinkingObjects("root")
val timelineEventEntity: RealmResults<TimelineEventEntity>? = null
fun setDecryptionResult(result: MXEventDecryptionResult) {
val decryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
@ -78,6 +62,5 @@ internal open class EventEntity(@Index var eventId: String = "",
val adapter = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java)
decryptionResultJson = adapter.toJson(decryptionResult)
decryptionErrorCode = null
timelineEventEntity?.firstOrNull()?.root = this
}
}

View file

@ -23,7 +23,6 @@ import io.realm.annotations.PrimaryKey
internal open class RoomEntity(@PrimaryKey var roomId: String = "",
var chunks: RealmList<ChunkEntity> = RealmList(),
var untimelinedStateEvents: RealmList<EventEntity> = RealmList(),
var sendingTimelineEvents: RealmList<TimelineEventEntity> = RealmList(),
var areAllMembersLoaded: Boolean = false
) : RealmObject() {

View file

@ -16,6 +16,7 @@
package im.vector.matrix.android.internal.database.model
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.VersioningState
@ -47,7 +48,8 @@ internal open class RoomSummaryEntity(
// this is required for querying
var flatAliases: String = "",
var isEncrypted: Boolean = false,
var typingUserIds: RealmList<String> = RealmList()
var typingUserIds: RealmList<String> = RealmList(),
var roomEncryptionTrustLevelStr: String? = null
) : RealmObject() {
private var membershipStr: String = Membership.NONE.name
@ -68,5 +70,19 @@ internal open class RoomSummaryEntity(
versioningStateStr = value.name
}
var roomEncryptionTrustLevel: RoomEncryptionTrustLevel?
get() {
return roomEncryptionTrustLevelStr?.let {
try {
RoomEncryptionTrustLevel.valueOf(it)
} catch (failure: Throwable) {
null
}
}
}
set(value) {
roomEncryptionTrustLevelStr = value?.name
}
companion object
}

View file

@ -22,35 +22,36 @@ import io.realm.annotations.RealmModule
* Realm module for Session
*/
@RealmModule(library = true,
classes = [
ChunkEntity::class,
EventEntity::class,
TimelineEventEntity::class,
FilterEntity::class,
GroupEntity::class,
GroupSummaryEntity::class,
ReadReceiptEntity::class,
RoomEntity::class,
RoomSummaryEntity::class,
RoomTagEntity::class,
SyncEntity::class,
UserEntity::class,
IgnoredUserEntity::class,
BreadcrumbsEntity::class,
EventAnnotationsSummaryEntity::class,
ReferencesAggregatedSummaryEntity::class,
ReactionAggregatedSummaryEntity::class,
EditAggregatedSummaryEntity::class,
PushRulesEntity::class,
PushRuleEntity::class,
PushConditionEntity::class,
PusherEntity::class,
PusherDataEntity::class,
ReadReceiptsSummaryEntity::class,
ReadMarkerEntity::class,
UserDraftsEntity::class,
DraftEntity::class,
HomeServerCapabilitiesEntity::class,
RoomMemberSummaryEntity::class
])
classes = [
ChunkEntity::class,
EventEntity::class,
TimelineEventEntity::class,
FilterEntity::class,
GroupEntity::class,
GroupSummaryEntity::class,
ReadReceiptEntity::class,
RoomEntity::class,
RoomSummaryEntity::class,
RoomTagEntity::class,
SyncEntity::class,
UserEntity::class,
IgnoredUserEntity::class,
BreadcrumbsEntity::class,
EventAnnotationsSummaryEntity::class,
ReactionAggregatedSummaryEntity::class,
EditAggregatedSummaryEntity::class,
ReferencesAggregatedSummaryEntity::class,
PushRulesEntity::class,
PushRuleEntity::class,
PushConditionEntity::class,
PusherEntity::class,
PusherDataEntity::class,
ReadReceiptsSummaryEntity::class,
ReadMarkerEntity::class,
UserDraftsEntity::class,
DraftEntity::class,
HomeServerCapabilitiesEntity::class,
RoomMemberSummaryEntity::class,
CurrentStateEventEntity::class
])
internal class SessionRealmModule

View file

@ -24,6 +24,7 @@ import io.realm.annotations.LinkingObjects
internal open class TimelineEventEntity(var localId: Long = 0,
@Index var eventId: String = "",
@Index var roomId: String = "",
@Index var displayIndex: Int = 0,
var root: EventEntity? = null,
var annotations: EventAnnotationsSummaryEntity? = null,
var senderName: String? = null,

View file

@ -60,12 +60,10 @@ internal fun ChunkEntity.Companion.findIncludingEvent(realm: Realm, eventId: Str
internal fun ChunkEntity.Companion.create(
realm: Realm,
prevToken: String?,
nextToken: String?,
isUnlinked: Boolean
nextToken: String?
): ChunkEntity {
return realm.createObject<ChunkEntity>().apply {
this.prevToken = prevToken
this.nextToken = nextToken
this.isUnlinked = isUnlinked
}
}

View file

@ -0,0 +1,52 @@
/*
* 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.database.query
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntityFields
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.kotlin.createObject
internal fun CurrentStateEventEntity.Companion.where(realm: Realm, roomId: String, type: String): RealmQuery<CurrentStateEventEntity> {
return realm.where(CurrentStateEventEntity::class.java)
.equalTo(CurrentStateEventEntityFields.ROOM_ID, roomId)
.equalTo(CurrentStateEventEntityFields.TYPE, type)
}
internal fun CurrentStateEventEntity.Companion.whereStateKey(realm: Realm, roomId: String, type: String, stateKey: String)
: RealmQuery<CurrentStateEventEntity> {
return where(realm = realm, roomId = roomId, type = type)
.equalTo(CurrentStateEventEntityFields.STATE_KEY, stateKey)
}
internal fun CurrentStateEventEntity.Companion.getOrNull(realm: Realm, roomId: String, stateKey: String, type: String): CurrentStateEventEntity? {
return whereStateKey(realm = realm, roomId = roomId, type = type, stateKey = stateKey).findFirst()
}
internal fun CurrentStateEventEntity.Companion.getOrCreate(realm: Realm, roomId: String, stateKey: String, type: String): CurrentStateEventEntity {
return getOrNull(realm = realm, roomId = roomId, stateKey = stateKey, type = type) ?: create(realm, roomId, stateKey, type)
}
private fun create(realm: Realm, roomId: String, stateKey: String, type: String): CurrentStateEventEntity {
return realm.createObject<CurrentStateEventEntity>().apply {
this.type = type
this.roomId = roomId
this.stateKey = stateKey
}
}

View file

@ -17,12 +17,10 @@
package im.vector.matrix.android.internal.database.query
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntity.LinkFilterMode.*
import im.vector.matrix.android.internal.database.model.EventEntityFields
import io.realm.Realm
import io.realm.RealmList
import io.realm.RealmQuery
import io.realm.Sort
import io.realm.kotlin.where
internal fun EventEntity.Companion.where(realm: Realm, eventId: String): RealmQuery<EventEntity> {
@ -35,61 +33,28 @@ internal fun EventEntity.Companion.where(realm: Realm, eventIds: List<String>):
.`in`(EventEntityFields.EVENT_ID, eventIds.toTypedArray())
}
internal fun EventEntity.Companion.where(realm: Realm,
roomId: String? = null,
type: String? = null,
linkFilterMode: EventEntity.LinkFilterMode = LINKED_ONLY): RealmQuery<EventEntity> {
internal fun EventEntity.Companion.whereType(realm: Realm,
type: String,
roomId: String? = null
): RealmQuery<EventEntity> {
val query = realm.where<EventEntity>()
if (roomId != null) {
query.equalTo(EventEntityFields.ROOM_ID, roomId)
}
if (type != null) {
query.equalTo(EventEntityFields.TYPE, type)
}
return when (linkFilterMode) {
LINKED_ONLY -> query.equalTo(EventEntityFields.IS_UNLINKED, false)
UNLINKED_ONLY -> query.equalTo(EventEntityFields.IS_UNLINKED, true)
BOTH -> query
}
return query.equalTo(EventEntityFields.TYPE, type)
}
internal fun EventEntity.Companion.types(realm: Realm,
typeList: List<String> = emptyList()): RealmQuery<EventEntity> {
internal fun EventEntity.Companion.whereTypes(realm: Realm,
typeList: List<String> = emptyList(),
roomId: String? = null): RealmQuery<EventEntity> {
val query = realm.where<EventEntity>()
query.`in`(EventEntityFields.TYPE, typeList.toTypedArray())
if (roomId != null) {
query.equalTo(EventEntityFields.ROOM_ID, roomId)
}
return query
}
internal fun RealmQuery<EventEntity>.descending(since: Int? = null, strict: Boolean = false): RealmQuery<EventEntity> {
if (since != null) {
if (strict) {
this.lessThan(EventEntityFields.STATE_INDEX, since)
} else {
this.lessThanOrEqualTo(EventEntityFields.STATE_INDEX, since)
}
}
return this.sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING)
}
internal fun RealmQuery<EventEntity>.ascending(from: Int? = null, strict: Boolean = true): RealmQuery<EventEntity> {
if (from != null) {
if (strict) {
this.greaterThan(EventEntityFields.STATE_INDEX, from)
} else {
this.greaterThanOrEqualTo(EventEntityFields.STATE_INDEX, from)
}
}
return this.sort(EventEntityFields.STATE_INDEX, Sort.ASCENDING)
}
internal fun RealmQuery<EventEntity>.next(from: Int? = null, strict: Boolean = true): EventEntity? {
return this.ascending(from, strict).findFirst()
}
internal fun RealmQuery<EventEntity>.prev(since: Int? = null, strict: Boolean = false): EventEntity? {
return descending(since, strict).findFirst()
}
internal fun RealmList<EventEntity>.find(eventId: String): EventEntity? {
return this.where()
.equalTo(EventEntityFields.EVENT_ID, eventId)

View file

@ -20,6 +20,7 @@ import im.vector.matrix.android.api.session.events.model.LocalEcho
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import io.realm.Realm
internal fun isEventRead(monarchy: Monarchy,
@ -36,16 +37,15 @@ internal fun isEventRead(monarchy: Monarchy,
monarchy.doWithRealm { realm ->
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) ?: return@doWithRealm
val eventToCheck = liveChunk.timelineEvents.find(eventId)?.root
isEventRead = if (eventToCheck?.sender == userId) {
val eventToCheck = liveChunk.timelineEvents.find(eventId)
isEventRead = if (eventToCheck == null || eventToCheck.root?.sender == userId) {
true
} else {
val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst()
?: return@doWithRealm
val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex
?: Int.MIN_VALUE
val eventToCheckIndex = eventToCheck?.displayIndex ?: Int.MAX_VALUE
?: return@doWithRealm
val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.displayIndex
?: Int.MIN_VALUE
val eventToCheckIndex = eventToCheck.displayIndex
eventToCheckIndex <= readReceiptIndex
}
@ -61,13 +61,17 @@ internal fun isReadMarkerMoreRecent(monarchy: Monarchy,
return false
}
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) ?: return false
val eventToCheck = liveChunk.timelineEvents.find(eventId)?.root
val eventToCheck = TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId).findFirst()
val eventToCheckChunk = eventToCheck?.chunk?.firstOrNull()
val readMarker = ReadMarkerEntity.where(realm, roomId).findFirst() ?: return false
val readMarkerIndex = liveChunk.timelineEvents.find(readMarker.eventId)?.root?.displayIndex
?: Int.MIN_VALUE
val eventToCheckIndex = eventToCheck?.displayIndex ?: Int.MAX_VALUE
eventToCheckIndex <= readMarkerIndex
val readMarkerEvent = TimelineEventEntity.where(realm, roomId = roomId, eventId = readMarker.eventId).findFirst()
val readMarkerChunk = readMarkerEvent?.chunk?.firstOrNull()
if (eventToCheckChunk == readMarkerChunk) {
val readMarkerIndex = readMarkerEvent?.displayIndex ?: Int.MIN_VALUE
val eventToCheckIndex = eventToCheck?.displayIndex ?: Int.MAX_VALUE
eventToCheckIndex <= readMarkerIndex
} else {
eventToCheckChunk?.isLastForward == false
}
}
}

View file

@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.database.query
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.internal.database.model.*
import im.vector.matrix.android.internal.database.model.EventEntity.LinkFilterMode.*
import io.realm.*
import io.realm.kotlin.where
@ -34,22 +33,10 @@ internal fun TimelineEventEntity.Companion.where(realm: Realm, roomId: String, e
.`in`(TimelineEventEntityFields.EVENT_ID, eventIds.toTypedArray())
}
internal fun TimelineEventEntity.Companion.where(realm: Realm,
roomId: String? = null,
type: String? = null,
linkFilterMode: EventEntity.LinkFilterMode = LINKED_ONLY): RealmQuery<TimelineEventEntity> {
val query = realm.where<TimelineEventEntity>()
if (roomId != null) {
query.equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
}
if (type != null) {
query.equalTo(TimelineEventEntityFields.ROOT.TYPE, type)
}
return when (linkFilterMode) {
LINKED_ONLY -> query.equalTo(TimelineEventEntityFields.ROOT.IS_UNLINKED, false)
UNLINKED_ONLY -> query.equalTo(TimelineEventEntityFields.ROOT.IS_UNLINKED, true)
BOTH -> query
}
internal fun TimelineEventEntity.Companion.whereRoomId(realm: Realm,
roomId: String): RealmQuery<TimelineEventEntity> {
return realm.where<TimelineEventEntity>()
.equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
}
internal fun TimelineEventEntity.Companion.findWithSenderMembershipEvent(realm: Realm, senderMembershipEventId: String): List<TimelineEventEntity> {
@ -71,7 +58,7 @@ internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm,
liveEvents
}
return query
?.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING)
?.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
?.findFirst()
}
@ -83,32 +70,6 @@ internal fun RealmQuery<TimelineEventEntity>.filterTypes(filterTypes: List<Strin
}
}
internal fun RealmQuery<TimelineEventEntity>.next(from: Int? = null, strict: Boolean = true): TimelineEventEntity? {
if (from != null) {
if (strict) {
this.greaterThan(TimelineEventEntityFields.ROOT.STATE_INDEX, from)
} else {
this.greaterThanOrEqualTo(TimelineEventEntityFields.ROOT.STATE_INDEX, from)
}
}
return this
.sort(TimelineEventEntityFields.ROOT.STATE_INDEX, Sort.ASCENDING)
.findFirst()
}
internal fun RealmQuery<TimelineEventEntity>.prev(since: Int? = null, strict: Boolean = false): TimelineEventEntity? {
if (since != null) {
if (strict) {
this.lessThan(TimelineEventEntityFields.ROOT.STATE_INDEX, since)
} else {
this.lessThanOrEqualTo(TimelineEventEntityFields.ROOT.STATE_INDEX, since)
}
}
return this
.sort(TimelineEventEntityFields.ROOT.STATE_INDEX, Sort.DESCENDING)
.findFirst()
}
internal fun RealmList<TimelineEventEntity>.find(eventId: String): TimelineEventEntity? {
return this.where()
.equalTo(TimelineEventEntityFields.EVENT_ID, eventId)

View file

@ -22,11 +22,11 @@ import com.squareup.moshi.Moshi
import dagger.BindsInstance
import dagger.Component
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.MatrixConfiguration
import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.auth.AuthModule
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
@ -48,6 +48,8 @@ internal interface MatrixComponent {
fun context(): Context
fun matrixConfiguration(): MatrixConfiguration
fun resources(): Resources
fun olmManager(): OlmManager
@ -56,8 +58,6 @@ internal interface MatrixComponent {
fun sessionParamsStore(): SessionParamsStore
fun networkConnectivityChecker(): NetworkConnectivityChecker
fun backgroundDetectionObserver(): BackgroundDetectionObserver
fun sessionManager(): SessionManager
@ -66,6 +66,7 @@ internal interface MatrixComponent {
@Component.Factory
interface Factory {
fun create(@BindsInstance context: Context): MatrixComponent
fun create(@BindsInstance context: Context,
@BindsInstance matrixConfiguration: MatrixConfiguration): MatrixComponent
}
}

View file

@ -0,0 +1,76 @@
/*
* 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.network
import android.annotation.TargetApi
import android.content.Context
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.net.Network
import android.os.Build
import javax.inject.Inject
internal interface NetworkCallbackStrategy {
fun register(hasChanged: () -> Unit)
fun unregister()
}
internal class FallbackNetworkCallbackStrategy @Inject constructor(private val context: Context,
private val networkInfoReceiver: NetworkInfoReceiver) : NetworkCallbackStrategy {
@Suppress("DEPRECATION")
val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
override fun register(hasChanged: () -> Unit) {
networkInfoReceiver.isConnectedCallback = {
hasChanged()
}
context.registerReceiver(networkInfoReceiver, filter)
}
override fun unregister() {
networkInfoReceiver.isConnectedCallback = null
context.unregisterReceiver(networkInfoReceiver)
}
}
@TargetApi(Build.VERSION_CODES.N)
internal class PreferredNetworkCallbackStrategy @Inject constructor(context: Context) : NetworkCallbackStrategy {
private var hasChangedCallback: (() -> Unit)? = null
private val conn = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onLost(network: Network) {
hasChangedCallback?.invoke()
}
override fun onAvailable(network: Network) {
hasChangedCallback?.invoke()
}
}
override fun register(hasChanged: () -> Unit) {
hasChangedCallback = hasChanged
conn.registerDefaultNetworkCallback(networkCallback)
}
override fun unregister() {
hasChangedCallback = null
conn.unregisterNetworkCallback(networkCallback)
}
}

View file

@ -16,112 +16,94 @@
package im.vector.matrix.android.internal.network
import android.content.Context
import androidx.annotation.WorkerThread
import com.novoda.merlin.Merlin
import com.novoda.merlin.MerlinsBeard
import im.vector.matrix.android.internal.di.MatrixScope
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.homeserver.HomeServerPinger
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
import timber.log.Timber
import kotlinx.coroutines.runBlocking
import java.util.Collections
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@MatrixScope
internal class NetworkConnectivityChecker @Inject constructor(private val context: Context,
private val backgroundDetectionObserver: BackgroundDetectionObserver)
: BackgroundDetectionObserver.Listener {
interface NetworkConnectivityChecker {
/**
* Returns true when internet is available
*/
@WorkerThread
fun hasInternetAccess(forcePing: Boolean): Boolean
private val merlin = Merlin.Builder()
.withConnectableCallbacks()
.withDisconnectableCallbacks()
.build(context)
fun register(listener: Listener)
fun unregister(listener: Listener)
private val merlinsBeard = MerlinsBeard.Builder().build(context)
interface Listener {
fun onConnectivityChanged()
}
}
private val listeners = Collections.synchronizedSet(LinkedHashSet<Listener>())
private var hasInternetAccess = merlinsBeard.isConnected
@SessionScope
internal class DefaultNetworkConnectivityChecker @Inject constructor(private val homeServerPinger: HomeServerPinger,
private val backgroundDetectionObserver: BackgroundDetectionObserver,
private val networkCallbackStrategy: NetworkCallbackStrategy)
: NetworkConnectivityChecker {
init {
backgroundDetectionObserver.register(this)
private val hasInternetAccess = AtomicBoolean(true)
private val listeners = Collections.synchronizedSet(LinkedHashSet<NetworkConnectivityChecker.Listener>())
private val backgroundDetectionObserverListener = object : BackgroundDetectionObserver.Listener {
override fun onMoveToForeground() {
bind()
}
override fun onMoveToBackground() {
unbind()
}
}
/**
* 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()
override fun hasInternetAccess(forcePing: Boolean): Boolean {
return if (forcePing) {
runBlocking {
homeServerPinger.canReachHomeServer()
}
} else {
hasInternetAccess
hasInternetAccess.get()
}
}
override fun onMoveToForeground() {
merlin.bind()
merlinsBeard.hasInternetAccess {
hasInternetAccess = it
}
merlin.registerDisconnectable {
if (hasInternetAccess) {
Timber.v("On Disconnect")
hasInternetAccess = false
val localListeners = listeners.toList()
localListeners.forEach {
it.onDisconnect()
}
override fun register(listener: NetworkConnectivityChecker.Listener) {
if (listeners.isEmpty()) {
if (backgroundDetectionObserver.isInBackground) {
unbind()
} else {
bind()
}
backgroundDetectionObserver.register(backgroundDetectionObserverListener)
}
merlin.registerConnectable {
if (!hasInternetAccess) {
Timber.v("On Connect")
hasInternetAccess = true
val localListeners = listeners.toList()
localListeners.forEach {
it.onConnect()
}
}
}
}
override fun onMoveToBackground() {
merlin.unbind()
}
// In background you won't get notification as merlin is unbound
suspend fun waitUntilConnected() {
if (hasInternetAccess) {
return
} else {
Timber.v("Waiting for network...")
suspendCoroutine<Unit> { continuation ->
register(object : Listener {
override fun onConnect() {
unregister(this)
Timber.v("Connected to network...")
continuation.resume(Unit)
}
})
}
}
}
fun register(listener: Listener) {
listeners.add(listener)
}
fun unregister(listener: Listener) {
override fun unregister(listener: NetworkConnectivityChecker.Listener) {
listeners.remove(listener)
if (listeners.isEmpty()) {
backgroundDetectionObserver.unregister(backgroundDetectionObserverListener)
}
}
interface Listener {
fun onConnect() {
private fun bind() {
networkCallbackStrategy.register {
val localListeners = listeners.toList()
localListeners.forEach {
it.onConnectivityChanged()
}
}
homeServerPinger.canReachHomeServer {
hasInternetAccess.set(it)
}
}
fun onDisconnect() {
}
private fun unbind() {
networkCallbackStrategy.unregister()
}
}

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.internal.network
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.NetworkInfo
import javax.inject.Inject
internal class NetworkInfoReceiver @Inject constructor() : BroadcastReceiver() {
var isConnectedCallback: ((Boolean) -> Unit)? = null
override fun onReceive(context: Context, intent: Intent) {
val conn = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val networkInfo: NetworkInfo? = conn.activeNetworkInfo
isConnectedCallback?.invoke(networkInfo?.isConnected ?: false)
}
}

View file

@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.network
import im.vector.matrix.android.api.failure.Failure
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay
import org.greenrobot.eventbus.EventBus
import retrofit2.Call
import java.io.IOException
@ -27,11 +28,17 @@ internal suspend inline fun <DATA> executeRequest(eventBus: EventBus?,
internal class Request<DATA>(private val eventBus: EventBus?) {
var isRetryable = false
var initialDelay: Long = 100L
var maxDelay: Long = 10_000L
var maxRetryCount = Int.MAX_VALUE
private var currentRetryCount = 0
private var currentDelay = initialDelay
lateinit var apiCall: Call<DATA>
suspend fun execute(): DATA {
return try {
val response = apiCall.awaitResponse()
val response = apiCall.clone().awaitResponse()
if (response.isSuccessful) {
response.body()
?: throw IllegalStateException("The request returned a null body")
@ -39,12 +46,18 @@ internal class Request<DATA>(private val eventBus: EventBus?) {
throw response.toFailure(eventBus)
}
} catch (exception: Throwable) {
throw when (exception) {
is IOException -> Failure.NetworkConnection(exception)
is Failure.ServerError,
is Failure.OtherServerError -> exception
is CancellationException -> Failure.Cancelled(exception)
else -> Failure.Unknown(exception)
if (isRetryable && currentRetryCount++ < maxRetryCount && exception is IOException) {
delay(currentDelay)
currentDelay = (currentDelay * 2L).coerceAtMost(maxDelay)
return execute()
} else {
throw when (exception) {
is IOException -> Failure.NetworkConnection(exception)
is Failure.ServerError,
is Failure.OtherServerError -> exception
is CancellationException -> Failure.Cancelled(exception)
else -> Failure.Unknown(exception)
}
}
}
}

View file

@ -18,18 +18,20 @@ package im.vector.matrix.android.internal.network
import android.content.Context
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.MatrixConfiguration
import im.vector.matrix.android.internal.di.MatrixScope
import timber.log.Timber
import javax.inject.Inject
@MatrixScope
internal class UserAgentHolder @Inject constructor(private val context: Context) {
internal class UserAgentHolder @Inject constructor(private val context: Context,
matrixConfiguration: MatrixConfiguration) {
var userAgent: String = ""
private set
init {
setApplicationFlavor("NoFlavor")
setApplicationFlavor(matrixConfiguration.applicationFlavor)
}
/**
@ -38,7 +40,7 @@ internal class UserAgentHolder @Inject constructor(private val context: Context)
*
* @param flavorDescription the flavor description
*/
fun setApplicationFlavor(flavorDescription: String) {
private fun setApplicationFlavor(flavorDescription: String) {
var appName = ""
var appVersion = ""

View file

@ -43,6 +43,7 @@ import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.crypto.DefaultCryptoService
import im.vector.matrix.android.internal.crypto.crosssigning.ShieldTrustUpdater
import im.vector.matrix.android.internal.database.LiveEntityObserver
import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.di.WorkManagerProvider
@ -89,7 +90,8 @@ internal class DefaultSession @Inject constructor(
private val sessionParamsStore: SessionParamsStore,
private val contentUploadProgressTracker: ContentUploadStateTracker,
private val initialSyncProgressService: Lazy<InitialSyncProgressService>,
private val homeServerCapabilitiesService: Lazy<HomeServerCapabilitiesService>)
private val homeServerCapabilitiesService: Lazy<HomeServerCapabilitiesService>,
private val shieldTrustUpdater: ShieldTrustUpdater)
: Session,
RoomService by roomService.get(),
RoomDirectoryService by roomDirectoryService.get(),
@ -119,6 +121,7 @@ internal class DefaultSession @Inject constructor(
isOpen = true
liveEntityObservers.forEach { it.start() }
eventBus.register(this)
shieldTrustUpdater.start()
}
override fun requireBackgroundSync() {
@ -160,6 +163,7 @@ internal class DefaultSession @Inject constructor(
isOpen = false
eventBus.unregister(this)
syncTaskSequencer.close()
shieldTrustUpdater.stop()
}
override fun getSyncStateLive(): LiveData<SyncState> {

View file

@ -17,16 +17,19 @@
package im.vector.matrix.android.internal.session
import android.content.Context
import android.os.Build
import com.zhuinden.monarchy.Monarchy
import dagger.Binds
import dagger.Lazy
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoSet
import im.vector.matrix.android.api.MatrixConfiguration
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.auth.data.sessionId
import im.vector.matrix.android.api.crypto.MXCryptoConfig
import im.vector.matrix.android.api.session.InitialSyncProgressService
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService
@ -34,8 +37,21 @@ import im.vector.matrix.android.api.session.securestorage.SecureStorageService
import im.vector.matrix.android.internal.crypto.verification.VerificationMessageLiveObserver
import im.vector.matrix.android.internal.database.LiveEntityObserver
import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory
import im.vector.matrix.android.internal.di.*
import im.vector.matrix.android.internal.di.Authenticated
import im.vector.matrix.android.internal.di.DeviceId
import im.vector.matrix.android.internal.di.SessionCacheDirectory
import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.di.SessionFilesDirectory
import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.di.UserMd5
import im.vector.matrix.android.internal.network.AccessTokenInterceptor
import im.vector.matrix.android.internal.network.DefaultNetworkConnectivityChecker
import im.vector.matrix.android.internal.network.FallbackNetworkCallbackStrategy
import im.vector.matrix.android.internal.network.NetworkCallbackStrategy
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
import im.vector.matrix.android.internal.network.PreferredNetworkCallbackStrategy
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
@ -51,6 +67,7 @@ import okhttp3.OkHttpClient
import org.greenrobot.eventbus.EventBus
import retrofit2.Retrofit
import java.io.File
import javax.inject.Provider
@Module
internal abstract class SessionModule {
@ -59,6 +76,11 @@ internal abstract class SessionModule {
companion object {
internal fun getKeyAlias(userMd5: String) = "session_db_$userMd5"
/**
* Rules:
* Annotate methods with @SessionScope only the @Provides annotated methods with computation and logic.
*/
@JvmStatic
@Provides
fun providesHomeServerConnectionConfig(sessionParams: SessionParams): HomeServerConnectionConfig {
@ -74,6 +96,7 @@ internal abstract class SessionModule {
@JvmStatic
@UserId
@Provides
@SessionScope
fun providesUserId(credentials: Credentials): String {
return credentials.userId
}
@ -88,6 +111,7 @@ internal abstract class SessionModule {
@JvmStatic
@UserMd5
@Provides
@SessionScope
fun providesUserMd5(@UserId userId: String): String {
return userId.md5()
}
@ -95,6 +119,7 @@ internal abstract class SessionModule {
@JvmStatic
@SessionId
@Provides
@SessionScope
fun providesSessionId(credentials: Credentials): String {
return credentials.sessionId()
}
@ -178,11 +203,34 @@ internal abstract class SessionModule {
fun providesEventBus(): EventBus {
return EventBus.builder().build()
}
@JvmStatic
@Provides
@SessionScope
fun providesNetworkCallbackStrategy(fallbackNetworkCallbackStrategy: Provider<FallbackNetworkCallbackStrategy>,
preferredNetworkCallbackStrategy: Provider<PreferredNetworkCallbackStrategy>
): NetworkCallbackStrategy {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
preferredNetworkCallbackStrategy.get()
} else {
fallbackNetworkCallbackStrategy.get()
}
}
@JvmStatic
@Provides
@SessionScope
fun providesMxCryptoConfig(matrixConfiguration: MatrixConfiguration): MXCryptoConfig {
return matrixConfiguration.cryptoConfig
}
}
@Binds
abstract fun bindSession(session: DefaultSession): Session
@Binds
abstract fun bindNetworkConnectivityChecker(networkConnectivityChecker: DefaultNetworkConnectivityChecker): NetworkConnectivityChecker
@Binds
@IntoSet
abstract fun bindGroupSummaryUpdater(groupSummaryUpdater: GroupSummaryUpdater): LiveEntityObserver

View file

@ -25,6 +25,7 @@ import im.vector.matrix.android.internal.session.group.model.GroupRooms
import im.vector.matrix.android.internal.session.group.model.GroupSummaryResponse
import im.vector.matrix.android.internal.session.group.model.GroupUsers
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.awaitTransaction
import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
@ -53,12 +54,12 @@ internal class DefaultGetGroupDataTask @Inject constructor(
insertInDb(groupSummary, groupRooms, groupUsers, groupId)
}
private fun insertInDb(groupSummary: GroupSummaryResponse,
private suspend fun insertInDb(groupSummary: GroupSummaryResponse,
groupRooms: GroupRooms,
groupUsers: GroupUsers,
groupId: String) {
monarchy
.writeAsync { realm ->
.awaitTransaction { realm ->
val groupSummaryEntity = GroupSummaryEntity.where(realm, groupId).findFirst()
?: realm.createObject(GroupSummaryEntity::class.java, groupId)

View file

@ -27,4 +27,10 @@ internal interface CapabilitiesAPI {
*/
@GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "config")
fun getUploadCapabilities(): Call<GetUploadCapabilitiesResult>
/**
* Request the versions
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_ + "versions")
fun getVersions(): Call<Unit>
}

View file

@ -0,0 +1,49 @@
/*
* 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.session.homeserver
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.TaskExecutor
import kotlinx.coroutines.launch
import javax.inject.Inject
internal class HomeServerPinger @Inject constructor(private val taskExecutor: TaskExecutor,
private val capabilitiesAPI: CapabilitiesAPI) {
fun canReachHomeServer(callback: (Boolean) -> Unit) {
taskExecutor.executorScope.launch {
val canReach = canReachHomeServer()
callback(canReach)
}
}
suspend fun canReachHomeServer(): Boolean {
return try {
executeRequest<Unit>(null) {
apiCall = capabilitiesAPI.getVersions()
}
true
} catch (throwable: Throwable) {
if (throwable is Failure.OtherServerError) {
(throwable.httpCode == 404 || throwable.httpCode == 400)
} else {
false
}
}
}
}

View file

@ -38,12 +38,13 @@ import timber.log.Timber
import javax.inject.Inject
@SessionScope
internal class DefaultPushRuleService @Inject constructor(private val getPushRulesTask: GetPushRulesTask,
private val updatePushRuleEnableStatusTask: UpdatePushRuleEnableStatusTask,
private val addPushRuleTask: AddPushRuleTask,
private val removePushRuleTask: RemovePushRuleTask,
private val taskExecutor: TaskExecutor,
private val monarchy: Monarchy
internal class DefaultPushRuleService @Inject constructor(
private val getPushRulesTask: GetPushRulesTask,
private val updatePushRuleEnableStatusTask: UpdatePushRuleEnableStatusTask,
private val addPushRuleTask: AddPushRuleTask,
private val removePushRuleTask: RemovePushRuleTask,
private val taskExecutor: TaskExecutor,
private val monarchy: Monarchy
) : PushRuleService {
private var listeners = mutableSetOf<PushRuleService.PushRuleListener>()

View file

@ -16,12 +16,11 @@
package im.vector.matrix.android.internal.session.notification
import im.vector.matrix.android.api.pushrules.ConditionResolver
import im.vector.matrix.android.api.pushrules.rest.PushRule
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.room.RoomService
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.session.pushers.DefaultConditionResolver
import im.vector.matrix.android.internal.session.sync.model.RoomsSyncResponse
import im.vector.matrix.android.internal.task.Task
import timber.log.Timber
@ -36,7 +35,7 @@ internal interface ProcessEventForPushTask : Task<ProcessEventForPushTask.Params
internal class DefaultProcessEventForPushTask @Inject constructor(
private val defaultPushRuleService: DefaultPushRuleService,
private val roomService: RoomService,
private val conditionResolver: ConditionResolver,
@UserId private val userId: String
) : ProcessEventForPushTask {
@ -97,12 +96,10 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
}
private fun fulfilledBingRule(event: Event, rules: List<PushRule>): PushRule? {
// TODO This should be injected
val conditionResolver = DefaultConditionResolver(event, roomService, userId)
return rules.firstOrNull { rule ->
// All conditions must hold true for an event in order to apply the action for the event.
rule.enabled && rule.conditions?.all {
it.asExecutableCondition()?.isSatisfied(conditionResolver) ?: false
it.asExecutableCondition()?.isSatisfied(event, conditionResolver) ?: false
} ?: false
}
}

View file

@ -15,37 +15,52 @@
*/
package im.vector.matrix.android.internal.session.pushers
import im.vector.matrix.android.api.pushrules.*
import im.vector.matrix.android.api.pushrules.ConditionResolver
import im.vector.matrix.android.api.pushrules.ContainsDisplayNameCondition
import im.vector.matrix.android.api.pushrules.EventMatchCondition
import im.vector.matrix.android.api.pushrules.RoomMemberCountCondition
import im.vector.matrix.android.api.pushrules.SenderNotificationPermissionCondition
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.RoomService
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.PowerLevelsContent
import im.vector.matrix.android.internal.di.UserId
import timber.log.Timber
import im.vector.matrix.android.internal.session.room.RoomGetter
import javax.inject.Inject
// TODO Inject constructor
internal class DefaultConditionResolver(private val event: Event,
private val roomService: RoomService,
@UserId private val userId: String) : ConditionResolver {
internal class DefaultConditionResolver @Inject constructor(
private val roomGetter: RoomGetter,
@UserId private val userId: String
) : ConditionResolver {
override fun resolveEventMatchCondition(eventMatchCondition: EventMatchCondition): Boolean {
return eventMatchCondition.isSatisfied(event)
override fun resolveEventMatchCondition(event: Event,
condition: EventMatchCondition): Boolean {
return condition.isSatisfied(event)
}
override fun resolveRoomMemberCountCondition(roomMemberCountCondition: RoomMemberCountCondition): Boolean {
return roomMemberCountCondition.isSatisfied(event, roomService)
override fun resolveRoomMemberCountCondition(event: Event,
condition: RoomMemberCountCondition): Boolean {
return condition.isSatisfied(event, roomGetter)
}
override fun resolveSenderNotificationPermissionCondition(senderNotificationPermissionCondition: SenderNotificationPermissionCondition): Boolean {
// val roomId = event.roomId ?: return false
// val room = roomService.getRoom(roomId) ?: return false
// TODO RoomState not yet managed
Timber.e("POWER LEVELS STATE NOT YET MANAGED BY RIOTX")
return false // senderNotificationPermissionCondition.isSatisfied(event, )
}
override fun resolveContainsDisplayNameCondition(containsDisplayNameCondition: ContainsDisplayNameCondition): Boolean {
override fun resolveSenderNotificationPermissionCondition(event: Event,
condition: SenderNotificationPermissionCondition): Boolean {
val roomId = event.roomId ?: return false
val room = roomService.getRoom(roomId) ?: return false
val room = roomGetter.getRoom(roomId) ?: return false
val powerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, "")
?.content
?.toModel<PowerLevelsContent>()
?: PowerLevelsContent()
return condition.isSatisfied(event, powerLevelsContent)
}
override fun resolveContainsDisplayNameCondition(event: Event,
condition: ContainsDisplayNameCondition): Boolean {
val roomId = event.roomId ?: return false
val room = roomGetter.getRoom(roomId) ?: return false
val myDisplayName = room.getRoomMember(userId)?.displayName ?: return false
return containsDisplayNameCondition.isSatisfied(event, myDisplayName)
return condition.isSatisfied(event, myDisplayName)
}
}

View file

@ -21,6 +21,7 @@ import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.members.MembershipService
import im.vector.matrix.android.api.session.room.model.RoomSummary
@ -35,10 +36,15 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineService
import im.vector.matrix.android.api.session.room.typing.TypingService
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.room.state.SendStateTask
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import java.security.InvalidParameterException
import javax.inject.Inject
internal class DefaultRoom @Inject constructor(override val roomId: String,
@ -54,7 +60,9 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
private val cryptoService: CryptoService,
private val relationService: RelationService,
private val roomMembersService: MembershipService,
private val roomPushRuleService: RoomPushRuleService) :
private val roomPushRuleService: RoomPushRuleService,
private val taskExecutor: TaskExecutor,
private val sendStateTask: SendStateTask) :
Room,
TimelineService by timelineService,
SendService by sendService,
@ -96,11 +104,27 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
return cryptoService.shouldEncryptForInvitedMembers(roomId)
}
override fun enableEncryptionWithAlgorithm(algorithm: String, callback: MatrixCallback<Unit>) {
if (isEncrypted()) {
callback.onFailure(IllegalStateException("Encryption is already enabled for this room"))
} else {
stateService.enableEncryption(algorithm, callback)
override fun enableEncryption(algorithm: String, callback: MatrixCallback<Unit>) {
when {
isEncrypted() -> {
callback.onFailure(IllegalStateException("Encryption is already enabled for this room"))
}
algorithm != MXCRYPTO_ALGORITHM_MEGOLM -> {
callback.onFailure(InvalidParameterException("Only MXCRYPTO_ALGORITHM_MEGOLM algorithm is supported"))
}
else -> {
val params = SendStateTask.Params(roomId,
EventType.STATE_ROOM_ENCRYPTION,
mapOf(
"algorithm" to algorithm
))
sendStateTask
.configureWith(params) {
this.callback = callback
}
.executeBy(taskExecutor)
}
}
}
}

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