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: plugins:
- docker#v3.1.0: - docker#v3.1.0:
image: "openjdk" 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 files: exclude everything except dictionnaries
.idea/caches .idea/caches
.idea/libraries .idea/libraries
.idea/inspectionProfiles
.idea/*.xml .idea/*.xml
.DS_Store .DS_Store
/build /build
@ -13,7 +14,3 @@
/tmp /tmp
ktlint 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"> <component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173"> <code_scheme name="Project" version="173">
<option name="RIGHT_MARGIN" value="160" />
<AndroidXmlCodeStyleSettings> <AndroidXmlCodeStyleSettings>
<option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" /> <option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" />
</AndroidXmlCodeStyleSettings> </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) Changes in RiotX 0.14.3 (2020-02-03)
=================================================== ===================================================

View file

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

View file

@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.android.MainThreadDisposable import io.reactivex.android.MainThreadDisposable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
private class LiveDataObservable<T>( private class LiveDataObservable<T>(
@ -60,3 +61,11 @@ private class LiveDataObservable<T>(
fun <T> LiveData<T>.asObservable(): Observable<T> { fun <T> LiveData<T>.asObservable(): Observable<T> {
return LiveDataObservable(this).observeOn(Schedulers.computation()) 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 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.events.model.Event
import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams import im.vector.matrix.android.api.session.room.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.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Single 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>> { fun liveRoomSummary(): Observable<Optional<RoomSummary>> {
val summaryObservable = room.getRoomSummaryLive() return room.getRoomSummaryLive()
.asObservable() .asObservable()
.startWith(room.roomSummary().toOptional()) .startWithCallable { 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}") }
} }
fun liveRoomMembers(queryParams: RoomMemberQueryParams): Observable<List<RoomMemberSummary>> { fun liveRoomMembers(queryParams: RoomMemberQueryParams): Observable<List<RoomMemberSummary>> {
val roomMembersObservable = room.getRoomMembersLive(queryParams).asObservable() return room.getRoomMembersLive(queryParams).asObservable()
.startWith(room.getRoomMembers(queryParams)) .startWithCallable {
.doOnNext { Timber.v("RX: room members emitted. Size: ${it.size}") } room.getRoomMembers(queryParams)
}
// 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}") }
} }
fun liveAnnotationSummary(eventId: String): Observable<Optional<EventAnnotationsSummary>> { fun liveAnnotationSummary(eventId: String): Observable<Optional<EventAnnotationsSummary>> {
return room.getEventAnnotationsSummaryLive(eventId).asObservable() return room.getEventAnnotationsSummaryLive(eventId).asObservable()
.startWith(room.getEventAnnotationsSummary(eventId).toOptional()) .startWithCallable {
room.getEventAnnotationsSummary(eventId).toOptional()
}
} }
fun liveTimelineEvent(eventId: String): Observable<Optional<TimelineEvent>> { fun liveTimelineEvent(eventId: String): Observable<Optional<TimelineEvent>> {
return room.getTimeLineEventLive(eventId).asObservable() return room.getTimeLineEventLive(eventId).asObservable()
.startWith(room.getTimeLineEvent(eventId).toOptional()) .startWithCallable {
room.getTimeLineEvent(eventId).toOptional()
}
} }
fun liveStateEvent(eventType: String): Observable<Optional<Event>> { fun liveStateEvent(eventType: String, stateKey: String): Observable<Optional<Event>> {
return room.getStateEventLive(eventType).asObservable() return room.getStateEventLive(eventType, stateKey).asObservable()
.startWith(room.getStateEvent(eventType).toOptional()) .startWithCallable {
room.getStateEvent(eventType, stateKey).toOptional()
}
} }
fun liveReadMarker(): Observable<Optional<String>> { 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 { fun Room.rx(): RxRoom {
return RxRoom(this, session) 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 im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Single import io.reactivex.Single
import io.reactivex.functions.BiFunction
import timber.log.Timber
class RxSession(private val session: Session) { class RxSession(private val session: Session) {
fun liveRoomSummaries(queryParams: RoomSummaryQueryParams): Observable<List<RoomSummary>> { fun liveRoomSummaries(queryParams: RoomSummaryQueryParams): Observable<List<RoomSummary>> {
val summariesObservable = session.getRoomSummariesLive(queryParams).asObservable() return session.getRoomSummariesLive(queryParams).asObservable()
.startWith(session.getRoomSummaries(queryParams)) .startWithCallable {
.doOnNext { Timber.v("RX: summaries emitted: size: ${it.size}") } session.getRoomSummaries(queryParams)
}
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}") }
} }
fun liveGroupSummaries(queryParams: GroupSummaryQueryParams): Observable<List<GroupSummary>> { fun liveGroupSummaries(queryParams: GroupSummaryQueryParams): Observable<List<GroupSummary>> {
return session.getGroupSummariesLive(queryParams).asObservable() return session.getGroupSummariesLive(queryParams).asObservable()
.startWith(session.getGroupSummaries(queryParams)) .startWithCallable {
session.getGroupSummaries(queryParams)
}
} }
fun liveBreadcrumbs(): Observable<List<RoomSummary>> { fun liveBreadcrumbs(): Observable<List<RoomSummary>> {
return session.getBreadcrumbsLive().asObservable() return session.getBreadcrumbsLive().asObservable()
.startWith(session.getBreadcrumbs()) .startWithCallable {
session.getBreadcrumbs()
}
} }
fun liveSyncState(): Observable<SyncState> { fun liveSyncState(): Observable<SyncState> {
@ -87,7 +67,9 @@ class RxSession(private val session: Session) {
fun liveUser(userId: String): Observable<Optional<User>> { fun liveUser(userId: String): Observable<Optional<User>> {
return session.getUserLive(userId).asObservable() return session.getUserLive(userId).asObservable()
.startWith(session.getUser(userId).toOptional()) .startWithCallable {
session.getUser(userId).toOptional()
}
} }
fun liveUsers(): Observable<List<User>> { fun liveUsers(): Observable<List<User>> {
@ -128,12 +110,16 @@ class RxSession(private val session: Session) {
} }
fun liveUserCryptoDevices(userId: String): Observable<List<CryptoDeviceInfo>> { 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>> { fun liveCrossSigningInfo(userId: String): Observable<Optional<MXCrossSigningInfo>> {
return session.getCrossSigningService().getLiveCrossSigningKeys(userId).asObservable() 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 arrow_version = "0.8.2"
def moshi_version = '1.8.0' 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 coroutines_version = "1.3.2"
def markwon_version = '3.1.0' 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.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
@ -104,14 +105,13 @@ dependencies {
implementation "androidx.appcompat:appcompat:1.1.0" implementation "androidx.appcompat:appcompat:1.1.0"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
// Network // Network
implementation 'com.squareup.retrofit2:retrofit:2.6.2' implementation 'com.squareup.retrofit2:retrofit:2.6.2'
implementation 'com.squareup.retrofit2:converter-moshi:2.6.2' implementation 'com.squareup.retrofit2:converter-moshi:2.6.2'
implementation 'com.squareup.okhttp3:okhttp:4.2.2' implementation 'com.squareup.okhttp3:okhttp:4.2.2'
implementation 'com.squareup.okhttp3:logging-interceptor: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" implementation "com.squareup.moshi:moshi-adapters:$moshi_version"
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
@ -125,7 +125,7 @@ dependencies {
kapt 'dk.ilios:realmfieldnameshelper:1.1.1' kapt 'dk.ilios:realmfieldnameshelper:1.1.1'
// Work // Work
implementation "androidx.work:work-runtime-ktx:2.3.0-beta02" implementation "androidx.work:work-runtime-ktx:2.3.0"
// FP // FP
implementation "io.arrow-kt:arrow-core:$arrow_version" implementation "io.arrow-kt:arrow-core:$arrow_version"
@ -167,7 +167,7 @@ dependencies {
androidTestImplementation 'org.amshove.kluent:kluent-android:1.44' androidTestImplementation 'org.amshove.kluent:kluent-android:1.44'
// Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281 // 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 '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" 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 onTimelineFailure(throwable: Throwable) {
} }
override fun onNewTimelineEvents(eventIds: List<String>) {
// noop
}
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) { override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
// TODO Count only new messages? // TODO Count only new messages?
if (snapshot.count { it.root.type == EventType.MESSAGE } == nbOfMessages) { 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 room = aliceSession.getRoom(roomId!!)!!
val lock2 = CountDownLatch(1) val lock2 = CountDownLatch(1)
room.enableEncryptionWithAlgorithm(MXCRYPTO_ALGORITHM_MEGOLM, object : TestMatrixCallback<Unit>(lock2) {}) room.enableEncryption(callback = TestMatrixCallback(lock2))
mTestHelper.await(lock2) mTestHelper.await(lock2)
return CryptoTestData(aliceSession, roomId!!) return CryptoTestData(aliceSession, roomId!!)
@ -241,6 +241,11 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) {
val bobEventsListener = object : Timeline.Listener { val bobEventsListener = object : Timeline.Listener {
override fun onTimelineFailure(throwable: Throwable) { override fun onTimelineFailure(throwable: Throwable) {
// noop
}
override fun onNewTimelineEvents(eventIds: List<String>) {
// noop
} }
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) { 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 com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.InstrumentedTest import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.internal.database.helper.add import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.internal.database.helper.lastStateIndex 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.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.ChunkEntity
import im.vector.matrix.android.internal.database.model.SessionRealmModule import im.vector.matrix.android.internal.database.model.SessionRealmModule
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeListOfEvents import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeListOfEvents
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeMessageEvent import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeMessageEvent
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeRoomMemberEvent
import io.realm.Realm import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import io.realm.kotlin.createObject import io.realm.kotlin.createObject
@ -58,8 +58,11 @@ internal class ChunkEntityTest : InstrumentedTest {
fun add_shouldAdd_whenNotAlreadyIncluded() { fun add_shouldAdd_whenNotAlreadyIncluded() {
monarchy.runTransactionSync { realm -> monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject() 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 chunk.timelineEvents.size shouldEqual 1
} }
} }
@ -68,65 +71,23 @@ internal class ChunkEntityTest : InstrumentedTest {
fun add_shouldNotAdd_whenAlreadyIncluded() { fun add_shouldNotAdd_whenAlreadyIncluded() {
monarchy.runTransactionSync { realm -> monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject() val chunk: ChunkEntity = realm.createObject()
val fakeEvent = createFakeMessageEvent() val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED).let {
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) realm.copyToRealmOrUpdate(it)
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) }
chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
chunk.timelineEvents.size shouldEqual 1 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 @Test
fun merge_shouldAddEvents_whenMergingBackward() { fun merge_shouldAddEvents_whenMergingBackward() {
monarchy.runTransactionSync { realm -> monarchy.runTransactionSync { realm ->
val chunk1: ChunkEntity = realm.createObject() val chunk1: ChunkEntity = realm.createObject()
val chunk2: ChunkEntity = realm.createObject() val chunk2: ChunkEntity = realm.createObject()
chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS) chunk1.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS) chunk2.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS) chunk1.merge(ROOM_ID, chunk2, PaginationDirection.BACKWARDS)
chunk1.timelineEvents.size shouldEqual 60 chunk1.timelineEvents.size shouldEqual 60
} }
} }
@ -140,9 +101,9 @@ internal class ChunkEntityTest : InstrumentedTest {
val eventsForChunk2 = eventsForChunk1 + createFakeListOfEvents(10) val eventsForChunk2 = eventsForChunk1 + createFakeListOfEvents(10)
chunk1.isLastForward = true chunk1.isLastForward = true
chunk2.isLastForward = false chunk2.isLastForward = false
chunk1.addAll("roomId", eventsForChunk1, PaginationDirection.FORWARDS) chunk1.addAll(ROOM_ID, eventsForChunk1, PaginationDirection.FORWARDS)
chunk2.addAll("roomId", eventsForChunk2, PaginationDirection.BACKWARDS) chunk2.addAll(ROOM_ID, eventsForChunk2, PaginationDirection.BACKWARDS)
chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS) chunk1.merge(ROOM_ID, chunk2, PaginationDirection.BACKWARDS)
chunk1.timelineEvents.size shouldEqual 40 chunk1.timelineEvents.size shouldEqual 40
chunk1.isLastForward.shouldBeTrue() chunk1.isLastForward.shouldBeTrue()
} }
@ -155,9 +116,9 @@ internal class ChunkEntityTest : InstrumentedTest {
val chunk2: ChunkEntity = realm.createObject() val chunk2: ChunkEntity = realm.createObject()
val prevToken = "prev_token" val prevToken = "prev_token"
chunk1.prevToken = prevToken chunk1.prevToken = prevToken
chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS) chunk1.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS) chunk2.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk1.merge("roomId", chunk2, PaginationDirection.FORWARDS) chunk1.merge(ROOM_ID, chunk2, PaginationDirection.FORWARDS)
chunk1.prevToken shouldEqual prevToken chunk1.prevToken shouldEqual prevToken
} }
} }
@ -169,19 +130,25 @@ internal class ChunkEntityTest : InstrumentedTest {
val chunk2: ChunkEntity = realm.createObject() val chunk2: ChunkEntity = realm.createObject()
val nextToken = "next_token" val nextToken = "next_token"
chunk1.nextToken = nextToken chunk1.nextToken = nextToken
chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS) chunk1.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS) chunk2.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS) chunk1.merge(ROOM_ID, chunk2, PaginationDirection.BACKWARDS)
chunk1.nextToken shouldEqual nextToken chunk1.nextToken shouldEqual nextToken
} }
} }
private fun ChunkEntity.addAll(roomId: String, private fun ChunkEntity.addAll(roomId: String,
events: List<Event>, events: List<Event>,
direction: PaginationDirection, direction: PaginationDirection) {
stateIndexOffset: Int = 0) {
events.forEach { event -> 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 com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.auth.AuthenticationService 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.SessionManager
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
@ -35,7 +36,8 @@ import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject import javax.inject.Inject
data class MatrixConfiguration( data class MatrixConfiguration(
val applicationFlavor: String = "Default-application-flavor" val applicationFlavor: String = "Default-application-flavor",
val cryptoConfig: MXCryptoConfig = MXCryptoConfig()
) { ) {
interface Provider { interface Provider {
@ -57,12 +59,11 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
init { init {
Monarchy.init(context) Monarchy.init(context)
DaggerMatrixComponent.factory().create(context).inject(this) DaggerMatrixComponent.factory().create(context, matrixConfiguration).inject(this)
if (context.applicationContext !is Configuration.Provider) { if (context.applicationContext !is Configuration.Provider) {
WorkManager.initialize(context, Configuration.Builder().build()) WorkManager.initialize(context, Configuration.Builder().build())
} }
ProcessLifecycleOwner.get().lifecycle.addObserver(backgroundDetectionObserver) ProcessLifecycleOwner.get().lifecycle.addObserver(backgroundDetectionObserver)
userAgentHolder.setApplicationFlavor(matrixConfiguration.applicationFlavor)
} }
fun getUserAgent() = userAgentHolder.userAgent fun getUserAgent() = userAgentHolder.userAgent

View file

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

View file

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

View file

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

View file

@ -15,14 +15,22 @@
*/ */
package im.vector.matrix.android.api.pushrules package im.vector.matrix.android.api.pushrules
import im.vector.matrix.android.api.session.events.model.Event
/** /**
* Acts like a visitor on Conditions. * Acts like a visitor on Conditions.
* This class as all required context needed to evaluate rules * This class as all required context needed to evaluate rules
*/ */
interface ConditionResolver { interface ConditionResolver {
fun resolveEventMatchCondition(event: Event,
condition: EventMatchCondition): Boolean
fun resolveEventMatchCondition(eventMatchCondition: EventMatchCondition): Boolean fun resolveRoomMemberCountCondition(event: Event,
fun resolveRoomMemberCountCondition(roomMemberCountCondition: RoomMemberCountCondition): Boolean condition: RoomMemberCountCondition): Boolean
fun resolveSenderNotificationPermissionCondition(senderNotificationPermissionCondition: SenderNotificationPermissionCondition): Boolean
fun resolveContainsDisplayNameCondition(containsDisplayNameCondition: ContainsDisplayNameCondition) : 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 im.vector.matrix.android.api.session.room.model.message.MessageContent
import timber.log.Timber import timber.log.Timber
class ContainsDisplayNameCondition : Condition(Kind.contains_display_name) { class ContainsDisplayNameCondition : Condition(Kind.ContainsDisplayName) {
override fun isSatisfied(conditionResolver: ConditionResolver): Boolean { override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveContainsDisplayNameCondition(this) return conditionResolver.resolveContainsDisplayNameCondition(event, this)
} }
override fun technicalDescription(): String { 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 im.vector.matrix.android.internal.di.MoshiProvider
import timber.log.Timber 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 { override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveEventMatchCondition(this) return conditionResolver.resolveEventMatchCondition(event, this)
} }
override fun technicalDescription(): String { override fun technicalDescription(): String {

View file

@ -16,25 +16,32 @@
package im.vector.matrix.android.api.pushrules package im.vector.matrix.android.api.pushrules
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.internal.session.room.RoomGetter
import timber.log.Timber import timber.log.Timber
private val regex = Regex("^(==|<=|>=|<|>)?(\\d*)$") 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 { override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveRoomMemberCountCondition(this) return conditionResolver.resolveRoomMemberCountCondition(event, this)
} }
override fun technicalDescription(): String { override fun technicalDescription(): String {
return "Room member count is $iz" return "Room member count is $iz"
} }
fun isSatisfied(event: Event, session: RoomService?): Boolean { internal fun isSatisfied(event: Event, roomGetter: RoomGetter): Boolean {
// sanity check^ // sanity checks
val roomId = event.roomId ?: return false 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 // Parse the is field into prefix and number the first time
val (prefix, count) = parseIsField() ?: return false 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 val (prefix, count) = match.destructured
return prefix to count.toInt() return prefix to count.toInt()
} catch (t: Throwable) { } catch (t: Throwable) {
Timber.d(t) Timber.e(t, "Unable to parse 'is' field")
} }
return null 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.model.PowerLevelsContent
import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper 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 { override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveSenderNotificationPermissionCondition(this) return conditionResolver.resolveSenderNotificationPermissionCondition(event, this)
} }
override fun technicalDescription(): String { override fun technicalDescription(): String {

View file

@ -22,7 +22,7 @@ import com.squareup.moshi.JsonClass
* All push rulesets for a user. * All push rulesets for a user.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class GetPushRulesResponse( internal data class GetPushRulesResponse(
/** /**
* Global rules, account level applying to all devices * 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.Json
import com.squareup.moshi.JsonClass 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 import timber.log.Timber
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
@ -30,13 +34,13 @@ data class PushCondition(
/** /**
* Required for event_match conditions. The dot- separated field of the event to match. * Required for event_match conditions. The dot- separated field of the event to match.
*/ */
val key: String? = null, val key: String? = null,
/**
*Required for event_match conditions.
*/
/**
* Required for event_match conditions.
*/
val pattern: String? = null, val pattern: String? = null,
/** /**
* Required for room_member_count conditions. * Required for room_member_count conditions.
* A decimal integer optionally prefixed by one of, ==, <, >, >= or <=. * A decimal integer optionally prefixed by one of, ==, <, >, >= or <=.
@ -47,30 +51,35 @@ data class PushCondition(
) { ) {
fun asExecutableCondition(): Condition? { fun asExecutableCondition(): Condition? {
return when (Condition.Kind.fromString(this.kind)) { return when (Condition.Kind.fromString(kind)) {
Condition.Kind.event_match -> { Condition.Kind.EventMatch -> {
if (this.key != null && this.pattern != null) { if (key != null && pattern != null) {
EventMatchCondition(key, pattern) EventMatchCondition(key, pattern)
} else { } else {
Timber.e("Malformed Event match condition") Timber.e("Malformed Event match condition")
null null
} }
} }
Condition.Kind.contains_display_name -> { Condition.Kind.ContainsDisplayName -> {
ContainsDisplayNameCondition() ContainsDisplayNameCondition()
} }
Condition.Kind.room_member_count -> { Condition.Kind.RoomMemberCount -> {
if (this.iz.isNullOrBlank()) { if (iz.isNullOrEmpty()) {
Timber.e("Malformed ROOM_MEMBER_COUNT condition") Timber.e("Malformed ROOM_MEMBER_COUNT condition")
null null
} else { } else {
RoomMemberCountCondition(this.iz) RoomMemberCountCondition(iz)
} }
} }
Condition.Kind.sender_notification_permission -> { Condition.Kind.SenderNotificationPermission -> {
this.key?.let { SenderNotificationPermissionCondition(it) } 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") Timber.e("Unknown kind $kind")
null null
} }

View file

@ -18,7 +18,7 @@ package im.vector.matrix.android.api.pushrules.rest
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class Ruleset( internal data class Ruleset(
val content: List<PushRule>? = null, val content: List<PushRule>? = null,
val override: List<PushRule>? = null, val override: List<PushRule>? = null,
val room: 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 androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback 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.api.util.Optional
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustResult import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustResult
import im.vector.matrix.android.internal.crypto.crosssigning.UserTrustResult import im.vector.matrix.android.internal.crypto.crosssigning.UserTrustResult
@ -63,6 +62,4 @@ interface CrossSigningService {
fun checkDeviceTrust(otherUserId: String, fun checkDeviceTrust(otherUserId: String,
otherDeviceId: String, otherDeviceId: String,
locallyTrusted: Boolean?): DeviceTrustResult locallyTrusted: Boolean?): DeviceTrustResult
fun getTrustLevelForUsers(userIds: List<String>): RoomEncryptionTrustLevel
} }

View file

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

View file

@ -17,6 +17,7 @@
package im.vector.matrix.android.api.session.room.crypto package im.vector.matrix.android.api.session.room.crypto
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
interface RoomCryptoService { interface RoomCryptoService {
@ -26,5 +27,9 @@ interface RoomCryptoService {
fun shouldEncryptForInvitedMembers(): Boolean 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 import com.squareup.moshi.Json
/**
* Ref: https://matrix.org/docs/spec/client_server/latest#room-history-visibility
*/
enum class RoomHistoryVisibility { 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, @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 = "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 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 * 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 membership: Membership,
val userId: String, val userId: String,
val displayName: String? = null, val displayName: String? = null,
val avatarUrl: String? = null, val avatarUrl: String? = null
// TODO Warning: Will not be populated if not using RxRoom
val userEncryptionTrustLevel: RoomEncryptionTrustLevel? = null
) )

View file

@ -25,9 +25,9 @@ import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class MessageAudioContent( 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'. * 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, @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, @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 import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
interface MessageContent { interface MessageContent {
// TODO Rename to msgType val msgType: String
val type: String
val body: String val body: String
val relatesTo: RelationDefaultContent? val relatesTo: RelationDefaultContent?
val newContent: Content? val newContent: Content?

View file

@ -23,7 +23,7 @@ import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultC
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class MessageDefaultContent( 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 = "body") override val body: String,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = 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) @JsonClass(generateAdapter = true)
data class MessageEmoteContent( 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, @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, @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 = "formatted_body") val formattedBody: String? = null,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null @Json(name = "m.new_content") override val newContent: Content? = null
) : MessageContent ) : MessageContent

View file

@ -23,10 +23,13 @@ import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
*/ */
interface MessageEncryptedContent : MessageContent { 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? val url: String?
/**
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
*/
val encryptedFileInfo: EncryptedFileInfo? val encryptedFileInfo: EncryptedFileInfo?
} }

View file

@ -26,9 +26,9 @@ import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class MessageFileContent( 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. * 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, @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 = "url") override val url: String? = null,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = 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 @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
) : MessageEncryptedContent { ) : 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,10 +14,8 @@
* limitations under the License. * 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( object MessageFormat {
val filename: String, const val FORMAT_MATRIX_HTML = "org.matrix.custom.html"
val fileSizeInBytes: Long, }
val homeServerLimitInBytes: Long
)

View file

@ -27,7 +27,7 @@ data class MessageImageContent(
/** /**
* Required. Must be 'm.image'. * 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, * 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, @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, @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) @JsonClass(generateAdapter = true)
data class MessageLocationContent( 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'. * 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) @JsonClass(generateAdapter = true)
data class MessageNoticeContent( 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, @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, @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 = "formatted_body") val formattedBody: String? = null,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null @Json(name = "m.new_content") override val newContent: Content? = null
) : MessageContent ) : MessageContent

View file

@ -12,7 +12,6 @@
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*
*/ */
package im.vector.matrix.android.api.session.room.model.message package im.vector.matrix.android.api.session.room.model.message
@ -28,7 +27,7 @@ data class MessageStickerContent(
/** /**
* Set in local, not from server * 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, * 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, @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, @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) @JsonClass(generateAdapter = true)
data class MessageTextContent( 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, @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, @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 = "formatted_body") val formattedBody: String? = null,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null @Json(name = "m.new_content") override val newContent: Content? = null
) : MessageContent ) : MessageContent

View file

@ -17,7 +17,6 @@
package im.vector.matrix.android.api.session.room.model.message package im.vector.matrix.android.api.session.room.model.message
object MessageType { object MessageType {
const val MSGTYPE_TEXT = "m.text" const val MSGTYPE_TEXT = "m.text"
const val MSGTYPE_EMOTE = "m.emote" const val MSGTYPE_EMOTE = "m.emote"
const val MSGTYPE_NOTICE = "m.notice" const val MSGTYPE_NOTICE = "m.notice"
@ -27,7 +26,6 @@ object MessageType {
const val MSGTYPE_LOCATION = "m.location" const val MSGTYPE_LOCATION = "m.location"
const val MSGTYPE_FILE = "m.file" const val MSGTYPE_FILE = "m.file"
const val MSGTYPE_VERIFICATION_REQUEST = "m.key.verification.request" 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 // 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 // Because sticker isn't a message type but a event type without msgtype field
const val MSGTYPE_STICKER_LOCAL = "org.matrix.android.sdk.sticker" 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) @JsonClass(generateAdapter = true)
data class MessageVerificationRequestContent( 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 = "body") override val body: String,
@Json(name = "from_device") override val fromDevice: String?, @Json(name = "from_device") override val fromDevice: String?,
@Json(name = "methods") override val methods: List<String>, @Json(name = "methods") override val methods: List<String>,

View file

@ -27,7 +27,7 @@ data class MessageVideoContent(
/** /**
* Required. Must be 'm.video'. * 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'. * 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, @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, @Json(name = "url") override val url: String? = null,

View file

@ -26,10 +26,16 @@ import im.vector.matrix.android.api.util.Optional
*/ */
interface ReadService { interface ReadService {
enum class MarkAsReadParams {
READ_RECEIPT,
READ_MARKER,
BOTH
}
/** /**
* Force the read marker to be set on the latest event. * 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. * 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 package im.vector.matrix.android.api.session.room.send
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
interface DraftService { interface DraftService {
/** /**
* Save or update a draft to the room * 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 * 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 * 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>) fun updateTopic(topic: String, callback: MatrixCallback<Unit>)
/** fun getStateEvent(eventType: String, stateKey: String): Event?
* Enable encryption of the room
*/
fun enableEncryption(algorithm: String, callback: MatrixCallback<Unit>)
fun getStateEvent(eventType: String): Event? fun getStateEventLive(eventType: String, stateKey: String): LiveData<Optional<Event>>
fun getStateEventLive(eventType: String): LiveData<Optional<Event>>
} }

View file

@ -112,6 +112,11 @@ interface Timeline {
* Called whenever an error we can't recover from occurred * Called whenever an error we can't recover from occurred
*/ */
fun onTimelineFailure(throwable: Throwable) 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( data class TimelineEvent(
val root: Event, val root: Event,
val localId: Long, val localId: Long,
val eventId: String,
val displayIndex: Int, val displayIndex: Int,
val senderName: String?, val senderName: String?,
val isUniqueDisplayName: Boolean, val isUniqueDisplayName: Boolean,

View file

@ -19,10 +19,11 @@ package im.vector.matrix.android.internal.crypto
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
import dagger.Provides 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.CryptoService
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService 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.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.crosssigning.DefaultCrossSigningService
import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask import im.vector.matrix.android.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask
@ -137,15 +138,6 @@ internal abstract class CryptoModule {
return RealmClearCacheTask(realmConfiguration) return RealmClearCacheTask(realmConfiguration)
} }
@JvmStatic
@Provides
fun providesCryptoStore(@CryptoDatabase
realmConfiguration: RealmConfiguration, credentials: Credentials): IMXCryptoStore {
return RealmCryptoStore(
realmConfiguration,
credentials)
}
@JvmStatic @JvmStatic
@Provides @Provides
@SessionScope @SessionScope
@ -159,13 +151,6 @@ internal abstract class CryptoModule {
fun providesRoomKeysAPI(retrofit: Retrofit): RoomKeysApi { fun providesRoomKeysAPI(retrofit: Retrofit): RoomKeysApi {
return retrofit.create(RoomKeysApi::class.java) return retrofit.create(RoomKeysApi::class.java)
} }
@JvmStatic
@Provides
@SessionScope
fun providesCryptoConfig(): MXCryptoConfig {
return MXCryptoConfig()
}
} }
@Binds @Binds
@ -256,4 +241,10 @@ internal abstract class CryptoModule {
@Binds @Binds
abstract fun bindCrossSigningService(crossSigningService: DefaultCrossSigningService): CrossSigningService 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 com.zhuinden.monarchy.Monarchy
import dagger.Lazy import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback 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.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.failure.Failure
import im.vector.matrix.android.api.listeners.ProgressListener import im.vector.matrix.android.api.listeners.ProgressListener
import im.vector.matrix.android.api.session.crypto.CryptoService 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.crypto.verification.DefaultVerificationService
import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.model.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.di.MoshiProvider
import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
@ -116,7 +118,7 @@ internal class DefaultCryptoService @Inject constructor(
// Olm device // Olm device
private val olmDevice: MXOlmDevice, private val olmDevice: MXOlmDevice,
// Set of parameters used to configure/customize the end-to-end crypto. // Set of parameters used to configure/customize the end-to-end crypto.
private val cryptoConfig: MXCryptoConfig = MXCryptoConfig(), private val mxCryptoConfig: MXCryptoConfig,
// Device list manager // Device list manager
private val deviceListManager: DeviceListManager, private val deviceListManager: DeviceListManager,
// The key backup service. // The key backup service.
@ -189,7 +191,7 @@ internal class DefaultCryptoService @Inject constructor(
this.callback = object : MatrixCallback<Unit> { this.callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
// bg refresh of crypto device // bg refresh of crypto device
downloadKeys(listOf(credentials.userId), true, object : MatrixCallback<MXUsersDevicesMap<CryptoDeviceInfo>> {}) downloadKeys(listOf(credentials.userId), true, NoOpMatrixCallback())
callback.onSuccess(data) callback.onSuccess(data)
} }
@ -399,6 +401,7 @@ internal class DefaultCryptoService @Inject constructor(
null null
} }
} }
override fun getCryptoDeviceInfo(userId: String): List<CryptoDeviceInfo> { override fun getCryptoDeviceInfo(userId: String): List<CryptoDeviceInfo> {
return cryptoStore.getUserDevices(userId)?.map { it.value } ?: emptyList() return cryptoStore.getUserDevices(userId)?.map { it.value } ?: emptyList()
} }
@ -531,7 +534,7 @@ internal class DefaultCryptoService @Inject constructor(
*/ */
override fun isRoomEncrypted(roomId: String): Boolean { override fun isRoomEncrypted(roomId: String): Boolean {
val encryptionEvent = monarchy.fetchCopied { realm -> 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\"") .contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
.findFirst() .findFirst()
} }
@ -545,8 +548,8 @@ internal class DefaultCryptoService @Inject constructor(
return cryptoStore.getUserDevices(userId)?.values?.toMutableList() ?: ArrayList() return cryptoStore.getUserDevices(userId)?.values?.toMutableList() ?: ArrayList()
} }
fun isEncryptionEnabledForInvitedUser(): Boolean { private fun isEncryptionEnabledForInvitedUser(): Boolean {
return cryptoConfig.enableEncryptionForInvitedMembers return mxCryptoConfig.enableEncryptionForInvitedMembers
} }
override fun getEncryptionAlgorithm(roomId: String): String? { override fun getEncryptionAlgorithm(roomId: String): String? {
@ -779,7 +782,7 @@ internal class DefaultCryptoService @Inject constructor(
deviceListManager.startTrackingDeviceList(listOf(userId)) deviceListManager.startTrackingDeviceList(listOf(userId))
} else if (membership == Membership.INVITE } else if (membership == Membership.INVITE
&& shouldEncryptForInvitedMembers(roomId) && shouldEncryptForInvitedMembers(roomId)
&& cryptoConfig.enableEncryptionForInvitedMembers) { && isEncryptionEnabledForInvitedUser()) {
// track the deviceList for this invited user. // track the deviceList for this invited user.
// Caution: there's a big edge case here in that federated servers do not // 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. // 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.crypto.tasks.DownloadKeysForUsersTask
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.sync.SyncTokenStore 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 timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -36,10 +39,12 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
private val olmDevice: MXOlmDevice, private val olmDevice: MXOlmDevice,
private val syncTokenStore: SyncTokenStore, private val syncTokenStore: SyncTokenStore,
private val credentials: Credentials, private val credentials: Credentials,
private val downloadKeysForUsersTask: DownloadKeysForUsersTask) { private val downloadKeysForUsersTask: DownloadKeysForUsersTask,
coroutineDispatchers: MatrixCoroutineDispatchers,
taskExecutor: TaskExecutor) {
interface UserDevicesUpdateListener { interface UserDevicesUpdateListener {
fun onUsersDeviceUpdate(users: List<String>) fun onUsersDeviceUpdate(userIds: List<String>)
} }
private val deviceChangeListeners = mutableListOf<UserDevicesUpdateListener>() private val deviceChangeListeners = mutableListOf<UserDevicesUpdateListener>()
@ -72,17 +77,19 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
private val notReadyToRetryHS = mutableSetOf<String>() private val notReadyToRetryHS = mutableSetOf<String>()
init { init {
var isUpdated = false taskExecutor.executorScope.launch(coroutineDispatchers.crypto) {
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() var isUpdated = false
for ((userId, status) in deviceTrackingStatuses) { val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
if (TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == status || TRACKING_STATUS_UNREACHABLE_SERVER == status) { for ((userId, status) in deviceTrackingStatuses) {
// if a download was in progress when we got shut down, it isn't any more. if (TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == status || TRACKING_STATUS_UNREACHABLE_SERVER == status) {
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD // if a download was in progress when we got shut down, it isn't any more.
isUpdated = true 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 = // al devices =
val models = response.deviceKeys?.get(userId)?.mapValues { entry -> CryptoInfoMapper.map(entry.value) } val models = response.deviceKeys?.get(userId)?.mapValues { entry -> CryptoInfoMapper.map(entry.value) }
Timber.v("## doKeyDownloadForUsers() : Got keys for $userId : $models") Timber.v("## doKeyDownloadForUsers() : Got keys for $userId : $models")
if (!models.isNullOrEmpty()) { if (!models.isNullOrEmpty()) {
val workingCopy = models.toMutableMap() 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 androidx.lifecycle.LiveData
import dagger.Lazy import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback 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.CrossSigningService
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
import im.vector.matrix.android.api.util.Optional 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.crypto.tasks.UploadSigningKeysTask
import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.session.SessionScope 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.TaskExecutor
import im.vector.matrix.android.internal.task.TaskThread
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.JsonCanonicalizer import im.vector.matrix.android.internal.util.JsonCanonicalizer
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.withoutPrefix import im.vector.matrix.android.internal.util.withoutPrefix
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.matrix.olm.OlmPkSigning import org.matrix.olm.OlmPkSigning
import org.matrix.olm.OlmUtility import org.matrix.olm.OlmUtility
import timber.log.Timber import timber.log.Timber
@ -57,9 +57,11 @@ internal class DefaultCrossSigningService @Inject constructor(
private val deviceListManager: DeviceListManager, private val deviceListManager: DeviceListManager,
private val uploadSigningKeysTask: UploadSigningKeysTask, private val uploadSigningKeysTask: UploadSigningKeysTask,
private val uploadSignaturesTask: UploadSignaturesTask, private val uploadSignaturesTask: UploadSignaturesTask,
private val cryptoCoroutineScope: CoroutineScope, private val computeTrustTask: ComputeTrustTask,
private val taskExecutor: TaskExecutor,
private val coroutineDispatchers: MatrixCoroutineDispatchers, 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 private var olmUtility: OlmUtility? = null
@ -211,7 +213,7 @@ internal class DefaultCrossSigningService @Inject constructor(
cryptoStore.storePrivateKeysInfo(masterKeyPrivateKey?.toBase64NoPadding(), uskPrivateKey?.toBase64NoPadding(), sskPrivateKey?.toBase64NoPadding()) cryptoStore.storePrivateKeysInfo(masterKeyPrivateKey?.toBase64NoPadding(), uskPrivateKey?.toBase64NoPadding(), sskPrivateKey?.toBase64NoPadding())
uploadSigningKeysTask.configureWith(params) { uploadSigningKeysTask.configureWith(params) {
this.constraints = TaskConstraints(true) this.executionThread = TaskThread.CRYPTO
this.callback = object : MatrixCallback<Unit> { this.callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
Timber.i("## CrossSigning - Keys successfully uploaded") Timber.i("## CrossSigning - Keys successfully uploaded")
@ -247,7 +249,7 @@ internal class DefaultCrossSigningService @Inject constructor(
resetTrustOnKeyChange() resetTrustOnKeyChange()
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadSignatureQueryBuilder.build())) { uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadSignatureQueryBuilder.build())) {
// this.retryCount = 3 // this.retryCount = 3
this.constraints = TaskConstraints(true) this.executionThread = TaskThread.CRYPTO
this.callback = object : MatrixCallback<Unit> { this.callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
Timber.i("## CrossSigning - signatures successfully uploaded") Timber.i("## CrossSigning - signatures successfully uploaded")
@ -396,7 +398,7 @@ internal class DefaultCrossSigningService @Inject constructor(
return@forEach return@forEach
} catch (failure: Throwable) { } catch (failure: Throwable) {
// log // 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)) .withSigningKeyInfo(otherMasterKeys.copyForSignature(userId, userPubKey, newSignature))
.build() .build()
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) { uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) {
this.executionThread = TaskThread.CRYPTO
this.callback = callback this.callback = callback
}.executeBy(taskExecutor) }.executeBy(taskExecutor)
} }
@ -546,6 +549,7 @@ internal class DefaultCrossSigningService @Inject constructor(
.withDeviceInfo(toUpload) .withDeviceInfo(toUpload)
.build() .build()
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) { uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) {
this.executionThread = TaskThread.CRYPTO
this.callback = callback this.callback = callback
}.executeBy(taskExecutor) }.executeBy(taskExecutor)
} }
@ -611,81 +615,52 @@ internal class DefaultCrossSigningService @Inject constructor(
} }
} }
override fun onUsersDeviceUpdate(users: List<String>) { override fun onUsersDeviceUpdate(userIds: List<String>) {
Timber.d("## CrossSigning - onUsersDeviceUpdate for ${users.size} users") cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
users.forEach { otherUserId -> 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 { // TODO if my keys have changes, i should recheck all devices of all users?
Timber.d("## CrossSigning - update trust for $otherUserId , verified=${it.isVerified()}") val devices = cryptoStore.getUserDeviceList(otherUserId)
setUserKeysAsTrusted(otherUserId, it.isVerified()) 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? if (otherUserId == userId) {
val devices = cryptoStore.getUserDeviceList(otherUserId) // It's me, i should check if a newly trusted device is signing my master key
devices?.forEach { device -> // In this case it will change my MSK trust, and should then re-trigger a check of all other user trust
val updatedTrust = checkDeviceTrust(otherUserId, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false) setUserKeysAsTrusted(otherUserId, checkSelfTrust().isVerified())
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) { eventBus.post(CryptoToSessionUserTrustChange(userIds))
// 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())
} }
} }
} }
private fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) { private fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) {
val currentTrust = cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted() cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
cryptoStore.setUserKeysAsTrusted(otherUserId, trusted) val currentTrust = cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted()
cryptoStore.setUserKeysAsTrusted(otherUserId, trusted)
// If it's me, recheck trust of all users and devices? // If it's me, recheck trust of all users and devices?
val users = ArrayList<String>() val users = ArrayList<String>()
if (otherUserId == userId && currentTrust != trusted) { if (otherUserId == userId && currentTrust != trusted) {
cryptoStore.updateUsersTrust { cryptoStore.updateUsersTrust {
users.add(it) users.add(it)
checkUserTrust(it).isVerified() 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())
} }
}
}
}
override fun getTrustLevelForUsers(userIds: List<String>): RoomEncryptionTrustLevel { users.forEach {
val allTrusted = userIds cryptoStore.getUserDeviceList(it)?.forEach { device ->
.filter { getUserCrossSigningKeys(it)?.isTrusted() == true } val updatedTrust = checkDeviceTrust(it, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false)
Timber.d("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust")
val allUsersAreVerified = userIds.size == allTrusted.size cryptoStore.setDeviceTrust(it, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified())
}
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
} }
} }
} }

View file

@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * 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 * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * 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 * See the License for the specific language governing permissions and
* limitations under the License. * 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 CryptoToSessionUserTrustChange(
val userIds: List<String>
data class DownloadFileState( )
val mimeType: String,
val file: File?,
val throwable: Throwable?
)

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) olmDevice.verifySignature(fingerprint, authData.signalableJSONDictionary(), mySignature)
isSignatureValid = true isSignatureValid = true
} catch (e: OlmException) { } 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.get
import im.vector.matrix.android.internal.crypto.store.db.query.getById 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.crypto.store.db.query.getOrCreate
import im.vector.matrix.android.internal.di.CryptoDatabase
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
import io.realm.Realm import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
@ -70,11 +71,13 @@ import io.realm.kotlin.where
import org.matrix.olm.OlmAccount import org.matrix.olm.OlmAccount
import org.matrix.olm.OlmException import org.matrix.olm.OlmException
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject
import kotlin.collections.set import kotlin.collections.set
@SessionScope @SessionScope
internal class RealmCryptoStore(private val realmConfiguration: RealmConfiguration, internal class RealmCryptoStore @Inject constructor(
private val credentials: Credentials) : IMXCryptoStore { @CryptoDatabase private val realmConfiguration: RealmConfiguration,
private val credentials: Credentials) : IMXCryptoStore {
/* ========================================================================================== /* ==========================================================================================
* Memory cache, to correctly release JNI objects * Memory cache, to correctly release JNI objects
@ -403,14 +406,14 @@ internal class RealmCryptoStore(private val realmConfiguration: RealmConfigurati
{ realm: Realm -> { realm: Realm ->
realm realm
.where<UserEntity>() .where<UserEntity>()
.`in`(UserEntityFields.USER_ID, userIds.toTypedArray()) .`in`(UserEntityFields.USER_ID, userIds.distinct().toTypedArray())
}, },
{ entity -> { entity ->
entity.devices.map { CryptoMapper.mapToModel(it) } entity.devices.map { CryptoMapper.mapToModel(it) }
} }
) )
return Transformations.map(liveData) { 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 // done from another device of mine
if (EventType.MESSAGE == event.type) { 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) { if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) {
event.getClearContent().toModel<MessageVerificationRequestContent>()?.let { event.getClearContent().toModel<MessageVerificationRequestContent>()?.let {
if (it.fromDevice != deviceId) { if (it.fromDevice != deviceId) {
@ -144,7 +144,7 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
params.verificationService.onRoomEvent(event) params.verificationService.onRoomEvent(event)
} }
EventType.MESSAGE -> { 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) params.verificationService.onRoomRequestReceived(event)
} }
} }

View file

@ -37,7 +37,9 @@ internal class DefaultUploadSignaturesTask @Inject constructor(
override suspend fun execute(params: UploadSignaturesTask.Params) { override suspend fun execute(params: UploadSignaturesTask.Params) {
try { try {
val response = executeRequest<SignatureUploadResponse>(eventBus) { 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) { if (response.failures?.isNotEmpty() == true) {
throw Throwable(response.failures.toString()) throw Throwable(response.failures.toString())

View file

@ -165,7 +165,7 @@ internal class DefaultVerificationService @Inject constructor(
onRoomDoneReceived(event) onRoomDoneReceived(event)
} }
EventType.MESSAGE -> { EventType.MESSAGE -> {
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.type) { if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.msgType) {
onRoomRequestReceived(event) 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.RealmLiveEntityObserver
import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.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.di.SessionDatabase
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
@ -42,7 +42,7 @@ internal class VerificationMessageLiveObserver @Inject constructor(
) : RealmLiveEntityObserver<EventEntity>(realmConfiguration) { ) : RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
override val query = Monarchy.Query { override val query = Monarchy.Query {
EventEntity.types(it, listOf( EventEntity.whereTypes(it, listOf(
EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_KEY, EventType.KEY_VERIFICATION_KEY,

View file

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

View file

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

View file

@ -16,15 +16,8 @@
package im.vector.matrix.android.internal.database.helper package im.vector.matrix.android.internal.database.helper
import im.vector.matrix.android.api.session.events.model.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.ChunkEntity
import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.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) { internal fun RoomEntity.deleteOnCascade(chunkEntity: ChunkEntity) {
chunks.remove(chunkEntity) chunks.remove(chunkEntity)
@ -36,39 +29,3 @@ internal fun RoomEntity.addOrUpdate(chunkEntity: ChunkEntity) {
chunks.add(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.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.UnsignedData 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.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.MoshiProvider
@ -43,7 +44,6 @@ internal object EventMapper {
eventEntity.redacts = event.redacts eventEntity.redacts = event.redacts
eventEntity.age = event.unsignedData?.age ?: event.originServerTs eventEntity.age = event.unsignedData?.age ?: event.originServerTs
eventEntity.unsignedData = uds eventEntity.unsignedData = uds
eventEntity.ageLocalTs = event.ageLocalTs
return eventEntity return eventEntity
} }
@ -92,6 +92,9 @@ internal fun EventEntity.asDomain(): Event {
return EventMapper.map(this) return EventMapper.map(this)
} }
internal fun Event.toEntity(roomId: String): EventEntity { internal fun Event.toEntity(roomId: String, sendState: SendState, ageLocalTs: Long? = null): EventEntity {
return EventMapper.map(this, roomId) return EventMapper.map(this, roomId).apply {
this.sendState = sendState
this.ageLocalTs = ageLocalTs
}
} }

View file

@ -38,7 +38,7 @@ internal object PushRulesMapper {
enabled = pushrule.enabled, enabled = pushrule.enabled,
ruleId = pushrule.ruleId, ruleId = pushrule.ruleId,
conditions = listOf( 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, enabled = pushrule.enabled,
ruleId = pushrule.ruleId, ruleId = pushrule.ruleId,
conditions = listOf( 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, enabled = pushrule.enabled,
ruleId = pushrule.ruleId, ruleId = pushrule.ruleId,
conditions = listOf( 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, userId = roomMemberSummaryEntity.userId,
avatarUrl = roomMemberSummaryEntity.avatarUrl, avatarUrl = roomMemberSummaryEntity.avatarUrl,
displayName = roomMemberSummaryEntity.displayName, displayName = roomMemberSummaryEntity.displayName,
membership = roomMemberSummaryEntity.membership, membership = roomMemberSummaryEntity.membership
userEncryptionTrustLevel = null
) )
} }
} }

View file

@ -17,11 +17,11 @@
package im.vector.matrix.android.internal.database.mapper 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.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.RoomSummary
import im.vector.matrix.android.api.session.room.model.tag.RoomTag 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.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import timber.log.Timber
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
@ -49,7 +49,8 @@ internal class RoomSummaryMapper @Inject constructor(
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain 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(), aliases = roomSummaryEntity.aliases.toList(),
isEncrypted = roomSummaryEntity.isEncrypted, isEncrypted = roomSummaryEntity.isEncrypted,
typingRoomMemberIds = roomSummaryEntity.typingUserIds.toList(), 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( return TimelineEvent(
root = timelineEventEntity.root?.asDomain() root = timelineEventEntity.root?.asDomain()
?: Event("", timelineEventEntity.eventId), ?: Event("", timelineEventEntity.eventId),
eventId = timelineEventEntity.eventId,
annotations = timelineEventEntity.annotations?.asDomain(), annotations = timelineEventEntity.annotations?.asDomain(),
localId = timelineEventEntity.localId, localId = timelineEventEntity.localId,
displayIndex = timelineEventEntity.root?.displayIndex ?: 0, displayIndex = timelineEventEntity.displayIndex,
senderName = timelineEventEntity.senderName, senderName = timelineEventEntity.senderName,
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName, isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
senderAvatar = timelineEventEntity.senderAvatar, senderAvatar = timelineEventEntity.senderAvatar,
readReceipts = readReceipts?.sortedByDescending { readReceipts = readReceipts
it.originServerTs ?.distinctBy {
} ?: emptyList() 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, internal open class ChunkEntity(@Index var prevToken: String? = null,
@Index var nextToken: String? = null, @Index var nextToken: String? = null,
var stateEvents: RealmList<EventEntity> = RealmList(),
var timelineEvents: RealmList<TimelineEventEntity> = RealmList(), var timelineEvents: RealmList<TimelineEventEntity> = RealmList(),
@Index var isLastForward: Boolean = false, @Index var isLastForward: Boolean = false,
@Index var isLastBackward: 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
) : RealmObject() { ) : RealmObject() {
fun identifier() = "${prevToken}_$nextToken" 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.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.MoshiProvider
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.Index 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 roomId: String = "",
@Index var type: String = "", @Index var type: String = "",
var content: String? = null, var content: String? = null,
@ -36,20 +35,11 @@ internal open class EventEntity(@Index var eventId: String = "",
var age: Long? = 0, var age: Long? = 0,
var unsignedData: String? = null, var unsignedData: String? = null,
var redacts: 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 decryptionResultJson: String? = null,
var decryptionErrorCode: String? = null, var decryptionErrorCode: String? = null,
var ageLocalTs: Long? = null var ageLocalTs: Long? = null
) : RealmObject() { ) : RealmObject() {
enum class LinkFilterMode {
LINKED_ONLY,
UNLINKED_ONLY,
BOTH
}
private var sendStateStr: String = SendState.UNKNOWN.name private var sendStateStr: String = SendState.UNKNOWN.name
var sendState: SendState var sendState: SendState
@ -62,12 +52,6 @@ internal open class EventEntity(@Index var eventId: String = "",
companion object companion object
@LinkingObjects("untimelinedStateEvents")
val room: RealmResults<RoomEntity>? = null
@LinkingObjects("root")
val timelineEventEntity: RealmResults<TimelineEventEntity>? = null
fun setDecryptionResult(result: MXEventDecryptionResult) { fun setDecryptionResult(result: MXEventDecryptionResult) {
val decryptionResult = OlmDecryptionResult( val decryptionResult = OlmDecryptionResult(
payload = result.clearEvent, payload = result.clearEvent,
@ -78,6 +62,5 @@ internal open class EventEntity(@Index var eventId: String = "",
val adapter = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java) val adapter = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java)
decryptionResultJson = adapter.toJson(decryptionResult) decryptionResultJson = adapter.toJson(decryptionResult)
decryptionErrorCode = null 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 = "", internal open class RoomEntity(@PrimaryKey var roomId: String = "",
var chunks: RealmList<ChunkEntity> = RealmList(), var chunks: RealmList<ChunkEntity> = RealmList(),
var untimelinedStateEvents: RealmList<EventEntity> = RealmList(),
var sendingTimelineEvents: RealmList<TimelineEventEntity> = RealmList(), var sendingTimelineEvents: RealmList<TimelineEventEntity> = RealmList(),
var areAllMembersLoaded: Boolean = false var areAllMembersLoaded: Boolean = false
) : RealmObject() { ) : RealmObject() {

View file

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

View file

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

View file

@ -24,6 +24,7 @@ import io.realm.annotations.LinkingObjects
internal open class TimelineEventEntity(var localId: Long = 0, internal open class TimelineEventEntity(var localId: Long = 0,
@Index var eventId: String = "", @Index var eventId: String = "",
@Index var roomId: String = "", @Index var roomId: String = "",
@Index var displayIndex: Int = 0,
var root: EventEntity? = null, var root: EventEntity? = null,
var annotations: EventAnnotationsSummaryEntity? = null, var annotations: EventAnnotationsSummaryEntity? = null,
var senderName: String? = 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( internal fun ChunkEntity.Companion.create(
realm: Realm, realm: Realm,
prevToken: String?, prevToken: String?,
nextToken: String?, nextToken: String?
isUnlinked: Boolean
): ChunkEntity { ): ChunkEntity {
return realm.createObject<ChunkEntity>().apply { return realm.createObject<ChunkEntity>().apply {
this.prevToken = prevToken this.prevToken = prevToken
this.nextToken = nextToken this.nextToken = nextToken
this.isUnlinked = isUnlinked
} }
} }

View file

@ -0,0 +1,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 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
import im.vector.matrix.android.internal.database.model.EventEntity.LinkFilterMode.*
import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.model.EventEntityFields
import io.realm.Realm import io.realm.Realm
import io.realm.RealmList import io.realm.RealmList
import io.realm.RealmQuery import io.realm.RealmQuery
import io.realm.Sort
import io.realm.kotlin.where import io.realm.kotlin.where
internal fun EventEntity.Companion.where(realm: Realm, eventId: String): RealmQuery<EventEntity> { 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()) .`in`(EventEntityFields.EVENT_ID, eventIds.toTypedArray())
} }
internal fun EventEntity.Companion.where(realm: Realm, internal fun EventEntity.Companion.whereType(realm: Realm,
roomId: String? = null, type: String,
type: String? = null, roomId: String? = null
linkFilterMode: EventEntity.LinkFilterMode = LINKED_ONLY): RealmQuery<EventEntity> { ): RealmQuery<EventEntity> {
val query = realm.where<EventEntity>() val query = realm.where<EventEntity>()
if (roomId != null) { if (roomId != null) {
query.equalTo(EventEntityFields.ROOM_ID, roomId) query.equalTo(EventEntityFields.ROOM_ID, roomId)
} }
if (type != null) { return query.equalTo(EventEntityFields.TYPE, type)
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
}
} }
internal fun EventEntity.Companion.types(realm: Realm, internal fun EventEntity.Companion.whereTypes(realm: Realm,
typeList: List<String> = emptyList()): RealmQuery<EventEntity> { typeList: List<String> = emptyList(),
roomId: String? = null): RealmQuery<EventEntity> {
val query = realm.where<EventEntity>() val query = realm.where<EventEntity>()
query.`in`(EventEntityFields.TYPE, typeList.toTypedArray()) query.`in`(EventEntityFields.TYPE, typeList.toTypedArray())
if (roomId != null) {
query.equalTo(EventEntityFields.ROOM_ID, roomId)
}
return query 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? { internal fun RealmList<EventEntity>.find(eventId: String): EventEntity? {
return this.where() return this.where()
.equalTo(EventEntityFields.EVENT_ID, eventId) .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.ChunkEntity
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity 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.ReadReceiptEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import io.realm.Realm import io.realm.Realm
internal fun isEventRead(monarchy: Monarchy, internal fun isEventRead(monarchy: Monarchy,
@ -36,16 +37,15 @@ internal fun isEventRead(monarchy: Monarchy,
monarchy.doWithRealm { realm -> monarchy.doWithRealm { realm ->
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) ?: return@doWithRealm val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) ?: return@doWithRealm
val eventToCheck = liveChunk.timelineEvents.find(eventId)?.root val eventToCheck = liveChunk.timelineEvents.find(eventId)
isEventRead = if (eventToCheck == null || eventToCheck.root?.sender == userId) {
isEventRead = if (eventToCheck?.sender == userId) {
true true
} else { } else {
val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst() val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst()
?: return@doWithRealm ?: return@doWithRealm
val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.displayIndex
?: Int.MIN_VALUE ?: Int.MIN_VALUE
val eventToCheckIndex = eventToCheck?.displayIndex ?: Int.MAX_VALUE val eventToCheckIndex = eventToCheck.displayIndex
eventToCheckIndex <= readReceiptIndex eventToCheckIndex <= readReceiptIndex
} }
@ -61,13 +61,17 @@ internal fun isReadMarkerMoreRecent(monarchy: Monarchy,
return false return false
} }
return Realm.getInstance(monarchy.realmConfiguration).use { realm -> return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) ?: return false val eventToCheck = TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId).findFirst()
val eventToCheck = liveChunk.timelineEvents.find(eventId)?.root val eventToCheckChunk = eventToCheck?.chunk?.firstOrNull()
val readMarker = ReadMarkerEntity.where(realm, roomId).findFirst() ?: return false val readMarker = ReadMarkerEntity.where(realm, roomId).findFirst() ?: return false
val readMarkerIndex = liveChunk.timelineEvents.find(readMarker.eventId)?.root?.displayIndex val readMarkerEvent = TimelineEventEntity.where(realm, roomId = roomId, eventId = readMarker.eventId).findFirst()
?: Int.MIN_VALUE val readMarkerChunk = readMarkerEvent?.chunk?.firstOrNull()
val eventToCheckIndex = eventToCheck?.displayIndex ?: Int.MAX_VALUE if (eventToCheckChunk == readMarkerChunk) {
eventToCheckIndex <= readMarkerIndex 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.api.session.room.send.SendState
import im.vector.matrix.android.internal.database.model.* import im.vector.matrix.android.internal.database.model.*
import im.vector.matrix.android.internal.database.model.EventEntity.LinkFilterMode.*
import io.realm.* import io.realm.*
import io.realm.kotlin.where 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()) .`in`(TimelineEventEntityFields.EVENT_ID, eventIds.toTypedArray())
} }
internal fun TimelineEventEntity.Companion.where(realm: Realm, internal fun TimelineEventEntity.Companion.whereRoomId(realm: Realm,
roomId: String? = null, roomId: String): RealmQuery<TimelineEventEntity> {
type: String? = null, return realm.where<TimelineEventEntity>()
linkFilterMode: EventEntity.LinkFilterMode = LINKED_ONLY): RealmQuery<TimelineEventEntity> { .equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
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.findWithSenderMembershipEvent(realm: Realm, senderMembershipEventId: String): List<TimelineEventEntity> { internal fun TimelineEventEntity.Companion.findWithSenderMembershipEvent(realm: Realm, senderMembershipEventId: String): List<TimelineEventEntity> {
@ -71,7 +58,7 @@ internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm,
liveEvents liveEvents
} }
return query return query
?.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING) ?.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
?.findFirst() ?.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? { internal fun RealmList<TimelineEventEntity>.find(eventId: String): TimelineEventEntity? {
return this.where() return this.where()
.equalTo(TimelineEventEntityFields.EVENT_ID, eventId) .equalTo(TimelineEventEntityFields.EVENT_ID, eventId)

View file

@ -22,11 +22,11 @@ import com.squareup.moshi.Moshi
import dagger.BindsInstance import dagger.BindsInstance
import dagger.Component import dagger.Component
import im.vector.matrix.android.api.Matrix 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.api.auth.AuthenticationService
import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.auth.AuthModule import im.vector.matrix.android.internal.auth.AuthModule
import im.vector.matrix.android.internal.auth.SessionParamsStore 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.task.TaskExecutor
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
@ -48,6 +48,8 @@ internal interface MatrixComponent {
fun context(): Context fun context(): Context
fun matrixConfiguration(): MatrixConfiguration
fun resources(): Resources fun resources(): Resources
fun olmManager(): OlmManager fun olmManager(): OlmManager
@ -56,8 +58,6 @@ internal interface MatrixComponent {
fun sessionParamsStore(): SessionParamsStore fun sessionParamsStore(): SessionParamsStore
fun networkConnectivityChecker(): NetworkConnectivityChecker
fun backgroundDetectionObserver(): BackgroundDetectionObserver fun backgroundDetectionObserver(): BackgroundDetectionObserver
fun sessionManager(): SessionManager fun sessionManager(): SessionManager
@ -66,6 +66,7 @@ internal interface MatrixComponent {
@Component.Factory @Component.Factory
interface 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 package im.vector.matrix.android.internal.network
import android.content.Context
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import com.novoda.merlin.Merlin import im.vector.matrix.android.internal.session.SessionScope
import com.novoda.merlin.MerlinsBeard import im.vector.matrix.android.internal.session.homeserver.HomeServerPinger
import im.vector.matrix.android.internal.di.MatrixScope
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
import timber.log.Timber import kotlinx.coroutines.runBlocking
import java.util.Collections import java.util.Collections
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject import javax.inject.Inject
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@MatrixScope interface NetworkConnectivityChecker {
internal class NetworkConnectivityChecker @Inject constructor(private val context: Context, /**
private val backgroundDetectionObserver: BackgroundDetectionObserver) * Returns true when internet is available
: BackgroundDetectionObserver.Listener { */
@WorkerThread
fun hasInternetAccess(forcePing: Boolean): Boolean
private val merlin = Merlin.Builder() fun register(listener: Listener)
.withConnectableCallbacks() fun unregister(listener: Listener)
.withDisconnectableCallbacks()
.build(context)
private val merlinsBeard = MerlinsBeard.Builder().build(context) interface Listener {
fun onConnectivityChanged()
}
}
private val listeners = Collections.synchronizedSet(LinkedHashSet<Listener>()) @SessionScope
private var hasInternetAccess = merlinsBeard.isConnected internal class DefaultNetworkConnectivityChecker @Inject constructor(private val homeServerPinger: HomeServerPinger,
private val backgroundDetectionObserver: BackgroundDetectionObserver,
private val networkCallbackStrategy: NetworkCallbackStrategy)
: NetworkConnectivityChecker {
init { private val hasInternetAccess = AtomicBoolean(true)
backgroundDetectionObserver.register(this) 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 * Returns true when internet is available
*/ */
@WorkerThread @WorkerThread
fun hasInternetAccess(): Boolean { override fun hasInternetAccess(forcePing: Boolean): Boolean {
// If we are in background we have unbound merlin, so we have to check return if (forcePing) {
return if (backgroundDetectionObserver.isInBackground) { runBlocking {
merlinsBeard.hasInternetAccess() homeServerPinger.canReachHomeServer()
}
} else { } else {
hasInternetAccess hasInternetAccess.get()
} }
} }
override fun onMoveToForeground() { override fun register(listener: NetworkConnectivityChecker.Listener) {
merlin.bind() if (listeners.isEmpty()) {
merlinsBeard.hasInternetAccess { if (backgroundDetectionObserver.isInBackground) {
hasInternetAccess = it unbind()
} } else {
merlin.registerDisconnectable { bind()
if (hasInternetAccess) {
Timber.v("On Disconnect")
hasInternetAccess = false
val localListeners = listeners.toList()
localListeners.forEach {
it.onDisconnect()
}
} }
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) listeners.add(listener)
} }
fun unregister(listener: Listener) { override fun unregister(listener: NetworkConnectivityChecker.Listener) {
listeners.remove(listener) listeners.remove(listener)
if (listeners.isEmpty()) {
backgroundDetectionObserver.unregister(backgroundDetectionObserverListener)
}
} }
interface Listener { private fun bind() {
fun onConnect() { 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 im.vector.matrix.android.api.failure.Failure
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import retrofit2.Call import retrofit2.Call
import java.io.IOException import java.io.IOException
@ -27,11 +28,17 @@ internal suspend inline fun <DATA> executeRequest(eventBus: EventBus?,
internal class Request<DATA>(private val 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> lateinit var apiCall: Call<DATA>
suspend fun execute(): DATA { suspend fun execute(): DATA {
return try { return try {
val response = apiCall.awaitResponse() val response = apiCall.clone().awaitResponse()
if (response.isSuccessful) { if (response.isSuccessful) {
response.body() response.body()
?: throw IllegalStateException("The request returned a null 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) throw response.toFailure(eventBus)
} }
} catch (exception: Throwable) { } catch (exception: Throwable) {
throw when (exception) { if (isRetryable && currentRetryCount++ < maxRetryCount && exception is IOException) {
is IOException -> Failure.NetworkConnection(exception) delay(currentDelay)
is Failure.ServerError, currentDelay = (currentDelay * 2L).coerceAtMost(maxDelay)
is Failure.OtherServerError -> exception return execute()
is CancellationException -> Failure.Cancelled(exception) } else {
else -> Failure.Unknown(exception) 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 android.content.Context
import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.MatrixConfiguration
import im.vector.matrix.android.internal.di.MatrixScope import im.vector.matrix.android.internal.di.MatrixScope
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@MatrixScope @MatrixScope
internal class UserAgentHolder @Inject constructor(private val context: Context) { internal class UserAgentHolder @Inject constructor(private val context: Context,
matrixConfiguration: MatrixConfiguration) {
var userAgent: String = "" var userAgent: String = ""
private set private set
init { init {
setApplicationFlavor("NoFlavor") setApplicationFlavor(matrixConfiguration.applicationFlavor)
} }
/** /**
@ -38,7 +40,7 @@ internal class UserAgentHolder @Inject constructor(private val context: Context)
* *
* @param flavorDescription the flavor description * @param flavorDescription the flavor description
*/ */
fun setApplicationFlavor(flavorDescription: String) { private fun setApplicationFlavor(flavorDescription: String) {
var appName = "" var appName = ""
var appVersion = "" 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.api.session.user.UserService
import im.vector.matrix.android.internal.auth.SessionParamsStore import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.crypto.DefaultCryptoService import im.vector.matrix.android.internal.crypto.DefaultCryptoService
import im.vector.matrix.android.internal.crypto.crosssigning.ShieldTrustUpdater
import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.database.LiveEntityObserver
import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.di.WorkManagerProvider import im.vector.matrix.android.internal.di.WorkManagerProvider
@ -89,7 +90,8 @@ internal class DefaultSession @Inject constructor(
private val sessionParamsStore: SessionParamsStore, private val sessionParamsStore: SessionParamsStore,
private val contentUploadProgressTracker: ContentUploadStateTracker, private val contentUploadProgressTracker: ContentUploadStateTracker,
private val initialSyncProgressService: Lazy<InitialSyncProgressService>, private val initialSyncProgressService: Lazy<InitialSyncProgressService>,
private val homeServerCapabilitiesService: Lazy<HomeServerCapabilitiesService>) private val homeServerCapabilitiesService: Lazy<HomeServerCapabilitiesService>,
private val shieldTrustUpdater: ShieldTrustUpdater)
: Session, : Session,
RoomService by roomService.get(), RoomService by roomService.get(),
RoomDirectoryService by roomDirectoryService.get(), RoomDirectoryService by roomDirectoryService.get(),
@ -119,6 +121,7 @@ internal class DefaultSession @Inject constructor(
isOpen = true isOpen = true
liveEntityObservers.forEach { it.start() } liveEntityObservers.forEach { it.start() }
eventBus.register(this) eventBus.register(this)
shieldTrustUpdater.start()
} }
override fun requireBackgroundSync() { override fun requireBackgroundSync() {
@ -160,6 +163,7 @@ internal class DefaultSession @Inject constructor(
isOpen = false isOpen = false
eventBus.unregister(this) eventBus.unregister(this)
syncTaskSequencer.close() syncTaskSequencer.close()
shieldTrustUpdater.stop()
} }
override fun getSyncStateLive(): LiveData<SyncState> { override fun getSyncStateLive(): LiveData<SyncState> {

View file

@ -17,16 +17,19 @@
package im.vector.matrix.android.internal.session package im.vector.matrix.android.internal.session
import android.content.Context import android.content.Context
import android.os.Build
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import dagger.Binds import dagger.Binds
import dagger.Lazy import dagger.Lazy
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.multibindings.IntoSet 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.Credentials
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.auth.data.sessionId 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.InitialSyncProgressService
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService
@ -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.crypto.verification.VerificationMessageLiveObserver
import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.database.LiveEntityObserver
import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory
import im.vector.matrix.android.internal.di.* import im.vector.matrix.android.internal.di.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.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.RetrofitFactory
import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
@ -51,6 +67,7 @@ import okhttp3.OkHttpClient
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import retrofit2.Retrofit import retrofit2.Retrofit
import java.io.File import java.io.File
import javax.inject.Provider
@Module @Module
internal abstract class SessionModule { internal abstract class SessionModule {
@ -59,6 +76,11 @@ internal abstract class SessionModule {
companion object { companion object {
internal fun getKeyAlias(userMd5: String) = "session_db_$userMd5" internal fun getKeyAlias(userMd5: String) = "session_db_$userMd5"
/**
* Rules:
* Annotate methods with @SessionScope only the @Provides annotated methods with computation and logic.
*/
@JvmStatic @JvmStatic
@Provides @Provides
fun providesHomeServerConnectionConfig(sessionParams: SessionParams): HomeServerConnectionConfig { fun providesHomeServerConnectionConfig(sessionParams: SessionParams): HomeServerConnectionConfig {
@ -74,6 +96,7 @@ internal abstract class SessionModule {
@JvmStatic @JvmStatic
@UserId @UserId
@Provides @Provides
@SessionScope
fun providesUserId(credentials: Credentials): String { fun providesUserId(credentials: Credentials): String {
return credentials.userId return credentials.userId
} }
@ -88,6 +111,7 @@ internal abstract class SessionModule {
@JvmStatic @JvmStatic
@UserMd5 @UserMd5
@Provides @Provides
@SessionScope
fun providesUserMd5(@UserId userId: String): String { fun providesUserMd5(@UserId userId: String): String {
return userId.md5() return userId.md5()
} }
@ -95,6 +119,7 @@ internal abstract class SessionModule {
@JvmStatic @JvmStatic
@SessionId @SessionId
@Provides @Provides
@SessionScope
fun providesSessionId(credentials: Credentials): String { fun providesSessionId(credentials: Credentials): String {
return credentials.sessionId() return credentials.sessionId()
} }
@ -178,11 +203,34 @@ internal abstract class SessionModule {
fun providesEventBus(): EventBus { fun providesEventBus(): EventBus {
return EventBus.builder().build() 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 @Binds
abstract fun bindSession(session: DefaultSession): Session abstract fun bindSession(session: DefaultSession): Session
@Binds
abstract fun bindNetworkConnectivityChecker(networkConnectivityChecker: DefaultNetworkConnectivityChecker): NetworkConnectivityChecker
@Binds @Binds
@IntoSet @IntoSet
abstract fun bindGroupSummaryUpdater(groupSummaryUpdater: GroupSummaryUpdater): LiveEntityObserver 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.GroupSummaryResponse
import im.vector.matrix.android.internal.session.group.model.GroupUsers import im.vector.matrix.android.internal.session.group.model.GroupUsers
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.awaitTransaction
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import javax.inject.Inject import javax.inject.Inject
@ -53,12 +54,12 @@ internal class DefaultGetGroupDataTask @Inject constructor(
insertInDb(groupSummary, groupRooms, groupUsers, groupId) insertInDb(groupSummary, groupRooms, groupUsers, groupId)
} }
private fun insertInDb(groupSummary: GroupSummaryResponse, private suspend fun insertInDb(groupSummary: GroupSummaryResponse,
groupRooms: GroupRooms, groupRooms: GroupRooms,
groupUsers: GroupUsers, groupUsers: GroupUsers,
groupId: String) { groupId: String) {
monarchy monarchy
.writeAsync { realm -> .awaitTransaction { realm ->
val groupSummaryEntity = GroupSummaryEntity.where(realm, groupId).findFirst() val groupSummaryEntity = GroupSummaryEntity.where(realm, groupId).findFirst()
?: realm.createObject(GroupSummaryEntity::class.java, groupId) ?: realm.createObject(GroupSummaryEntity::class.java, groupId)

View file

@ -27,4 +27,10 @@ internal interface CapabilitiesAPI {
*/ */
@GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "config") @GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "config")
fun getUploadCapabilities(): Call<GetUploadCapabilitiesResult> 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 import javax.inject.Inject
@SessionScope @SessionScope
internal class DefaultPushRuleService @Inject constructor(private val getPushRulesTask: GetPushRulesTask, internal class DefaultPushRuleService @Inject constructor(
private val updatePushRuleEnableStatusTask: UpdatePushRuleEnableStatusTask, private val getPushRulesTask: GetPushRulesTask,
private val addPushRuleTask: AddPushRuleTask, private val updatePushRuleEnableStatusTask: UpdatePushRuleEnableStatusTask,
private val removePushRuleTask: RemovePushRuleTask, private val addPushRuleTask: AddPushRuleTask,
private val taskExecutor: TaskExecutor, private val removePushRuleTask: RemovePushRuleTask,
private val monarchy: Monarchy private val taskExecutor: TaskExecutor,
private val monarchy: Monarchy
) : PushRuleService { ) : PushRuleService {
private var listeners = mutableSetOf<PushRuleService.PushRuleListener>() private var listeners = mutableSetOf<PushRuleService.PushRuleListener>()

View file

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

View file

@ -15,37 +15,52 @@
*/ */
package im.vector.matrix.android.internal.session.pushers 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.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 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 @Inject constructor(
internal class DefaultConditionResolver(private val event: Event, private val roomGetter: RoomGetter,
private val roomService: RoomService, @UserId private val userId: String
@UserId private val userId: String) : ConditionResolver { ) : ConditionResolver {
override fun resolveEventMatchCondition(eventMatchCondition: EventMatchCondition): Boolean { override fun resolveEventMatchCondition(event: Event,
return eventMatchCondition.isSatisfied(event) condition: EventMatchCondition): Boolean {
return condition.isSatisfied(event)
} }
override fun resolveRoomMemberCountCondition(roomMemberCountCondition: RoomMemberCountCondition): Boolean { override fun resolveRoomMemberCountCondition(event: Event,
return roomMemberCountCondition.isSatisfied(event, roomService) condition: RoomMemberCountCondition): Boolean {
return condition.isSatisfied(event, roomGetter)
} }
override fun resolveSenderNotificationPermissionCondition(senderNotificationPermissionCondition: SenderNotificationPermissionCondition): Boolean { override fun resolveSenderNotificationPermissionCondition(event: Event,
// val roomId = event.roomId ?: return false condition: SenderNotificationPermissionCondition): Boolean {
// 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 {
val roomId = event.roomId ?: return false 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 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 com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.crypto.CryptoService 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.Room
import im.vector.matrix.android.api.session.room.members.MembershipService import im.vector.matrix.android.api.session.room.members.MembershipService
import im.vector.matrix.android.api.session.room.model.RoomSummary 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.session.room.typing.TypingService
import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional import 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.mapper.RoomSummaryMapper
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
import im.vector.matrix.android.internal.database.query.where 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 import javax.inject.Inject
internal class DefaultRoom @Inject constructor(override val roomId: String, 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 cryptoService: CryptoService,
private val relationService: RelationService, private val relationService: RelationService,
private val roomMembersService: MembershipService, private val roomMembersService: MembershipService,
private val roomPushRuleService: RoomPushRuleService) : private val roomPushRuleService: RoomPushRuleService,
private val taskExecutor: TaskExecutor,
private val sendStateTask: SendStateTask) :
Room, Room,
TimelineService by timelineService, TimelineService by timelineService,
SendService by sendService, SendService by sendService,
@ -96,11 +104,27 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
return cryptoService.shouldEncryptForInvitedMembers(roomId) return cryptoService.shouldEncryptForInvitedMembers(roomId)
} }
override fun enableEncryptionWithAlgorithm(algorithm: String, callback: MatrixCallback<Unit>) { override fun enableEncryption(algorithm: String, callback: MatrixCallback<Unit>) {
if (isEncrypted()) { when {
callback.onFailure(IllegalStateException("Encryption is already enabled for this room")) isEncrypted() -> {
} else { callback.onFailure(IllegalStateException("Encryption is already enabled for this room"))
stateService.enableEncryption(algorithm, callback) }
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