1
0
mirror of https://github.com/vector-im/riotX-android synced 2025-10-06 00:02:48 +02:00

Compare commits

...

18 Commits

Author SHA1 Message Date
ganfra
9903a299b9 Sqlite: make it work with latest develop version 2020-04-23 20:34:26 +02:00
ganfra
9a4cad1e45 Merge sql crypto store implementation 2020-04-23 18:58:07 +02:00
ganfra
649b4496a6 Make rx module uses Flow/Rx converter 2020-04-23 18:55:10 +02:00
ganfra
617b76c0d5 Make sdk use sqldelight module 2020-04-23 18:53:06 +02:00
ganfra
9415d9b5c5 Configure project 2020-04-23 16:49:51 +02:00
ganfra
3ba5e97392 Introduce all the tables 2020-04-23 12:54:04 +02:00
onurays
0ef5ae38d0 Merge branch 'feature/cross_signing_as_task' into feature/sqldelight_crypto_store 2020-04-15 11:16:29 +03:00
onurays
c53f9359d7 Use json (de)serializer instead of object (de)serialization. 2020-04-13 13:43:39 +03:00
onurays
09c98c32f0 Check and insert metadata at init. 2020-04-09 18:49:10 +03:00
onurays
fc90844556 Check and insert metadata at init. 2020-04-09 18:48:46 +03:00
onurays
0ca7e6202c Use json (de)serializer instead of object (de)serialization. 2020-04-09 16:15:34 +03:00
onurays
96162218e4 Lint fixes. 2020-04-09 02:40:43 +03:00
onurays
ffaacdf436 Use SqlCryptoStore implementation instead of RealmCryptoStore. 2020-04-09 02:30:52 +03:00
onurays
39fdda3715 Realm entities removed. 2020-04-09 00:52:01 +03:00
onurays
af171b3b7e Complete storing user cross signing key function. 2020-04-08 18:05:07 +03:00
onurays
3dd9693f59 Changelog added. 2020-04-08 15:32:14 +03:00
onurays
8b0845a76b Key backup recovery key fields added to metadata. 2020-04-08 11:08:15 +03:00
onurays
8e896600c9 Initial implementation of SQLDelight CryptoStore. 2020-04-08 10:33:06 +03:00
267 changed files with 9581 additions and 4287 deletions

1
.gitignore vendored
View File

@@ -14,3 +14,4 @@
/tmp
ktlint
.idea

View File

@@ -0,0 +1 @@
{"databases":[{"packageName":"im.vector.matrix.sqldelight.auth","compilationUnits":[{"name":"main","sourceFolders":[{"path":"src/main/sqldelight/auth","dependency":false}]}],"outputDirectory":"build/generated/sqldelight/code/AuthDatabase","className":"AuthDatabase","dependencies":[]},{"packageName":"im.vector.matrix.sqldelight.crypto","compilationUnits":[{"name":"main","sourceFolders":[{"path":"src/main/sqldelight/crypto","dependency":false}]}],"outputDirectory":"build/generated/sqldelight/code/CryptoDatabase","className":"CryptoDatabase","dependencies":[]},{"packageName":"im.vector.matrix.sqldelight.session","compilationUnits":[{"name":"main","sourceFolders":[{"path":"src/main/sqldelight/session","dependency":false}]}],"outputDirectory":"build/generated/sqldelight/code/SessionDatabase","className":"SessionDatabase","dependencies":[]}]}

View File

@@ -2,52 +2,40 @@ Changes in RiotX 0.19.0 (2020-XX-XX)
===================================================
Features ✨:
- Change password (#528)
- Cross-Signing | Support SSSS secret sharing (#944)
- Cross-Signing | Verify new session from existing session (#1134)
- Cross-Signing | Bootstraping cross signing with 4S from mobile (#985)
- Save media files to Gallery (#973)
Improvements 🙌:
- Verification DM / Handle concurrent .start after .ready (#794)
- Reimplementation of multiple attachment picker
- Cross-Signing | Update Shield Logic for DM (#963)
- Cross-Signing | Complete security new session design update (#1135)
- Cross-Signing | Setup key backup as part of SSSS bootstrapping (#1201)
- Cross-Signing | Gossip key backup recovery key (#1200)
- Show room encryption status as a bubble tile (#1078)
- UX/UI | Add indicator to home tab on invite (#957)
- Cross-Signing | Restore history after recover from passphrase (#1214)
- Cross-Sign | QR code scan confirmation screens design update (#1187)
- Emoji Verification | It's not the same butterfly! (#1220)
- Cross-Signing | Composer decoration: shields (#1077)
- Cross-Signing | Migrate existing keybackup to cross signing with 4S from mobile (#1197)
Bugfix 🐛:
- Fix summary notification staying after "mark as read"
- Missing avatar/displayname after verification request message (#841)
- Crypto | RiotX sometimes rotate the current device keys (#1170)
- RiotX can't restore cross signing keys saved by web in SSSS (#1174)
- Cross- Signing | After signin in new session, verification paper trail in DM is off (#1191)
- Failed to encrypt message in room (message stays in red), [thanks to pwr22] (#925)
- Cross-Signing | web <-> riotX After QR code scan, gossiping fails (#1210)
- Fix crash when trying to download file without internet connection (#1229)
- Local echo are not updated in timeline (for failed & encrypted states)
- Render image event even if thumbnail_info does not have mimetype defined (#1209)
- RiotX now uses as many threads as it needs to do work and send messages (#1221)
- Fix issue with media path (#1227)
Translations 🗣:
-
SDK API changes ⚠️:
- Increase targetSdkVersion to 29
- Implementation of SqlCryptoStore on top of SQLDelight
Build 🧱:
- Compile with Android SDK 29 (Android Q)
-
Other changes:
- Add a setting to prevent screenshots of the application, disabled by default (#1027)
- Increase File Logger capacities ( + use dev log preferences)
Changes in RiotX 0.18.1 (2020-03-17)
@@ -456,7 +444,6 @@ Bugfix:
- Fix messages with empty `in_reply_to` not rendering (#447)
- Fix clear cache (#408) and Logout (#205)
- Fix `(edited)` link can be copied to clipboard (#402)
- KeyBackup / SSSS | Should get the key from SSSS instead of asking recovery Key (#1163)
Build:
- Split APK: generate one APK per arch, to reduce APK size of about 30%

View File

@@ -16,7 +16,7 @@ buildscript {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1'
classpath 'com.google.android.gms:oss-licenses-plugin:0.9.5'
classpath 'com.squareup.sqldelight:gradle-plugin:1.2.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
@@ -53,7 +53,7 @@ allprojects {
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
// Warnings are potential errors, so stop ignoring them
kotlinOptions.allWarningsAsErrors = true
kotlinOptions.allWarningsAsErrors = false
}
}

View File

@@ -38,6 +38,8 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-rx2:1.3.3'
// Paging
implementation "androidx.paging:paging-runtime-ktx:2.1.0"

View File

@@ -27,44 +27,34 @@ import im.vector.matrix.android.api.session.room.notification.RoomNotificationSt
import im.vector.matrix.android.api.session.room.send.UserDraft
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional
import io.reactivex.Observable
import io.reactivex.Single
import kotlinx.coroutines.rx2.asObservable
class RxRoom(private val room: Room) {
fun liveRoomSummary(): Observable<Optional<RoomSummary>> {
return room.getRoomSummaryLive()
.asObservable()
.startWithCallable { room.roomSummary().toOptional() }
return room.getRoomSummaryLive().asObservable()
}
fun liveRoomMembers(queryParams: RoomMemberQueryParams): Observable<List<RoomMemberSummary>> {
return room.getRoomMembersLive(queryParams).asObservable()
.startWithCallable {
room.getRoomMembers(queryParams)
}
}
fun liveAnnotationSummary(eventId: String): Observable<Optional<EventAnnotationsSummary>> {
return room.getEventAnnotationsSummaryLive(eventId).asObservable()
.startWithCallable {
room.getEventAnnotationsSummary(eventId).toOptional()
fun liveRoomMember(userId: String): Observable<Optional<RoomMemberSummary>> {
return room.getRoomMemberLive(userId).asObservable()
}
fun liveAnnotationSummary(eventId: String): Observable<EventAnnotationsSummary> {
return room.getEventAnnotationsSummaryLive(eventId).asObservable()
}
fun liveTimelineEvent(eventId: String): Observable<Optional<TimelineEvent>> {
return room.getTimeLineEventLive(eventId).asObservable()
.startWithCallable {
room.getTimeLineEvent(eventId).toOptional()
}
}
fun liveStateEvent(eventType: String, stateKey: String): Observable<Optional<Event>> {
return room.getStateEventLive(eventType, stateKey).asObservable()
.startWithCallable {
room.getStateEvent(eventType, stateKey).toOptional()
}
}
fun liveReadMarker(): Observable<Optional<String>> {
@@ -93,7 +83,7 @@ class RxRoom(private val room: Room) {
}
fun liveNotificationState(): Observable<RoomNotificationState> {
return room.getLiveRoomNotificationState().asObservable()
return room.getRoomNotificationStateLive().asObservable()
}
}

View File

@@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.group.GroupSummaryQueryParams
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.pushers.Pusher
import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams
import im.vector.matrix.android.api.session.room.model.Breadcrumb
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.sync.SyncState
@@ -34,28 +35,20 @@ import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
import io.reactivex.Observable
import io.reactivex.Single
import kotlinx.coroutines.rx2.asObservable
class RxSession(private val session: Session) {
fun liveRoomSummaries(queryParams: RoomSummaryQueryParams): Observable<List<RoomSummary>> {
return session.getRoomSummariesLive(queryParams).asObservable()
.startWithCallable {
session.getRoomSummaries(queryParams)
}
}
fun liveGroupSummaries(queryParams: GroupSummaryQueryParams): Observable<List<GroupSummary>> {
return session.getGroupSummariesLive(queryParams).asObservable()
.startWithCallable {
session.getGroupSummaries(queryParams)
}
}
fun liveBreadcrumbs(): Observable<List<RoomSummary>> {
fun liveBreadcrumbs(): Observable<List<Breadcrumb>> {
return session.getBreadcrumbsLive().asObservable()
.startWithCallable {
session.getBreadcrumbs()
}
}
fun liveSyncState(): Observable<SyncState> {
@@ -68,9 +61,6 @@ class RxSession(private val session: Session) {
fun liveUser(userId: String): Observable<Optional<User>> {
return session.getUserLive(userId).asObservable()
.startWithCallable {
session.getUser(userId).toOptional()
}
}
fun liveUsers(): Observable<List<User>> {
@@ -125,9 +115,6 @@ class RxSession(private val session: Session) {
fun liveAccountData(types: Set<String>): Observable<List<UserAccountDataEvent>> {
return session.getLiveAccountDataEvents(types).asObservable()
.startWithCallable {
session.getAccountDataEvents(types)
}
}
}

View File

@@ -71,6 +71,10 @@ android {
kotlinOptions {
jvmTarget = "1.8"
}
packagingOptions {
pickFirst 'META-INF/kotlinx-coroutines-core.kotlin_module'
}
}
static def gitRevision() {
@@ -125,6 +129,11 @@ dependencies {
// Database
implementation 'com.github.Zhuinden:realm-monarchy:0.5.1'
kapt 'dk.ilios:realmfieldnameshelper:1.1.1'
implementation "com.squareup.sqldelight:android-driver:1.2.2"
implementation 'net.zetetic:android-database-sqlcipher:4.3.0'
implementation project(':matrix-sdk-sqldelight')
implementation "com.squareup.sqldelight:coroutines-extensions:1.2.1"
implementation "com.squareup.sqldelight:android-paging-extensions:1.2.1"
// Work
implementation "androidx.work:work-runtime-ktx:$work_version"

View File

@@ -18,8 +18,5 @@ package im.vector.matrix.android
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors
internal val testCoroutineDispatchers = MatrixCoroutineDispatchers(Main, Main, Main, Main,
Executors.newSingleThreadExecutor().asCoroutineDispatcher())
internal val testCoroutineDispatchers = MatrixCoroutineDispatchers(Main, Main, Main, Main, Main, Main, Main)

View File

@@ -18,14 +18,18 @@ package im.vector.matrix.android.internal.crypto.verification.qrcode
import androidx.test.ext.junit.runners.AndroidJUnit4
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.api.session.crypto.verification.PendingVerificationRequest
import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod
import im.vector.matrix.android.api.session.crypto.verification.VerificationService
import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
import im.vector.matrix.android.common.CommonTestHelper
import im.vector.matrix.android.common.CryptoTestHelper
import im.vector.matrix.android.common.TestConstants
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import im.vector.matrix.android.api.session.crypto.verification.PendingVerificationRequest
import org.amshove.kluent.shouldBe
import org.junit.Assert.assertEquals
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
@@ -229,4 +233,46 @@ class VerificationTest : InstrumentedTest {
cryptoTestData.cleanUp(mTestHelper)
}
@Test
fun test_alice_sends_text_message_and_bob_can_decrypt() {
val lock = CountDownLatch(1)
val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession
val bobSession = cryptoTestData.secondSession!!
val roomFromBobPOV = bobSession.getRoom(cryptoTestData.roomId)!!
aliceSession.getRoom(cryptoTestData.roomId)!!.sendTextMessage("test")
val bobEventsListener = object : Timeline.Listener {
override fun onTimelineFailure(throwable: Throwable) {
// noop
}
override fun onNewTimelineEvents(eventIds: List<String>) {
// noop
}
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
if (snapshot.isNotEmpty() && snapshot.first().root.isEncrypted()) {
lock.countDown()
}
}
}
val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(20))
bobTimeline.start()
bobTimeline.addListener(bobEventsListener)
mTestHelper.await(lock)
bobTimeline.getTimelineEventAtIndex(0)?.root?.let {
val decryptionResult = bobSession.cryptoService().decryptEvent(it, "")
val text = (decryptionResult.clearEvent["content"] as Map<*, *>)["body"]
assertEquals("test", text)
}
}
}

View File

@@ -16,12 +16,12 @@
package im.vector.matrix.android.api.session.accountdata
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
import kotlinx.coroutines.flow.Flow
interface AccountDataService {
/**
@@ -32,7 +32,7 @@ interface AccountDataService {
/**
* Observe the account data with the provided type
*/
fun getLiveAccountDataEvent(type: String): LiveData<Optional<UserAccountDataEvent>>
fun getLiveAccountDataEvent(type: String): Flow<Optional<UserAccountDataEvent>>
/**
* Retrieve the account data with the provided types. The return list can have a different size that
@@ -44,7 +44,7 @@ interface AccountDataService {
/**
* Observe the account data with the provided types. If an empty set is provided, all the AccountData are observed
*/
fun getLiveAccountDataEvents(types: Set<String>): LiveData<List<UserAccountDataEvent>>
fun getLiveAccountDataEvents(types: Set<String>): Flow<List<UserAccountDataEvent>>
/**
* Update the account data with the provided type and the provided account data content

View File

@@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.group
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.session.group.model.GroupSummary
import kotlinx.coroutines.flow.Flow
/**
* This interface defines methods to get groups. It's implemented at the session level.
@@ -48,5 +49,5 @@ interface GroupService {
* Get a live list of group summaries. This list is refreshed as soon as the data changes.
* @return the [LiveData] of [GroupSummary]
*/
fun getGroupSummariesLive(groupSummaryQueryParams: GroupSummaryQueryParams): LiveData<List<GroupSummary>>
fun getGroupSummariesLive(groupSummaryQueryParams: GroupSummaryQueryParams): Flow<List<GroupSummary>>
}

View File

@@ -25,9 +25,7 @@ import im.vector.matrix.android.api.session.room.model.Membership
data class GroupSummary(
val groupId: String,
val membership: Membership,
val displayName: String = "",
val shortDescription: String = "",
val avatarUrl: String = "",
val roomIds: List<String> = emptyList(),
val userIds: List<String> = emptyList()
val displayName: String? = null,
val shortDescription: String? = null,
val avatarUrl: String? = null
)

View File

@@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.pushers
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
import kotlinx.coroutines.flow.Flow
import java.util.UUID
interface PushersService {
@@ -72,9 +73,9 @@ interface PushersService {
fun removeHttpPusher(pushkey: String, appId: String, callback: MatrixCallback<Unit>): Cancelable
/**
* Get the current pushers, as a LiveData
* Get the current pushers, as a Flow
*/
fun getPushersLive(): LiveData<List<Pusher>>
fun getPushersLive(): Flow<List<Pusher>>
/**
* Get the current pushers

View File

@@ -30,6 +30,7 @@ import im.vector.matrix.android.api.session.room.state.StateService
import im.vector.matrix.android.api.session.room.timeline.TimelineService
import im.vector.matrix.android.api.session.room.typing.TypingService
import im.vector.matrix.android.api.util.Optional
import kotlinx.coroutines.flow.Flow
/**
* This interface defines methods to interact within a room.
@@ -56,7 +57,7 @@ interface Room :
* A live [RoomSummary] associated with the room
* You can observe this summary to get dynamic data from this room.
*/
fun getRoomSummaryLive(): LiveData<Optional<RoomSummary>>
fun getRoomSummaryLive(): Flow<Optional<RoomSummary>>
/**
* A current snapshot of [RoomSummary] associated with the room

View File

@@ -18,10 +18,12 @@ package im.vector.matrix.android.api.session.room
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.room.model.Breadcrumb
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.Optional
import kotlinx.coroutines.flow.Flow
/**
* This interface defines methods to get rooms. It's implemented at the session level.
@@ -67,21 +69,21 @@ interface RoomService {
/**
* Get a live list of room summaries. This list is refreshed as soon as the data changes.
* @return the [LiveData] of List[RoomSummary]
* @return the [Flow] of List[RoomSummary]
*/
fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams): LiveData<List<RoomSummary>>
fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams): Flow<List<RoomSummary>>
/**
* Get a snapshot list of Breadcrumbs
* @return the immutable list of [RoomSummary]
* @return the immutable list of [Breadcrumb]
*/
fun getBreadcrumbs(): List<RoomSummary>
fun getBreadcrumbs(): List<Breadcrumb>
/**
* Get a live list of Breadcrumbs
* @return the [LiveData] of [RoomSummary]
* @return the [Flow] of [Breadcrumb]
*/
fun getBreadcrumbsLive(): LiveData<List<RoomSummary>>
fun getBreadcrumbsLive(): Flow<List<Breadcrumb>>
/**
* Inform the Matrix SDK that a room is displayed.

View File

@@ -28,6 +28,7 @@ fun roomSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {
* [im.vector.matrix.android.api.session.room.Room] and [im.vector.matrix.android.api.session.room.RoomService]
*/
data class RoomSummaryQueryParams(
val fromGroupId: String?,
val displayName: QueryStringValue,
val canonicalAlias: QueryStringValue,
val memberships: List<Membership>
@@ -35,11 +36,13 @@ data class RoomSummaryQueryParams(
class Builder {
var fromGroupId: String? = null
var displayName: QueryStringValue = QueryStringValue.IsNotEmpty
var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition
var memberships: List<Membership> = Membership.all()
fun build() = RoomSummaryQueryParams(
fromGroupId = fromGroupId,
displayName = displayName,
canonicalAlias = canonicalAlias,
memberships = memberships

View File

@@ -20,6 +20,8 @@ import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.Optional
import kotlinx.coroutines.flow.Flow
/**
* This interface defines methods to handling membership. It's implemented at the room level.
@@ -40,6 +42,14 @@ interface MembershipService {
*/
fun getRoomMember(userId: String): RoomMemberSummary?
/**
* Return a live version of an optionnal roomMember with the given userId.
* @param userId the userId param to look for
*
* @return a [Flow] of [Optional] [RoomMemberSummary] with userId
*/
fun getRoomMemberLive(userId: String): Flow<Optional<RoomMemberSummary>>
/**
* Return all the roomMembers of the room with params
* @param queryParams the params to query for
@@ -50,9 +60,10 @@ interface MembershipService {
/**
* Return all the roomMembers of the room filtered by memberships
* @param queryParams the params to query for
* @return a [LiveData] of roomMember list.
* @return a [Flow] of roomMember list.
*/
fun getRoomMembersLive(queryParams: RoomMemberQueryParams): LiveData<List<RoomMemberSummary>>
fun getRoomMembersLive(queryParams: RoomMemberQueryParams): Flow<List<RoomMemberSummary>>
fun getNumberOfJoinedMembers(): Int

View File

@@ -0,0 +1,18 @@
package im.vector.matrix.android.api.session.room.model
import im.vector.matrix.android.api.session.room.send.UserDraft
/**
* This data class holds data about a breadcrumb.
*/
data class Breadcrumb(
val roomId: String,
val displayName: String = "",
val avatarUrl: String = "",
val notificationCount: Int = 0,
val highlightCount: Int = 0,
val hasUnreadMessages: Boolean = false,
val userDrafts: List<UserDraft> = emptyList(),
var isEncrypted: Boolean,
val typingRoomMemberIds: List<String> = emptyList()
)

View File

@@ -31,7 +31,6 @@ data class RoomSummary constructor(
val topic: String = "",
val avatarUrl: String = "",
val canonicalAlias: String? = null,
val aliases: List<String> = emptyList(),
val isDirect: Boolean = false,
val joinedMembersCount: Int? = 0,
val invitedMembersCount: Int? = 0,

View File

@@ -22,6 +22,7 @@ import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.Optional
import kotlinx.coroutines.flow.Flow
/**
* In some cases, events may wish to reference other events.
@@ -113,12 +114,12 @@ interface RelationService {
* @param eventId the eventId to look for EventAnnotationsSummary
* @return the EventAnnotationsSummary found
*/
fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary?
fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary
/**
* Get a LiveData of EventAnnotationsSummary for the specified eventId
* @param eventId the eventId to look for EventAnnotationsSummary
* @return the LiveData of EventAnnotationsSummary
* @return a [Flow] of EventAnnotationsSummary
*/
fun getEventAnnotationsSummaryLive(eventId: String): LiveData<Optional<EventAnnotationsSummary>>
fun getEventAnnotationsSummaryLive(eventId: String): Flow<EventAnnotationsSummary>
}

View File

@@ -16,13 +16,13 @@
package im.vector.matrix.android.api.session.room.notification
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
import kotlinx.coroutines.flow.Flow
interface RoomPushRuleService {
fun getLiveRoomNotificationState(): LiveData<RoomNotificationState>
fun getRoomNotificationStateLive(): Flow<RoomNotificationState>
fun setRoomNotificationState(roomNotificationState: RoomNotificationState, matrixCallback: MatrixCallback<Unit>): Cancelable
}

View File

@@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.room.model.ReadReceipt
import im.vector.matrix.android.api.util.Optional
import kotlinx.coroutines.flow.Flow
/**
* This interface defines methods to handle read receipts and read marker in a room. It's implemented at the room level.
@@ -55,16 +56,16 @@ interface ReadService {
/**
* Returns a live read marker id for the room.
*/
fun getReadMarkerLive(): LiveData<Optional<String>>
fun getReadMarkerLive(): Flow<Optional<String>>
/**
* Returns a live read receipt id for the room.
*/
fun getMyReadReceiptLive(): LiveData<Optional<String>>
fun getMyReadReceiptLive(): Flow<Optional<String>>
/**
* Returns a live list of read receipts for a given event
* @param eventId: the event
*/
fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>>
fun getEventReadReceiptsLive(eventId: String): Flow<List<ReadReceipt>>
}

View File

@@ -16,9 +16,9 @@
package im.vector.matrix.android.api.session.room.send
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
import kotlinx.coroutines.flow.Flow
interface DraftService {
@@ -36,5 +36,5 @@ interface DraftService {
* Return the current drafts if any, as a live data
* The draft list can contain one draft for {regular, reply, quote} and an arbitrary number of {edit} drafts
*/
fun getDraftsLive(): LiveData<List<UserDraft>>
fun getDraftsLive(): Flow<List<UserDraft>>
}

View File

@@ -16,10 +16,10 @@
package im.vector.matrix.android.api.session.room.state
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.util.Optional
import kotlinx.coroutines.flow.Flow
interface StateService {
@@ -30,5 +30,5 @@ interface StateService {
fun getStateEvent(eventType: String, stateKey: String): Event?
fun getStateEventLive(eventType: String, stateKey: String): LiveData<Optional<Event>>
fun getStateEventLive(eventType: String, stateKey: String): Flow<Optional<Event>>
}

View File

@@ -16,8 +16,8 @@
package im.vector.matrix.android.api.session.room.timeline
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.util.Optional
import kotlinx.coroutines.flow.Flow
/**
* This interface defines methods to interact with the timeline. It's implemented at the room level.
@@ -38,5 +38,5 @@ interface TimelineService {
fun getTimeLineEvent(eventId: String): TimelineEvent?
fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>>
fun getTimeLineEventLive(eventId: String): Flow<Optional<TimelineEvent>>
}

View File

@@ -22,6 +22,7 @@ import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.Optional
import kotlinx.coroutines.flow.Flow
/**
* This interface defines methods to get users. It's implemented at the session level.
@@ -48,27 +49,27 @@ interface UserService {
/**
* Observe a live user from a userId
* @param userId the userId to look for.
* @return a LiveData of user with userId
* @return a Flow of user with userId
*/
fun getUserLive(userId: String): LiveData<Optional<User>>
fun getUserLive(userId: String): Flow<Optional<User>>
/**
* Observe a live list of users sorted alphabetically
* @return a Livedata of users
* @return a Flow of users
*/
fun getUsersLive(): LiveData<List<User>>
fun getUsersLive(): Flow<List<User>>
/**
* Observe a live [PagedList] of users sorted alphabetically. You can filter the users.
* @param filter the filter. It will look into userId and displayName.
* @return a Livedata of users
* @return a Flow of users
*/
fun getPagedUsersLive(filter: String? = null): LiveData<PagedList<User>>
fun getPagedUsersLive(filter: String? = null): Flow<PagedList<User>>
/**
* Get list of ignored users
*/
fun getIgnoredUsersLive(): LiveData<List<User>>
fun getIgnoredUsersLive(): Flow<List<User>>
/**
* Ignore users

View File

@@ -18,6 +18,7 @@ package im.vector.matrix.android.api.util
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.Breadcrumb
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
@@ -148,6 +149,8 @@ fun GroupSummary.toMatrixItem() = MatrixItem.GroupItem(groupId, displayName, ava
fun RoomSummary.toMatrixItem() = MatrixItem.RoomItem(roomId, displayName, avatarUrl)
fun Breadcrumb.toMatrixItem() = MatrixItem.RoomItem(roomId, displayName, avatarUrl)
fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlias ?: roomId, displayName, avatarUrl)
// If no name is available, use room alias as Riot-Web does

View File

@@ -17,16 +17,19 @@
package im.vector.matrix.android.internal.auth
import android.content.Context
import com.squareup.sqldelight.android.AndroidSqliteDriver
import dagger.Binds
import dagger.Module
import dagger.Provides
import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.internal.auth.db.AuthRealmMigration
import im.vector.matrix.android.internal.auth.db.AuthRealmModule
import im.vector.matrix.android.internal.auth.db.RealmPendingSessionStore
import im.vector.matrix.android.internal.auth.db.RealmSessionParamsStore
import im.vector.matrix.android.internal.database.RealmKeysUtils
import im.vector.matrix.android.internal.di.AuthDatabase
import im.vector.matrix.android.internal.auth.realm.AuthRealmMigration
import im.vector.matrix.android.internal.auth.realm.AuthRealmModule
import im.vector.matrix.android.internal.auth.sqlite.AuthSchema
import im.vector.matrix.android.internal.auth.sqlite.SqlitePendingSessionStore
import im.vector.matrix.android.internal.auth.sqlite.SqliteSessionParamsStore
import im.vector.matrix.android.internal.database.DatabaseKeysUtils
import im.vector.matrix.android.internal.di.MatrixScope
import im.vector.matrix.sqldelight.auth.AuthDatabase
import io.realm.RealmConfiguration
import java.io.File
@@ -39,16 +42,16 @@ internal abstract class AuthModule {
@JvmStatic
@Provides
@AuthDatabase
fun providesRealmConfiguration(context: Context, realmKeysUtils: RealmKeysUtils): RealmConfiguration {
@im.vector.matrix.android.internal.di.RealmAuthDatabase
@MatrixScope
fun providesRealmConfiguration(context: Context, databaseKeysUtils: DatabaseKeysUtils): RealmConfiguration {
val old = File(context.filesDir, "matrix-sdk-auth")
if (old.exists()) {
old.renameTo(File(context.filesDir, "matrix-sdk-auth.realm"))
}
return RealmConfiguration.Builder()
.apply {
realmKeysUtils.configureEncryption(this, DB_ALIAS)
databaseKeysUtils.configureEncryption(this, DB_ALIAS)
}
.name("matrix-sdk-auth.realm")
.modules(AuthRealmModule())
@@ -56,13 +59,22 @@ internal abstract class AuthModule {
.migration(AuthRealmMigration)
.build()
}
@JvmStatic
@Provides
@MatrixScope
fun providesAuthDatabase(context: Context, authSchema: AuthSchema, databaseKeysUtils: DatabaseKeysUtils): AuthDatabase {
val supportFactory = databaseKeysUtils.createEncryptedSQLiteOpenHelperFactory(DB_ALIAS)
val driver = AndroidSqliteDriver(authSchema, context, "matrix-sdk-auth.db", factory = supportFactory)
return AuthDatabase.invoke(driver)
}
}
@Binds
abstract fun bindSessionParamsStore(sessionParamsStore: RealmSessionParamsStore): SessionParamsStore
abstract fun bindSessionParamsStore(sessionParamsStore: SqliteSessionParamsStore): SessionParamsStore
@Binds
abstract fun bindPendingSessionStore(pendingSessionStore: RealmPendingSessionStore): PendingSessionStore
abstract fun bindPendingSessionStore(pendingSessionStore: SqlitePendingSessionStore): PendingSessionStore
@Binds
abstract fun bindAuthenticationService(authenticationService: DefaultAuthenticationService): AuthenticationService

View File

@@ -20,13 +20,7 @@ import android.net.Uri
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.AuthenticationService
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.LoginFlowResult
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.auth.data.Versions
import im.vector.matrix.android.api.auth.data.isLoginAndRegistrationSupportedBySdk
import im.vector.matrix.android.api.auth.data.isSupportedBySdk
import im.vector.matrix.android.api.auth.data.*
import im.vector.matrix.android.api.auth.login.LoginWizard
import im.vector.matrix.android.api.auth.registration.RegistrationWizard
import im.vector.matrix.android.api.failure.Failure
@@ -35,7 +29,7 @@ import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
import im.vector.matrix.android.internal.auth.data.RiotConfig
import im.vector.matrix.android.internal.auth.db.PendingSessionData
import im.vector.matrix.android.internal.auth.registration.PendingSessionData
import im.vector.matrix.android.internal.auth.login.DefaultLoginWizard
import im.vector.matrix.android.internal.auth.registration.DefaultRegistrationWizard
import im.vector.matrix.android.internal.di.Unauthenticated
@@ -114,7 +108,7 @@ internal class DefaultAuthenticationService @Inject constructor(
}
private suspend fun getLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig): LoginFlowResult {
return withContext(coroutineDispatchers.io) {
return withContext(coroutineDispatchers.computation) {
val authAPI = buildAuthAPI(homeServerConnectionConfig)
// First check the homeserver version

View File

@@ -16,7 +16,7 @@
package im.vector.matrix.android.internal.auth
import im.vector.matrix.android.internal.auth.db.PendingSessionData
import im.vector.matrix.android.internal.auth.registration.PendingSessionData
/**
* Store for elements when doing login or registration

View File

@@ -30,7 +30,7 @@ import im.vector.matrix.android.internal.auth.PendingSessionStore
import im.vector.matrix.android.internal.auth.SessionCreator
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
import im.vector.matrix.android.internal.auth.data.ThreePidMedium
import im.vector.matrix.android.internal.auth.db.PendingSessionData
import im.vector.matrix.android.internal.auth.registration.PendingSessionData
import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationParams
import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationResponse
import im.vector.matrix.android.internal.auth.registration.RegisterAddThreePidTask

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package im.vector.matrix.android.internal.auth.db
package im.vector.matrix.android.internal.auth.realm
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.sessionId

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package im.vector.matrix.android.internal.auth.db
package im.vector.matrix.android.internal.auth.realm
import io.realm.annotations.RealmModule

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package im.vector.matrix.android.internal.auth.db
package im.vector.matrix.android.internal.auth.realm
import io.realm.RealmObject

View File

@@ -14,11 +14,12 @@
* limitations under the License.
*/
package im.vector.matrix.android.internal.auth.db
package im.vector.matrix.android.internal.auth.realm
import com.squareup.moshi.Moshi
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.internal.auth.login.ResetPasswordData
import im.vector.matrix.android.internal.auth.registration.PendingSessionData
import im.vector.matrix.android.internal.auth.registration.ThreePidData
import javax.inject.Inject

View File

@@ -14,17 +14,18 @@
* limitations under the License.
*/
package im.vector.matrix.android.internal.auth.db
package im.vector.matrix.android.internal.auth.realm
import im.vector.matrix.android.internal.auth.PendingSessionStore
import im.vector.matrix.android.internal.auth.registration.PendingSessionData
import im.vector.matrix.android.internal.database.awaitTransaction
import im.vector.matrix.android.internal.di.AuthDatabase
import im.vector.matrix.android.internal.di.RealmAuthDatabase
import io.realm.Realm
import io.realm.RealmConfiguration
import javax.inject.Inject
internal class RealmPendingSessionStore @Inject constructor(private val mapper: PendingSessionMapper,
@AuthDatabase
@RealmAuthDatabase
private val realmConfiguration: RealmConfiguration
) : PendingSessionStore {

View File

@@ -14,14 +14,14 @@
* limitations under the License.
*/
package im.vector.matrix.android.internal.auth.db
package im.vector.matrix.android.internal.auth.realm
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.auth.data.sessionId
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.database.awaitTransaction
import im.vector.matrix.android.internal.di.AuthDatabase
import im.vector.matrix.android.internal.di.RealmAuthDatabase
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.exceptions.RealmPrimaryKeyConstraintException
@@ -29,7 +29,7 @@ import timber.log.Timber
import javax.inject.Inject
internal class RealmSessionParamsStore @Inject constructor(private val mapper: SessionParamsMapper,
@AuthDatabase
@RealmAuthDatabase
private val realmConfiguration: RealmConfiguration
) : SessionParamsStore {

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package im.vector.matrix.android.internal.auth.db
package im.vector.matrix.android.internal.auth.realm
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey

View File

@@ -14,8 +14,9 @@
* limitations under the License.
*/
package im.vector.matrix.android.internal.auth.db
package im.vector.matrix.android.internal.auth.realm
import com.squareup.moshi.JsonDataException
import com.squareup.moshi.Moshi
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
@@ -28,6 +29,15 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
private val credentialsAdapter = moshi.adapter(Credentials::class.java)
private val homeServerConnectionConfigAdapter = moshi.adapter(HomeServerConnectionConfig::class.java)
fun map(credentialsJson: String, homeServerConnectionConfigJson: String, isTokenValid: Boolean): SessionParams {
val credentials = credentialsAdapter.fromJson(credentialsJson)
val homeServerConnectionConfig = homeServerConnectionConfigAdapter.fromJson(homeServerConnectionConfigJson)
if (credentials == null || homeServerConnectionConfig == null) {
throw JsonDataException()
}
return SessionParams(credentials, homeServerConnectionConfig, isTokenValid)
}
fun map(entity: SessionParamsEntity?): SessionParams? {
if (entity == null) {
return null

View File

@@ -29,7 +29,6 @@ import im.vector.matrix.android.internal.auth.AuthAPI
import im.vector.matrix.android.internal.auth.PendingSessionStore
import im.vector.matrix.android.internal.auth.SessionCreator
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.auth.db.PendingSessionData
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.task.launchToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers

View File

@@ -14,12 +14,12 @@
* limitations under the License.
*/
package im.vector.matrix.android.internal.auth.db
package im.vector.matrix.android.internal.auth.registration
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.internal.auth.login.ResetPasswordData
import im.vector.matrix.android.internal.auth.registration.ThreePidData
import java.util.UUID
import java.util.*
/**
* This class holds all pending data when creating a session, either by login or by register

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.auth.sqlite
import com.squareup.sqldelight.db.SqlDriver
import im.vector.matrix.sqldelight.auth.AuthDatabase
import io.realm.Realm
import io.realm.RealmConfiguration
import javax.inject.Inject
internal class AuthSchema @Inject constructor(@im.vector.matrix.android.internal.di.RealmAuthDatabase private val realmConfiguration: RealmConfiguration) : SqlDriver.Schema by AuthDatabase.Schema {
override fun create(driver: SqlDriver) {
AuthDatabase.Schema.create(driver)
AuthDatabase(driver).apply {
val sessionParamsQueries = this.sessionParamsQueries
sessionParamsQueries.transaction {
Realm.getInstance(realmConfiguration).use {
it.where(im.vector.matrix.android.internal.auth.realm.SessionParamsEntity::class.java).findAll().forEach { realmSessionParams ->
sessionParamsQueries.insert(realmSessionParams.toSqlModel())
}
}
}
}
}
private fun im.vector.matrix.android.internal.auth.realm.SessionParamsEntity.toSqlModel(): im.vector.matrix.sqldelight.auth.SessionParamsEntity {
return im.vector.matrix.sqldelight.auth.SessionParamsEntity.Impl(
sessionId,
userId,
credentialsJson,
homeServerConnectionConfigJson,
isTokenValid
)
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package im.vector.matrix.android.internal.auth.sqlite
import com.squareup.moshi.Moshi
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.internal.auth.login.ResetPasswordData
import im.vector.matrix.android.internal.auth.registration.PendingSessionData
import im.vector.matrix.android.internal.auth.registration.ThreePidData
import im.vector.matrix.sqldelight.auth.PendingSessionEntity
import javax.inject.Inject
internal class SqlitePendingSessionMapper @Inject constructor(moshi: Moshi) {
private val homeServerConnectionConfigAdapter = moshi.adapter(HomeServerConnectionConfig::class.java)
private val resetPasswordDataAdapter = moshi.adapter(ResetPasswordData::class.java)
private val threePidDataAdapter = moshi.adapter(ThreePidData::class.java)
fun map(entity: PendingSessionEntity?): PendingSessionData? {
if (entity == null) {
return null
}
val homeServerConnectionConfig = homeServerConnectionConfigAdapter.fromJson(entity.home_server_connection_config_json)!!
val resetPasswordData = entity.reset_password_data_json?.let { resetPasswordDataAdapter.fromJson(it) }
val threePidData = entity.current_three_pid_data_json?.let { threePidDataAdapter.fromJson(it) }
return PendingSessionData(
homeServerConnectionConfig = homeServerConnectionConfig,
clientSecret = entity.client_secret,
sendAttempt = entity.send_attempts,
resetPasswordData = resetPasswordData,
currentSession = entity.current_session,
isRegistrationStarted = entity.is_registration_started,
currentThreePidData = threePidData)
}
fun map(sessionData: PendingSessionData?): PendingSessionEntity? {
if (sessionData == null) {
return null
}
val homeServerConnectionConfigJson = homeServerConnectionConfigAdapter.toJson(sessionData.homeServerConnectionConfig)
val resetPasswordDataJson = resetPasswordDataAdapter.toJson(sessionData.resetPasswordData)
val currentThreePidDataJson = threePidDataAdapter.toJson(sessionData.currentThreePidData)
return PendingSessionEntity.Impl(
home_server_connection_config_json = homeServerConnectionConfigJson,
client_secret = sessionData.clientSecret,
send_attempts = sessionData.sendAttempt,
reset_password_data_json = resetPasswordDataJson,
current_session = sessionData.currentSession,
is_registration_started = sessionData.isRegistrationStarted,
current_three_pid_data_json = currentThreePidDataJson
)
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package im.vector.matrix.android.internal.auth.sqlite
import im.vector.matrix.android.internal.auth.PendingSessionStore
import im.vector.matrix.android.internal.auth.registration.PendingSessionData
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.sqldelight.auth.PendingSessionQueries
import im.vector.matrix.sqldelight.auth.AuthDatabase
import kotlinx.coroutines.withContext
import javax.inject.Inject
internal class SqlitePendingSessionStore @Inject constructor(database: AuthDatabase,
private val mapper: SqlitePendingSessionMapper,
private val coroutineDispatchers: MatrixCoroutineDispatchers) : PendingSessionStore {
private val pendingSessionQueries: PendingSessionQueries = database.pendingSessionQueries
override suspend fun savePendingSessionData(pendingSessionData: PendingSessionData) = withContext(coroutineDispatchers.dbTransaction) {
val pendingSessionEntity = mapper.map(pendingSessionData)
if (pendingSessionEntity != null) {
pendingSessionQueries.transaction {
pendingSessionQueries.delete()
pendingSessionQueries.insertOrUpdate(pendingSessionEntity)
}
}
}
override fun getPendingSessionData(): PendingSessionData? {
val pendingSessionEntity = pendingSessionQueries.get().executeAsOneOrNull()
return mapper.map(pendingSessionEntity)
}
override suspend fun delete() = withContext(coroutineDispatchers.dbTransaction) {
pendingSessionQueries.delete()
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package im.vector.matrix.android.internal.auth.sqlite
import com.squareup.moshi.JsonDataException
import com.squareup.moshi.Moshi
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.auth.data.sessionId
import im.vector.matrix.sqldelight.auth.SessionParamsEntity
import javax.inject.Inject
internal class SqliteSessionParamsMapper @Inject constructor(moshi: Moshi) {
val credentialsAdapter = moshi.adapter(Credentials::class.java)
private val homeServerConnectionConfigAdapter = moshi.adapter(HomeServerConnectionConfig::class.java)
fun map(credentialsJson: String, homeServerConnectionConfigJson: String, isTokenValid: Boolean): SessionParams {
val credentials = credentialsAdapter.fromJson(credentialsJson)
val homeServerConnectionConfig = homeServerConnectionConfigAdapter.fromJson(homeServerConnectionConfigJson)
if (credentials == null || homeServerConnectionConfig == null) {
throw JsonDataException()
}
return SessionParams(credentials, homeServerConnectionConfig, isTokenValid)
}
fun map(sessionParams: SessionParams?): SessionParamsEntity? {
if (sessionParams == null) {
return null
}
val credentialsJson = credentialsAdapter.toJson(sessionParams.credentials)
val homeServerConnectionConfigJson = homeServerConnectionConfigAdapter.toJson(sessionParams.homeServerConnectionConfig)
if (credentialsJson == null || homeServerConnectionConfigJson == null) {
return null
}
return SessionParamsEntity.Impl(
session_id = sessionParams.credentials.sessionId(),
user_id = sessionParams.credentials.userId,
credentials_json = credentialsJson,
home_server_connection_config_json = homeServerConnectionConfigJson,
is_token_valid = sessionParams.isTokenValid)
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package im.vector.matrix.android.internal.auth.sqlite
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.auth.data.sessionId
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.sqldelight.auth.SessionParamsQueries
import im.vector.matrix.sqldelight.auth.AuthDatabase
import javax.inject.Inject
internal class SqliteSessionParamsStore @Inject constructor(database: AuthDatabase,
private val mapper: SqliteSessionParamsMapper) : SessionParamsStore {
private val sessionParamsQueries: SessionParamsQueries = database.sessionParamsQueries
override fun get(sessionId: String): SessionParams? {
return sessionParamsQueries.getSessionParamsWithId(sessionId) { credentials_json, home_server_connection_config_json, is_token_valid ->
mapper.map(credentials_json, home_server_connection_config_json, is_token_valid)
}.executeAsOneOrNull()
}
override fun getLast(): SessionParams? {
return getAll().lastOrNull()
}
override fun getAll(): List<SessionParams> {
return sessionParamsQueries.getAllSessionParams { credentials_json, home_server_connection_config_json, is_token_valid ->
mapper.map(credentials_json, home_server_connection_config_json, is_token_valid)
}.executeAsList()
}
override suspend fun save(sessionParams: SessionParams) {
val sessionParamsEntity = mapper.map(sessionParams)
if (sessionParamsEntity != null) {
sessionParamsQueries.insert(sessionParamsEntity)
}
}
override suspend fun setTokenInvalid(sessionId: String) {
sessionParamsQueries.setTokenInvalid(sessionId)
}
override suspend fun updateCredentials(newCredentials: Credentials) {
val newCredentialsJson = mapper.credentialsAdapter.toJson(newCredentials)
sessionParamsQueries.updateCredentials(newCredentialsJson, newCredentials.sessionId())
}
override suspend fun delete(sessionId: String) {
sessionParamsQueries.delete(sessionId)
}
override suspend fun deleteAll() {
sessionParamsQueries.deleteAll()
}
}

View File

@@ -0,0 +1,16 @@
package im.vector.matrix.android.internal.concurrency
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import java.util.concurrent.ThreadFactory
internal class NamedThreadFactory(private val name: String) : ThreadFactory {
override fun newThread(runnable: Runnable): Thread {
return Thread(runnable, name)
}
}
internal fun newNamedSingleThreadExecutor(name: String): Executor {
return Executors.newSingleThreadExecutor(NamedThreadFactory(name))
}

View File

@@ -16,9 +16,15 @@
package im.vector.matrix.android.internal.crypto
import android.content.Context
import android.util.Log
import com.squareup.sqldelight.ColumnAdapter
import com.squareup.sqldelight.android.AndroidSqliteDriver
import com.squareup.sqldelight.logs.LogSqliteDriver
import dagger.Binds
import dagger.Module
import dagger.Provides
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
import im.vector.matrix.android.internal.crypto.api.CryptoApi
@@ -55,9 +61,9 @@ import im.vector.matrix.android.internal.crypto.keysbackup.tasks.StoreRoomSessio
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.StoreSessionsDataTask
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStore
import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreMigration
import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreModule
import im.vector.matrix.android.internal.crypto.store.db.SqlCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask
import im.vector.matrix.android.internal.crypto.tasks.DefaultClaimOneTimeKeysForUsersDevice
import im.vector.matrix.android.internal.crypto.tasks.DefaultDeleteDeviceTask
@@ -86,13 +92,15 @@ import im.vector.matrix.android.internal.crypto.tasks.SetDeviceNameTask
import im.vector.matrix.android.internal.crypto.tasks.UploadKeysTask
import im.vector.matrix.android.internal.crypto.tasks.UploadSignaturesTask
import im.vector.matrix.android.internal.crypto.tasks.UploadSigningKeysTask
import im.vector.matrix.android.internal.database.RealmKeysUtils
import im.vector.matrix.android.internal.di.CryptoDatabase
import im.vector.matrix.android.internal.database.DatabaseKeysUtils
import im.vector.matrix.android.internal.di.RealmCryptoDatabase
import im.vector.matrix.android.internal.di.SessionFilesDirectory
import im.vector.matrix.android.internal.di.UserMd5
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.cache.ClearCacheTask
import im.vector.matrix.android.internal.session.cache.RealmClearCacheTask
import im.vector.matrix.sqldelight.crypto.CrossSigningInfoEntity
import im.vector.matrix.sqldelight.crypto.CryptoDatabase
import io.realm.RealmConfiguration
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
@@ -108,15 +116,15 @@ internal abstract class CryptoModule {
@JvmStatic
@Provides
@CryptoDatabase
@RealmCryptoDatabase
@SessionScope
fun providesRealmConfiguration(@SessionFilesDirectory directory: File,
@UserMd5 userMd5: String,
realmKeysUtils: RealmKeysUtils): RealmConfiguration {
databaseKeysUtils: DatabaseKeysUtils): RealmConfiguration {
return RealmConfiguration.Builder()
.directory(directory)
.apply {
realmKeysUtils.configureEncryption(this, getKeyAlias(userMd5))
databaseKeysUtils.configureEncryption(this, getKeyAlias(userMd5))
}
.name("crypto_store.realm")
.modules(RealmCryptoStoreModule())
@@ -125,6 +133,31 @@ internal abstract class CryptoModule {
.build()
}
@JvmStatic
@Provides
@SessionScope
fun providesCryptoDatabase(context: Context, @SessionFilesDirectory directory: File): CryptoDatabase {
val name = "${directory.name}-matrix-sdk-crypto.db"
val driver = if (BuildConfig.DEBUG) {
LogSqliteDriver(AndroidSqliteDriver(CryptoDatabase.Schema, context, name)) { log ->
Log.d("SQLite", log)
}
} else {
AndroidSqliteDriver(CryptoDatabase.Schema, context, name)
}
val listOfStringAdapter = object : ColumnAdapter<List<String>, String> {
override fun decode(databaseValue: String) = databaseValue.split(",")
override fun encode(value: List<String>) = value.joinToString(separator = ",")
}
return CryptoDatabase(
driver = driver,
crossSigningInfoEntityAdapter = CrossSigningInfoEntity.Adapter(
usagesAdapter = listOfStringAdapter
)
)
}
@JvmStatic
@Provides
@SessionScope
@@ -134,8 +167,8 @@ internal abstract class CryptoModule {
@JvmStatic
@Provides
@CryptoDatabase
fun providesClearCacheTask(@CryptoDatabase
@RealmCryptoDatabase
fun providesClearCacheTask(@RealmCryptoDatabase
realmConfiguration: RealmConfiguration): ClearCacheTask {
return RealmClearCacheTask(realmConfiguration)
}
@@ -243,7 +276,7 @@ internal abstract class CryptoModule {
abstract fun bindCrossSigningService(service: DefaultCrossSigningService): CrossSigningService
@Binds
abstract fun bindCryptoStore(store: RealmCryptoStore): IMXCryptoStore
abstract fun bindCryptoStore(store: SqlCryptoStore): IMXCryptoStore
@Binds
abstract fun bindComputeShieldTrustTask(task: DefaultComputeTrustTask): ComputeTrustTask

View File

@@ -23,7 +23,6 @@ import android.os.Handler
import android.os.Looper
import androidx.lifecycle.LiveData
import com.squareup.moshi.Types
import com.zhuinden.monarchy.Monarchy
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.NoOpMatrixCallback
@@ -33,7 +32,6 @@ import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.listeners.ProgressListener
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.keyshare.GossipingRequestListener
@@ -53,11 +51,7 @@ import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmEncryptionFa
import im.vector.matrix.android.internal.crypto.crosssigning.DefaultCrossSigningService
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import im.vector.matrix.android.internal.crypto.keysbackup.DefaultKeysBackupService
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.*
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
import im.vector.matrix.android.internal.crypto.model.event.SecretSendEventContent
@@ -65,19 +59,11 @@ import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import im.vector.matrix.android.internal.crypto.model.toRest
import im.vector.matrix.android.internal.crypto.repository.WarnOnUnknownDeviceRepository
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.DeleteDeviceTask
import im.vector.matrix.android.internal.crypto.tasks.DeleteDeviceWithUserPasswordTask
import im.vector.matrix.android.internal.crypto.tasks.GetDeviceInfoTask
import im.vector.matrix.android.internal.crypto.tasks.GetDevicesTask
import im.vector.matrix.android.internal.crypto.tasks.SetDeviceNameTask
import im.vector.matrix.android.internal.crypto.tasks.UploadKeysTask
import im.vector.matrix.android.internal.crypto.tasks.*
import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationService
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.query.whereType
import im.vector.matrix.android.internal.database.repository.CurrentStateEventDataSource
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.session.SessionScope
@@ -89,13 +75,8 @@ import im.vector.matrix.android.internal.task.TaskThread
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.JsonCanonicalizer
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.fetchCopied
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import im.vector.matrix.sqldelight.session.SessionDatabase
import kotlinx.coroutines.*
import org.matrix.olm.OlmManager
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
@@ -159,7 +140,7 @@ internal class DefaultCryptoService @Inject constructor(
private val setDeviceNameTask: SetDeviceNameTask,
private val uploadKeysTask: UploadKeysTask,
private val loadRoomMembersTask: LoadRoomMembersTask,
private val monarchy: Monarchy,
private val sessionDatabase: SessionDatabase,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val taskExecutor: TaskExecutor,
private val cryptoCoroutineScope: CoroutineScope
@@ -412,7 +393,7 @@ internal class DefaultCryptoService @Inject constructor(
}
override fun getCryptoDeviceInfo(userId: String): List<CryptoDeviceInfo> {
return cryptoStore.getUserDevices(userId)?.map { it.value }?.sortedBy { it.deviceId } ?: emptyList()
return cryptoStore.getUserDevices(userId)?.map { it.value } ?: emptyList()
}
override fun getLiveCryptoDeviceInfo(): LiveData<List<CryptoDeviceInfo>> {
@@ -542,12 +523,10 @@ internal class DefaultCryptoService @Inject constructor(
* @return true if the room is encrypted with algorithm MXCRYPTO_ALGORITHM_MEGOLM
*/
override fun isRoomEncrypted(roomId: String): Boolean {
val encryptionEvent = monarchy.fetchCopied { realm ->
EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
.contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
.findFirst()
}
return encryptionEvent != null
return sessionDatabase.eventQueries
.findWithContent(roomId = roomId, content = "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
.executeAsList()
.firstOrNull() != null
}
/**
@@ -767,30 +746,19 @@ internal class DefaultCryptoService @Inject constructor(
return
}
if (!handleSDKLevelGossip(existingRequest.secretName, secretContent.secretValue)) {
// TODO Ask to application layer?
Timber.v("## onSecretSend() : secret not handled by SDK")
}
}
/**
* Returns true if handled by SDK, otherwise should be sent to application layer
*/
private fun handleSDKLevelGossip(secretName: String?, secretValue: String): Boolean {
return when (secretName) {
when (existingRequest.secretName) {
SELF_SIGNING_KEY_SSSS_NAME -> {
crossSigningService.onSecretSSKGossip(secretValue)
true
crossSigningService.onSecretSSKGossip(secretContent.secretValue)
return
}
USER_SIGNING_KEY_SSSS_NAME -> {
crossSigningService.onSecretUSKGossip(secretValue)
true
crossSigningService.onSecretUSKGossip(secretContent.secretValue)
return
}
KEYBACKUP_SECRET_SSSS_NAME -> {
keysBackupService.onSecretKeyGossip(secretValue)
true
else -> {
// Ask to application layer?
Timber.v("## onSecretSend() : secret not handled by SDK")
}
else -> false
}
}
@@ -804,30 +772,25 @@ internal class DefaultCryptoService @Inject constructor(
val params = LoadRoomMembersTask.Params(roomId)
try {
loadRoomMembersTask.execute(params)
} catch (throwable: Throwable) {
Timber.e(throwable, "## onRoomEncryptionEvent ERROR FAILED TO SETUP CRYPTO ")
} finally {
val userIds = getRoomUserIds(roomId)
setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds)
} catch (throwable: Throwable) {
Timber.e(throwable, "## onRoomEncryptionEvent ERROR FAILED TO SETUP CRYPTO ")
}
}
}
private fun getRoomUserIds(roomId: String): List<String> {
var userIds: List<String> = emptyList()
monarchy.doWithRealm { realm ->
// Check whether the event content must be encrypted for the invited members.
val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser()
&& shouldEncryptForInvitedMembers(roomId)
userIds = if (encryptForInvitedMembers) {
RoomMemberHelper(realm, roomId).getActiveRoomMemberIds()
return if (encryptForInvitedMembers) {
RoomMemberHelper(sessionDatabase, roomId).getActiveRoomMemberIds()
} else {
RoomMemberHelper(realm, roomId).getJoinedRoomMemberIds()
RoomMemberHelper(sessionDatabase, roomId).getJoinedRoomMemberIds()
}
}
return userIds
}
/**
* Handle a change in the membership state of a member of a room.

View File

@@ -15,23 +15,18 @@
*/
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.SessionDatabase
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.internal.database.mapper.map
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.createBackgroundHandler
import io.realm.Realm
import io.realm.RealmConfiguration
import im.vector.matrix.sqldelight.session.SessionDatabase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject
internal class ShieldTrustUpdater @Inject constructor(
@@ -39,35 +34,23 @@ internal class ShieldTrustUpdater @Inject constructor(
private val computeTrustTask: ComputeTrustTask,
private val taskExecutor: TaskExecutor,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
@SessionDatabase private val sessionRealmConfiguration: RealmConfiguration,
private val roomSummaryUpdater: RoomSummaryUpdater
) {
private val sessionDatabase: SessionDatabase) {
companion object {
private val BACKGROUND_HANDLER = createBackgroundHandler("SHIELD_CRYPTO_DB_THREAD")
}
private val backgroundSessionRealm = AtomicReference<Realm>()
private val isStarted = AtomicBoolean()
fun start() {
if (isStarted.compareAndSet(false, true)) {
eventBus.register(this)
BACKGROUND_HANDLER.post {
backgroundSessionRealm.set(Realm.getInstance(sessionRealmConfiguration))
}
}
}
fun stop() {
if (isStarted.compareAndSet(true, false)) {
eventBus.unregister(this)
BACKGROUND_HANDLER.post {
backgroundSessionRealm.getAndSet(null).also {
it?.close()
}
}
}
}
@@ -76,16 +59,7 @@ internal class ShieldTrustUpdater @Inject constructor(
if (!isStarted.get()) {
return
}
taskExecutor.executorScope.launch(coroutineDispatchers.crypto) {
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)
}
}
}
taskExecutor.executorScope.updateTrustLevel(update.roomId, update.userIds)
}
@Subscribe
@@ -93,48 +67,32 @@ internal class ShieldTrustUpdater @Inject constructor(
if (!isStarted.get()) {
return
}
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 ->
backgroundSessionRealm.get()?.let { realm ->
RoomMemberSummaryEntity.where(realm, roomId)
.findAll()
.let { results ->
map[roomId] = results.map { it.userId }
val impactedRoomsId = sessionDatabase.roomMemberSummaryQueries.getAllRoomIdsFromUsers(users).executeAsList()
impactedRoomsId.forEach { roomId ->
val allUsersFromRoom = sessionDatabase.roomMemberSummaryQueries.getAllUserIdFromRoom(
memberships = Membership.all().map(),
excludedIds = emptyList(),
roomId = roomId
).executeAsList()
taskExecutor.executorScope.updateTrustLevel(roomId, allUsersFromRoom)
}
}
}
map.forEach { entry ->
val roomId = entry.key
val userList = entry.value
taskExecutor.executorScope.launch {
withContext(coroutineDispatchers.crypto) {
private fun CoroutineScope.updateTrustLevel(roomId: String, userIds: List<String>) {
launch(coroutineDispatchers.dbTransaction) {
try {
// Can throw if the crypto database has been closed in between, in this case log and ignore?
val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(userList))
BACKGROUND_HANDLER.post {
backgroundSessionRealm.get()?.executeTransaction { realm ->
roomSummaryUpdater.updateShieldTrust(realm, roomId, updatedTrust)
}
}
val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(userIds))
sessionDatabase.roomSummaryQueries.setRoomEncryptionTrustLevel(updatedTrust.name, roomId)
} catch (failure: Throwable) {
Timber.e(failure)
}
}
}
}
}
}
}

View File

@@ -69,7 +69,6 @@ import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.store.SavedKeyBackupKeyInfo
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.extensions.foldToCallback
@@ -1154,11 +1153,11 @@ internal class DefaultKeysBackupService @Inject constructor(
* Update the DB with data fetch from the server
*/
private fun onServerDataRetrieved(count: Int?, hash: String?) {
cryptoStore.setKeysBackupData(KeysBackupDataEntity()
.apply {
backupLastServerNumberOfKeys = count
cryptoStore.setKeysBackupData(
im.vector.matrix.android.internal.crypto.model.rest.KeysBackupData(
backupLastServerNumberOfKeys = count,
backupLastServerHash = hash
}
)
)
}

View File

@@ -0,0 +1,19 @@
/*
* Copyright (c) 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.model.rest
internal data class KeysBackupData(val backupLastServerHash: String?, val backupLastServerNumberOfKeys: Int?)

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.model.rest
import com.squareup.moshi.Json
/**
* Parent class representing an room key action request
* Note: this class cannot be abstract because of [org.matrix.androidsdk.core.JsonUtils.toRoomKeyShare]
*/
internal open class RoomKeyShare : SendToDeviceObject {
var action: String? = null
@Json(name = "requesting_device_id")
var requestingDeviceId: String? = null
@Json(name = "request_id")
var requestId: String? = null
companion object {
const val ACTION_SHARE_REQUEST = "request"
const val ACTION_SHARE_CANCELLATION = "request_cancellation"
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.model.rest
import com.squareup.moshi.JsonClass
/**
* Class representing an room key request cancellation content
*/
@JsonClass(generateAdapter = true)
internal class RoomKeyShareCancellation : RoomKeyShare() {
init {
action = ACTION_SHARE_CANCELLATION
}
}

View File

@@ -32,8 +32,8 @@ import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
import im.vector.matrix.android.internal.crypto.model.rest.KeysBackupData
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
import org.matrix.olm.OlmAccount
/**
@@ -104,14 +104,14 @@ internal interface IMXCryptoStore {
/**
* Get the current keys backup local data
*/
fun getKeysBackupData(): KeysBackupDataEntity?
fun getKeysBackupData(): KeysBackupData?
/**
* Set the keys backup local data
*
* @param keysBackupData the keys backup local data, or null to erase data
*/
fun setKeysBackupData(keysBackupData: KeysBackupDataEntity?)
fun setKeysBackupData(keysBackupData: KeysBackupData?)
/**
* @return the devices statuses map (userId -> tracking status)

View File

@@ -41,6 +41,7 @@ import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
import im.vector.matrix.android.internal.crypto.model.rest.KeysBackupData
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import im.vector.matrix.android.internal.crypto.model.toEntity
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
@@ -74,8 +75,8 @@ import im.vector.matrix.android.internal.crypto.store.db.query.get
import im.vector.matrix.android.internal.crypto.store.db.query.getById
import im.vector.matrix.android.internal.crypto.store.db.query.getOrCreate
import im.vector.matrix.android.internal.database.mapper.ContentMapper
import im.vector.matrix.android.internal.di.CryptoDatabase
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.di.RealmCryptoDatabase
import im.vector.matrix.android.internal.session.SessionScope
import io.realm.Realm
import io.realm.RealmConfiguration
@@ -90,7 +91,7 @@ import kotlin.collections.set
@SessionScope
internal class RealmCryptoStore @Inject constructor(
@CryptoDatabase private val realmConfiguration: RealmConfiguration,
@RealmCryptoDatabase private val realmConfiguration: RealmConfiguration,
private val credentials: Credentials) : IMXCryptoStore {
/* ==========================================================================================
@@ -705,13 +706,18 @@ internal class RealmCryptoStore @Inject constructor(
}
}
override fun getKeysBackupData(): KeysBackupDataEntity? {
override fun getKeysBackupData(): KeysBackupData? {
return doRealmQueryAndCopy(realmConfiguration) {
it.where<KeysBackupDataEntity>().findFirst()
}?.let { entity ->
return KeysBackupData(
backupLastServerHash = entity.backupLastServerHash,
backupLastServerNumberOfKeys = entity.backupLastServerNumberOfKeys
)
}
}
override fun setKeysBackupData(keysBackupData: KeysBackupDataEntity?) {
override fun setKeysBackupData(keysBackupData: KeysBackupData?) {
doRealmTransaction(realmConfiguration) {
if (keysBackupData == null) {
// Clear the table
@@ -720,7 +726,10 @@ internal class RealmCryptoStore @Inject constructor(
.deleteAllFromRealm()
} else {
// Only one object
it.copyToRealmOrUpdate(keysBackupData)
it.copyToRealmOrUpdate(it.createObject(KeysBackupDataEntity::class.java).apply {
backupLastServerHash = keysBackupData.backupLastServerHash
backupLastServerNumberOfKeys = keysBackupData.backupLastServerNumberOfKeys
})
}
}
}

View File

@@ -0,0 +1,242 @@
/*
* Copyright (c) 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.store.db
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import im.vector.matrix.android.api.extensions.tryThis
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.crypto.GossipRequestType
import im.vector.matrix.android.internal.crypto.GossipingRequestState
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest
import im.vector.matrix.android.internal.crypto.IncomingShareRequestCommon
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequest
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestState
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.OutgoingSecretRequest
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.di.SerializeNulls
import im.vector.matrix.sqldelight.crypto.CrossSigningInfoEntity
import im.vector.matrix.sqldelight.crypto.DeviceInfoEntity
import im.vector.matrix.sqldelight.crypto.GetByUserId
import im.vector.matrix.sqldelight.crypto.IncomingGossipingRequestEntity
import im.vector.matrix.sqldelight.crypto.OutgoingGossipingRequestEntity
import timber.log.Timber
object SqliteCryptoMapper {
private val moshi = Moshi.Builder().add(SerializeNulls.JSON_ADAPTER_FACTORY).build()
private val listMigrationAdapter = moshi.adapter<List<String>>(Types.newParameterizedType(
List::class.java,
String::class.java,
Any::class.java
))
private val mapMigrationAdapter = moshi.adapter<JsonDict>(Types.newParameterizedType(
Map::class.java,
String::class.java,
Any::class.java
))
private val mapOfStringMigrationAdapter = moshi.adapter<Map<String, Map<String, String>>>(Types.newParameterizedType(
Map::class.java,
String::class.java,
Any::class.java
))
private val recipientsDataMapper: JsonAdapter<Map<String, List<String>>> =
MoshiProvider
.providesMoshi()
.adapter<Map<String, List<String>>>(
Types.newParameterizedType(Map::class.java, String::class.java, List::class.java)
)
internal fun mapToEntity(deviceInfo: CryptoDeviceInfo): DeviceInfoEntity {
return DeviceInfoEntity.Impl(
user_id = deviceInfo.userId,
device_id = deviceInfo.deviceId,
identity_key = deviceInfo.identityKey(),
algorithm_list_json = listMigrationAdapter.toJson(deviceInfo.algorithms),
keys_map_json = mapMigrationAdapter.toJson(deviceInfo.keys),
signature_map_json = mapMigrationAdapter.toJson(deviceInfo.signatures),
is_blocked = deviceInfo.isBlocked,
locally_verified = deviceInfo.trustLevel?.locallyVerified == true,
cross_signed_verified = deviceInfo.trustLevel?.crossSigningVerified == true,
unsigned_map_json = mapMigrationAdapter.toJson(deviceInfo.unsigned)
)
}
internal fun mapToModel(deviceInfoEntity: DeviceInfoEntity): CryptoDeviceInfo {
return CryptoDeviceInfo(
deviceId = deviceInfoEntity.device_id,
userId = deviceInfoEntity.user_id,
algorithms = deviceInfoEntity.algorithm_list_json?.let {
try {
listMigrationAdapter.fromJson(it)
} catch (failure: Throwable) {
Timber.e(failure)
null
}
},
keys = deviceInfoEntity.keys_map_json?.let {
try {
moshi.adapter<Map<String, String>>(Types.newParameterizedType(
Map::class.java,
String::class.java,
Any::class.java
)).fromJson(it)
} catch (failure: Throwable) {
Timber.e(failure)
null
}
},
signatures = deviceInfoEntity.signature_map_json?.let {
try {
mapOfStringMigrationAdapter.fromJson(it)
} catch (failure: Throwable) {
Timber.e(failure)
null
}
},
unsigned = deviceInfoEntity.unsigned_map_json?.let {
try {
mapMigrationAdapter.fromJson(it)
} catch (failure: Throwable) {
Timber.e(failure)
null
}
},
trustLevel = DeviceTrustLevel(deviceInfoEntity.cross_signed_verified, deviceInfoEntity.locally_verified),
isBlocked = deviceInfoEntity.is_blocked
)
}
internal fun mapToModel(crossSigningInfoEntity: GetByUserId): CryptoCrossSigningKey {
val pubKey = crossSigningInfoEntity.public_key_base64 ?: ""
return CryptoCrossSigningKey(
userId = crossSigningInfoEntity.user_id,
signatures = crossSigningInfoEntity.signatures?.let { deserializeSignaturesFromSqlite(it) },
trustLevel = DeviceTrustLevel(
crossSigningInfoEntity.cross_signed_verified,
crossSigningInfoEntity.locally_verified
),
keys = mapOf("ed25519:$pubKey" to pubKey),
usages = crossSigningInfoEntity.usages
)
}
internal fun mapToEntity(cryptoCrossSigningKey: CryptoCrossSigningKey): CrossSigningInfoEntity {
return CrossSigningInfoEntity.Impl(
user_id = cryptoCrossSigningKey.userId,
signatures = serializeSignaturesForSqlite(cryptoCrossSigningKey.signatures),
public_key_base64 = cryptoCrossSigningKey.unpaddedBase64PublicKey,
usages = cryptoCrossSigningKey.usages ?: emptyList(),
locally_verified = false,
cross_signed_verified = false
)
}
fun serializeSignaturesForSqlite(signatures: Map<String, Map<String, String>>?): String {
return mapMigrationAdapter.toJson(signatures)
}
fun deserializeSignaturesFromSqlite(signatures: String):Map<String, Map<String, String>>? {
return try {
mapOfStringMigrationAdapter.fromJson(signatures)
} catch (failure: Throwable) {
Timber.e(failure)
null
}
}
fun toOutgoingGossipingRequest(entity: OutgoingGossipingRequestEntity): OutgoingGossipingRequest {
return when (entity.type) {
GossipRequestType.KEY.name -> {
OutgoingRoomKeyRequest(
requestBody = getRequestedKeyInfo(entity.type, entity.requested_info),
recipients = getRecipientsMap(entity) ?: emptyMap(),
requestId = entity.request_id,
state = tryThis { OutgoingGossipingRequestState.valueOf(entity.requested_info!!) }
?: OutgoingGossipingRequestState.UNSENT
)
}
GossipRequestType.SECRET.name -> {
OutgoingSecretRequest(
secretName = getRequestedSecretName(entity.type, entity.requested_info),
recipients = getRecipientsMap(entity) ?: emptyMap(),
requestId = entity.request_id,
state = tryThis { OutgoingGossipingRequestState.valueOf(entity.requested_info!!) }
?: OutgoingGossipingRequestState.UNSENT
)
}
else -> OutgoingRoomKeyRequest(
requestBody = getRequestedKeyInfo(entity.type, entity.requested_info),
recipients = getRecipientsMap(entity) ?: emptyMap(),
requestId = entity.request_id,
state = OutgoingGossipingRequestState.UNSENT
)
}
}
fun toIncomingGossipingRequest(entity: IncomingGossipingRequestEntity): IncomingShareRequestCommon {
return when (GossipRequestType.valueOf(entity.type)) {
GossipRequestType.KEY -> {
IncomingRoomKeyRequest(
requestBody = getRequestedKeyInfo(entity.type, entity.requested_info_str),
deviceId = entity.other_device_id,
userId = entity.other_user_id,
requestId = entity.request_id,
state = entity.request_state?.let { GossipingRequestState.valueOf(it) }
?: GossipingRequestState.NONE,
localCreationTimestamp = entity.local_creation_timestamp
)
}
GossipRequestType.SECRET -> {
IncomingSecretShareRequest(
secretName = getRequestedSecretName(entity.type, entity.requested_info_str),
deviceId = entity.other_device_id,
userId = entity.other_user_id,
requestId = entity.request_id,
localCreationTimestamp = entity.local_creation_timestamp
)
}
}
}
private fun getRecipientsMap(entity: OutgoingGossipingRequestEntity): Map<String, List<String>>? {
return entity.recipients_data?.let { recipientsDataMapper.fromJson(it) }
}
fun getRecipientsData(recipients: Map<String, List<String>>): String? {
return recipientsDataMapper.toJson(recipients)
}
fun getRequestedKeyInfo(type: String, requestedInfoStr: String?): RoomKeyRequestBody? {
return if (type == GossipRequestType.KEY.name) {
RoomKeyRequestBody.fromJson(requestedInfoStr)
} else null
}
fun getRequestedSecretName(type: String, requestedInfoStr: String?): String? {
return if (type == GossipRequestType.SECRET.name) {
requestedInfoStr
} else null
}
}

View File

@@ -0,0 +1,49 @@
package im.vector.matrix.android.internal.crypto.store.db
import android.util.Base64
import im.vector.matrix.android.internal.util.CompatUtil
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.util.zip.GZIPInputStream
/**
* Serialize any Serializable object, zip it and convert to Base64 String
*/
fun serializeForSqlite(o: Any?): String? {
if (o == null) {
return null
}
val baos = ByteArrayOutputStream()
val gzis = CompatUtil.createGzipOutputStream(baos)
val out = ObjectOutputStream(gzis)
out.writeObject(o)
out.close()
return Base64.encodeToString(baos.toByteArray(), Base64.DEFAULT)
}
/**
* Do the opposite of serializeForSqlite.
*/
fun <T> deserializeFromSqlite(string: String?): T? {
if (string == null) {
return null
}
val decodedB64 = Base64.decode(string.toByteArray(), Base64.DEFAULT)
val bais = ByteArrayInputStream(decodedB64)
val gzis = GZIPInputStream(bais)
val ois = ObjectInputStream(gzis)
@Suppress("UNCHECKED_CAST")
val result = ois.readObject() as T
ois.close()
return result
}

View File

@@ -0,0 +1,56 @@
// /*
// * Copyright 2018 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.store.db.model
//
// import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
// import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
// import io.realm.RealmObject
//
// internal open class IncomingRoomKeyRequestEntity(
// var requestId: String? = null,
// var userId: String? = null,
// var deviceId: String? = null,
// // RoomKeyRequestBody fields
// var requestBodyAlgorithm: String? = null,
// var requestBodyRoomId: String? = null,
// var requestBodySenderKey: String? = null,
// var requestBodySessionId: String? = null
// ) : RealmObject() {
//
// fun toIncomingRoomKeyRequest(): IncomingRoomKeyRequest {
// return IncomingRoomKeyRequest(
// requestId = requestId,
// userId = userId,
// deviceId = deviceId,
// requestBody = RoomKeyRequestBody(
// algorithm = requestBodyAlgorithm,
// roomId = requestBodyRoomId,
// senderKey = requestBodySenderKey,
// sessionId = requestBodySessionId
// )
// )
// }
//
// fun putRequestBody(requestBody: RoomKeyRequestBody?) {
// requestBody?.let {
// requestBodyAlgorithm = it.algorithm
// requestBodyRoomId = it.roomId
// requestBodySenderKey = it.senderKey
// requestBodySessionId = it.sessionId
// }
// }
// }

View File

@@ -0,0 +1,37 @@
// /*
// * Copyright (c) 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.store.db.model
//
// import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest
// import io.realm.RealmObject
//
// internal open class IncomingSecretRequestEntity(
// var requestId: String? = null,
// var userId: String? = null,
// var deviceId: String? = null,
// var secretName: String? = null
// ) : RealmObject() {
//
// fun toIncomingSecretShareRequest(): IncomingSecretShareRequest {
// return IncomingSecretShareRequest(
// requestId = requestId,
// userId = userId,
// deviceId = deviceId,
// secretName = secretName
// )
// }
// }

View File

@@ -0,0 +1,77 @@
// /*
// * Copyright 2018 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.store.db.model
//
// import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
// import im.vector.matrix.android.internal.crypto.ShareRequestState
// import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
// import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm
// import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm
// import io.realm.RealmObject
// import io.realm.annotations.PrimaryKey
//
// internal open class OutgoingRoomKeyRequestEntity(
// @PrimaryKey var requestId: String? = null,
// var cancellationTxnId: String? = null,
// // Serialized Json
// var recipientsData: String? = null,
// // RoomKeyRequestBody fields
// var requestBodyAlgorithm: String? = null,
// var requestBodyRoomId: String? = null,
// var requestBodySenderKey: String? = null,
// var requestBodySessionId: String? = null,
// // State
// var state: Int = 0
// ) : RealmObject() {
//
// /**
// * Convert to OutgoingRoomKeyRequest
// */
// fun toOutgoingRoomKeyRequest(): OutgoingRoomKeyRequest {
// val cancellationTxnId = this.cancellationTxnId
// return OutgoingRoomKeyRequest(
// RoomKeyRequestBody(
// algorithm = requestBodyAlgorithm,
// roomId = requestBodyRoomId,
// senderKey = requestBodySenderKey,
// sessionId = requestBodySessionId
// ),
// getRecipients()!!,
// requestId!!,
// ShareRequestState.from(state)
// ).apply {
// this.cancellationTxnId = cancellationTxnId
// }
// }
//
// private fun getRecipients(): List<Map<String, String>>? {
// return deserializeFromRealm(recipientsData)
// }
//
// fun putRecipients(recipients: List<Map<String, String>>?) {
// recipientsData = serializeForRealm(recipients)
// }
//
// fun putRequestBody(requestBody: RoomKeyRequestBody?) {
// requestBody?.let {
// requestBodyAlgorithm = it.algorithm
// requestBodyRoomId = it.roomId
// requestBodySenderKey = it.senderKey
// requestBodySessionId = it.sessionId
// }
// }
// }

View File

@@ -0,0 +1,63 @@
// /*
// * Copyright (c) 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.store.db.model
//
// import im.vector.matrix.android.internal.crypto.OutgoingSecretRequest
// import im.vector.matrix.android.internal.crypto.ShareRequestState
// import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm
// import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm
// import io.realm.RealmObject
// import io.realm.annotations.PrimaryKey
//
// internal open class OutgoingSecretRequestEntity(
// @PrimaryKey var requestId: String? = null,
// var cancellationTxnId: String? = null,
// // Serialized Json
// var recipientsData: String? = null,
// // RoomKeyRequestBody fields
// var secretName: String? = null,
// // State
// var state: Int = 0
// ) : RealmObject() {
//
// /**
// * Convert to OutgoingRoomKeyRequest
// */
// fun toOutgoingSecretRequest(): OutgoingSecretRequest {
// val cancellationTxnId = this.cancellationTxnId
// return OutgoingSecretRequest(
// secretName,
// getRecipients() ?: emptyList(),
// requestId!!,
// ShareRequestState.from(state)
// ).apply {
// this.cancellationTxnId = cancellationTxnId
// }
// }
//
// private fun getRecipients(): List<Map<String, String>>? {
// return try {
// deserializeFromRealm(recipientsData)
// } catch (failure: Throwable) {
// null
// }
// }
//
// fun putRecipients(recipients: List<Map<String, String>>?) {
// recipientsData = serializeForRealm(recipients)
// }
// }

View File

@@ -19,35 +19,33 @@ package im.vector.matrix.android.internal.crypto.tasks
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.crypto.verification.VerificationService
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.LocalEcho
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationReadyContent
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationStartContent
import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationService
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.di.DeviceId
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.sqldelight.session.EventInsertNotification
import im.vector.matrix.sqldelight.session.SessionDatabase
import timber.log.Timber
import java.util.ArrayList
import java.util.*
import javax.inject.Inject
internal interface RoomVerificationUpdateTask : Task<RoomVerificationUpdateTask.Params, Unit> {
data class Params(
val events: List<Event>,
val verificationService: DefaultVerificationService,
val cryptoService: CryptoService
val eventInsertNotifications: List<EventInsertNotification>
)
}
internal class DefaultRoomVerificationUpdateTask @Inject constructor(
@UserId private val userId: String,
@DeviceId private val deviceId: String?,
private val sessionDatabase: SessionDatabase,
private val verificationService: DefaultVerificationService,
private val cryptoService: CryptoService) : RoomVerificationUpdateTask {
companion object {
@@ -57,8 +55,15 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
override suspend fun execute(params: RoomVerificationUpdateTask.Params) {
// TODO ignore initial sync or back pagination?
params.eventInsertNotifications.forEach { eventInsertNotification ->
val eventId = eventInsertNotification.event_id
val isLocalEcho = LocalEcho.isLocalEchoId(eventId)
if (isLocalEcho) {
return@forEach
}
val event = sessionDatabase.eventQueries.select(eventId).executeAsOneOrNull()?.asDomain()
?: return@forEach
params.events.forEach { event ->
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}")
// If the request is in the future by more than 5 minutes or more than 10 minutes in the past,
@@ -74,7 +79,7 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
// TODO use a global event decryptor? attache to session and that listen to new sessionId?
// for now decrypt sync
try {
val result = cryptoService.decryptEvent(event, "")
val result = cryptoService.decryptEvent(event, event.roomId + UUID.randomUUID().toString())
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
@@ -83,7 +88,7 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
)
} catch (e: MXCryptoError) {
Timber.e("## SAS Failed to decrypt event: ${event.eventId}")
params.verificationService.onPotentiallyInterestingEventRoomFailToDecrypt(event)
verificationService.onPotentiallyInterestingEventRoomFailToDecrypt(event)
}
}
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}")
@@ -112,7 +117,7 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
// The verification is started from another device
Timber.v("## SAS Verification live observer: Transaction started by other device tid:$relatesToEventId ")
relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
params.verificationService.onRoomRequestHandledByOtherDevice(event)
verificationService.onRoomRequestHandledByOtherDevice(event)
}
}
} else if (EventType.KEY_VERIFICATION_READY == event.getClearType()) {
@@ -121,13 +126,13 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
// The verification is started from another device
Timber.v("## SAS Verification live observer: Transaction started by other device tid:$relatesToEventId ")
relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
params.verificationService.onRoomRequestHandledByOtherDevice(event)
verificationService.onRoomRequestHandledByOtherDevice(event)
}
}
} else if (EventType.KEY_VERIFICATION_CANCEL == event.getClearType() || EventType.KEY_VERIFICATION_DONE == event.getClearType()) {
relatesToEventId?.let {
transactionsHandledByOtherDevice.remove(it)
params.verificationService.onRoomRequestHandledByOtherDevice(event)
verificationService.onRoomRequestHandledByOtherDevice(event)
}
}
@@ -148,11 +153,11 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_READY,
EventType.KEY_VERIFICATION_DONE -> {
params.verificationService.onRoomEvent(event)
verificationService.onRoomEvent(event)
}
EventType.MESSAGE -> {
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.msgType) {
params.verificationService.onRoomRequestReceived(event)
verificationService.onRoomRequestReceived(event)
}
}
}

View File

@@ -15,34 +15,21 @@
*/
package im.vector.matrix.android.internal.crypto.verification
import com.zhuinden.monarchy.Monarchy
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.events.model.LocalEcho
import im.vector.matrix.android.internal.crypto.tasks.DefaultRoomVerificationUpdateTask
import im.vector.matrix.android.internal.crypto.tasks.RoomVerificationUpdateTask
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.query.whereTypes
import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import io.realm.OrderedCollectionChangeSet
import io.realm.RealmConfiguration
import io.realm.RealmResults
import im.vector.matrix.android.internal.database.SqlLiveEntityObserver
import im.vector.matrix.sqldelight.session.EventInsertNotification
import im.vector.matrix.sqldelight.session.SessionDatabase
import javax.inject.Inject
internal class VerificationMessageLiveObserver @Inject constructor(
@SessionDatabase realmConfiguration: RealmConfiguration,
private val roomVerificationUpdateTask: DefaultRoomVerificationUpdateTask,
private val cryptoService: CryptoService,
private val verificationService: DefaultVerificationService,
private val taskExecutor: TaskExecutor
) : RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
sessionDatabase: SessionDatabase,
private val roomVerificationUpdateTask: DefaultRoomVerificationUpdateTask
) : SqlLiveEntityObserver<EventInsertNotification>(sessionDatabase) {
override val query = Monarchy.Query {
EventEntity.whereTypes(it, listOf(
override val query = sessionDatabase.observerTriggerQueries.getAllEventInsertNotifications(
types = listOf(
EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_KEY,
@@ -53,21 +40,12 @@ internal class VerificationMessageLiveObserver @Inject constructor(
EventType.MESSAGE,
EventType.ENCRYPTED)
)
override suspend fun handleChanges(results: List<EventInsertNotification>) {
val params = RoomVerificationUpdateTask.Params(results)
roomVerificationUpdateTask.execute(params)
val notificationIds = results.map { it.event_id }
sessionDatabase.observerTriggerQueries.deleteEventInsertNotifications(notificationIds)
}
override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
// Should we ignore when it's an initial sync?
val events = changeSet.insertions
.asSequence()
.mapNotNull { results[it]?.asDomain() }
.filterNot {
// ignore local echos
LocalEcho.isLocalEchoId(it.eventId ?: "")
}
.toList()
roomVerificationUpdateTask.configureWith(
RoomVerificationUpdateTask.Params(events, verificationService, cryptoService)
).executeBy(taskExecutor)
}
}

View File

@@ -15,6 +15,8 @@
*/
package im.vector.matrix.android.internal.database
import com.squareup.sqldelight.Transacter
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import io.realm.Realm
import io.realm.RealmConfiguration
import kotlinx.coroutines.Dispatchers
@@ -22,7 +24,7 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
import timber.log.Timber
suspend fun <T> awaitTransaction(config: RealmConfiguration, transaction: suspend (realm: Realm) -> T) = withContext(Dispatchers.Default) {
suspend fun <T> awaitTransaction(config: RealmConfiguration, transaction: suspend (realm: Realm) -> T) = withContext(Dispatchers.IO) {
Realm.getInstance(config).use { bgRealm ->
bgRealm.beginTransaction()
val result: T
@@ -43,3 +45,22 @@ suspend fun <T> awaitTransaction(config: RealmConfiguration, transaction: suspen
result
}
}
internal suspend fun <T : Transacter> T.awaitTransaction(dispatchers: MatrixCoroutineDispatchers, noEnclosing: Boolean = false, transaction: (db: T) -> Unit) = withContext(dispatchers.dbTransaction) {
transaction(noEnclosing = noEnclosing) {
try {
val start = System.currentTimeMillis()
transaction(this@awaitTransaction)
if (isActive) {
val end = System.currentTimeMillis()
val time = end - start
Timber.v("Execute transaction in $time millis")
} else {
rollback()
}
} catch (exception: Exception) {
Timber.e(exception)
rollback()
}
}
}

View File

@@ -17,9 +17,11 @@ package im.vector.matrix.android.internal.database
import android.content.Context
import android.util.Base64
import androidx.sqlite.db.SupportSQLiteOpenHelper
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.internal.session.securestorage.SecretStoringUtils
import io.realm.RealmConfiguration
import net.sqlcipher.database.SupportFactory
import timber.log.Timber
import java.security.SecureRandom
import javax.inject.Inject
@@ -36,7 +38,7 @@ import javax.inject.Inject
* then we generate a random secret key. The database key is encrypted with the secret key; The secret
* key is encrypted with the public RSA key and stored with the encrypted key in the shared pref
*/
internal class RealmKeysUtils @Inject constructor(context: Context,
internal class DatabaseKeysUtils @Inject constructor(context: Context,
private val secretStoringUtils: SecretStoringUtils) {
private val rng = SecureRandom()
@@ -85,7 +87,7 @@ internal class RealmKeysUtils @Inject constructor(context: Context,
return Base64.decode(b64!!, Base64.NO_PADDING)
}
fun configureEncryption(realmConfigurationBuilder: RealmConfiguration.Builder, alias: String) {
private fun getOrCreateEncryptionKey(alias: String): ByteArray {
val key = if (hasKeyForDatabase(alias)) {
Timber.i("Found key for alias:$alias")
extractKeyForDatabase(alias)
@@ -98,7 +100,16 @@ internal class RealmKeysUtils @Inject constructor(context: Context,
val log = key.joinToString("") { "%02x".format(it) }
Timber.w("Database key for alias `$alias`: $log")
}
return key
}
fun createEncryptedSQLiteOpenHelperFactory(alias: String): SupportSQLiteOpenHelper.Factory {
val key = getOrCreateEncryptionKey(alias)
return SupportFactory(key)
}
fun configureEncryption(realmConfigurationBuilder: RealmConfiguration.Builder, alias: String) {
val key = getOrCreateEncryptionKey(alias)
realmConfigurationBuilder.encryptionKey(key)
}

View File

@@ -0,0 +1,70 @@
/*
* 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
import com.squareup.sqldelight.Query
import im.vector.matrix.sqldelight.session.SessionDatabase
import kotlinx.coroutines.*
import java.util.concurrent.atomic.AtomicBoolean
internal interface LiveEntityObserver {
fun start()
fun dispose()
fun cancelProcess()
fun isStarted(): Boolean
}
internal abstract class SqlLiveEntityObserver<T : Any>(protected val sessionDatabase: SessionDatabase)
: LiveEntityObserver, Query.Listener {
protected val observerScope = CoroutineScope(SupervisorJob())
protected abstract val query: Query<T>
private val isStarted = AtomicBoolean(false)
protected abstract suspend fun handleChanges(results: List<T>)
override fun queryResultsChanged() {
observerScope.launch(Dispatchers.Default) {
val results = query.executeAsList()
if(results.isNotEmpty()) {
handleChanges(results)
}
}
}
override fun start() {
if (isStarted.compareAndSet(false, true)) {
query.addListener(this)
}
}
override fun dispose() {
if (isStarted.compareAndSet(true, false)) {
query.removeListener(this)
observerScope.coroutineContext.cancelChildren()
}
}
override fun cancelProcess() {
observerScope.coroutineContext.cancelChildren()
}
override fun isStarted(): Boolean {
return isStarted.get()
}
}

View File

@@ -1,83 +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
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.util.createBackgroundHandler
import io.realm.OrderedRealmCollectionChangeListener
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.RealmObject
import io.realm.RealmResults
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelChildren
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
internal interface LiveEntityObserver {
fun start()
fun dispose()
fun cancelProcess()
fun isStarted(): Boolean
}
internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val realmConfiguration: RealmConfiguration)
: LiveEntityObserver, OrderedRealmCollectionChangeListener<RealmResults<T>> {
private companion object {
val BACKGROUND_HANDLER = createBackgroundHandler("LIVE_ENTITY_BACKGROUND")
}
protected val observerScope = CoroutineScope(SupervisorJob())
protected abstract val query: Monarchy.Query<T>
private val isStarted = AtomicBoolean(false)
private val backgroundRealm = AtomicReference<Realm>()
private lateinit var results: AtomicReference<RealmResults<T>>
override fun start() {
if (isStarted.compareAndSet(false, true)) {
BACKGROUND_HANDLER.post {
val realm = Realm.getInstance(realmConfiguration)
backgroundRealm.set(realm)
val queryResults = query.createQuery(realm).findAll()
queryResults.addChangeListener(this)
results = AtomicReference(queryResults)
}
}
}
override fun dispose() {
if (isStarted.compareAndSet(true, false)) {
BACKGROUND_HANDLER.post {
results.getAndSet(null).removeAllChangeListeners()
backgroundRealm.getAndSet(null).also {
it.close()
}
observerScope.coroutineContext.cancelChildren()
}
}
}
override fun cancelProcess() {
observerScope.coroutineContext.cancelChildren()
}
override fun isStarted(): Boolean {
return isStarted.get()
}
}

View File

@@ -1,60 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.database
import io.realm.Realm
import io.realm.RealmChangeListener
import io.realm.RealmConfiguration
import io.realm.RealmQuery
import io.realm.RealmResults
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
internal suspend fun <T> awaitNotEmptyResult(realmConfiguration: RealmConfiguration,
timeoutMillis: Long,
builder: (Realm) -> RealmQuery<T>) {
withTimeout(timeoutMillis) {
// Confine Realm interaction to a single thread with Looper.
withContext(Dispatchers.Main) {
val latch = CompletableDeferred<Unit>()
Realm.getInstance(realmConfiguration).use { realm ->
val result = builder(realm).findAllAsync()
val listener = object : RealmChangeListener<RealmResults<T>> {
override fun onChange(it: RealmResults<T>) {
if (it.isNotEmpty()) {
result.removeChangeListener(this)
latch.complete(Unit)
}
}
}
result.addChangeListener(listener)
try {
latch.await()
} catch (e: CancellationException) {
result.removeChangeListener(listener)
throw e
}
}
}
}
}

View File

@@ -16,79 +16,36 @@
package im.vector.matrix.android.internal.database
import android.content.Context
import im.vector.matrix.android.internal.database.model.SessionRealmModule
import im.vector.matrix.android.internal.di.SessionFilesDirectory
import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.di.UserMd5
import im.vector.matrix.android.internal.session.SessionModule
import io.realm.Realm
import io.realm.RealmConfiguration
import timber.log.Timber
import java.io.File
import javax.inject.Inject
private const val REALM_SHOULD_CLEAR_FLAG_ = "REALM_SHOULD_CLEAR_FLAG_"
private const val REALM_NAME = "disk_store.realm"
/**
* This class is handling creation of RealmConfiguration for a session.
* It will handle corrupted realm by clearing the db file. It allows to just clear cache without losing your crypto keys.
* It's clearly not perfect but there is no way to catch the native crash.
*/
internal class SessionRealmConfigurationFactory @Inject constructor(
private val realmKeysUtils: RealmKeysUtils,
private val databaseKeysUtils: DatabaseKeysUtils,
@SessionFilesDirectory val directory: File,
@SessionId val sessionId: String,
@UserMd5 val userMd5: String,
context: Context) {
private val sharedPreferences = context.getSharedPreferences("im.vector.matrix.android.realm", Context.MODE_PRIVATE)
@UserMd5 val userMd5: String) {
fun create(): RealmConfiguration {
val shouldClearRealm = sharedPreferences.getBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", false)
if (shouldClearRealm) {
Timber.v("************************************************************")
Timber.v("The realm file session was corrupted and couldn't be loaded.")
Timber.v("The file has been deleted to recover.")
Timber.v("************************************************************")
deleteRealmFiles()
}
sharedPreferences
.edit()
.putBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", true)
.apply()
val realmConfiguration = RealmConfiguration.Builder()
.compactOnLaunch()
return RealmConfiguration.Builder()
.directory(directory)
.name(REALM_NAME)
.apply {
realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5))
databaseKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5))
}
.modules(SessionRealmModule())
.deleteRealmIfMigrationNeeded()
.build()
// Try creating a realm instance and if it succeeds we can clear the flag
Realm.getInstance(realmConfiguration).use {
Timber.v("Successfully create realm instance")
sharedPreferences
.edit()
.putBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", false)
.apply()
}
return realmConfiguration
}
// Delete all the realm files of the session
private fun deleteRealmFiles() {
listOf(REALM_NAME, "$REALM_NAME.lock", "$REALM_NAME.note", "$REALM_NAME.management").forEach { file ->
try {
File(directory, file).deleteRecursively()
} catch (e: Exception) {
Timber.e(e, "Unable to delete files")
}
}
}
}

View File

@@ -0,0 +1,46 @@
/*
* 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
import com.squareup.sqldelight.Query
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
internal suspend fun Query<Boolean>.awaitResult(timeoutMillis: Long) = withTimeout(timeoutMillis) {
withContext(Dispatchers.IO) {
val exists = executeAsOne()
if (exists) {
return@withContext
}
val latch = CompletableDeferred<Unit>()
val listener = object : Query.Listener {
override fun queryResultsChanged() {
if (executeAsOne()) {
latch.complete(Unit)
}
}
}
addListener(listener)
try {
latch.await()
} finally {
removeListener(listener)
}
}
}

View File

@@ -0,0 +1,11 @@
package im.vector.matrix.android.internal.database.helper
import im.vector.matrix.sqldelight.session.Breadcrumbs
import im.vector.matrix.sqldelight.session.BreadcrumbsQueries
internal fun BreadcrumbsQueries.saveBreadcrumbs(breadcrumbs: List<String>) {
deleteAll()
breadcrumbs.forEachIndexed { index, roomId ->
insert(Breadcrumbs.Impl(room_id = roomId, breadcrumb_index = index))
}
}

View File

@@ -16,26 +16,15 @@
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.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntityFields
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
import im.vector.matrix.android.internal.database.query.find
import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.database.model.*
import im.vector.matrix.android.internal.extensions.assertIsManaged
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import im.vector.matrix.sqldelight.session.SessionDatabase
import im.vector.matrix.sqldelight.session.TimelineEventQueries
import io.realm.Realm
import io.realm.Sort
import io.realm.kotlin.createObject
import timber.log.Timber
internal fun ChunkEntity.deleteOnCascade() {
@@ -85,57 +74,47 @@ internal fun ChunkEntity.addStateEvent(roomId: String, stateEvent: EventEntity,
}
}
internal fun ChunkEntity.addTimelineEvent(roomId: String,
eventEntity: EventEntity,
internal fun SessionDatabase.addTimelineEvent(roomId: String,
chunkId: Long,
event: Event,
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 senderId = eventEntity.sender ?: ""
// Update RR for the sender of a new message with a dummy one
val readReceiptsSummaryEntity = handleReadReceipts(realm, roomId, eventEntity, senderId)
val timelineEventEntity = realm.createObject<TimelineEventEntity>().apply {
this.localId = localId
this.root = eventEntity
this.eventId = eventId
this.roomId = roomId
this.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
this.readReceipts = readReceiptsSummaryEntity
this.displayIndex = displayIndex
val eventId = event.eventId ?: "$roomId-$chunkId-${System.currentTimeMillis()}"
val displayIndex = timelineEventQueries.nextDisplayIndex(direction, chunkId)
val senderId = event.senderId ?: ""
val roomMemberContent = roomMemberContentsByUser[senderId]
this.senderAvatar = roomMemberContent?.avatarUrl
this.senderName = roomMemberContent?.displayName
isUniqueDisplayName = if (roomMemberContent?.displayName != null) {
computeIsUnique(realm, roomId, isLastForward, roomMemberContent, roomMemberContentsByUser)
// Update RR for the sender of a new message with a dummy one
handleReadReceipts(this, roomId, eventId, event.originServerTs, senderId)
val isDisplayNameUnique = if (roomMemberContent?.displayName != null) {
computeIsUnique(this, roomId, chunkId, roomMemberContent, roomMemberContentsByUser)
} else {
true
}
}
timelineEvents.add(timelineEventEntity)
timelineEventQueries.insert(
event_id = eventId,
sender_avatar = roomMemberContent?.avatarUrl,
chunk_id = chunkId,
room_id = roomId,
display_index = displayIndex,
is_unique_display_name = isDisplayNameUnique,
sender_name = roomMemberContent?.displayName
)
}
private fun computeIsUnique(
realm: Realm,
sessionDatabase: SessionDatabase,
roomId: String,
isLastForward: Boolean,
chunkId: Long,
myRoomMemberContent: RoomMemberContent,
roomMemberContentsByUser: Map<String, RoomMemberContent?>
): Boolean {
val isHistoricalUnique = roomMemberContentsByUser.values.find {
it != myRoomMemberContent && it?.displayName == myRoomMemberContent.displayName
} == null
val isLastForward = sessionDatabase.chunkQueries.isLastForward(chunkId).executeAsOne()
return if (isLastForward) {
val isLiveUnique = RoomMemberSummaryEntity
.where(realm, roomId)
.equalTo(RoomMemberSummaryEntityFields.DISPLAY_NAME, myRoomMemberContent.displayName)
.findAll().none {
!roomMemberContentsByUser.containsKey(it.userId)
}
val countMembersWithName = sessionDatabase.roomMemberSummaryQueries.countMembersWithNameInRoom(myRoomMemberContent.displayName, roomId).executeAsOne()
val isLiveUnique = countMembersWithName == 1L
isHistoricalUnique && isLiveUnique
} else {
isHistoricalUnique
@@ -143,6 +122,7 @@ private fun computeIsUnique(
}
private fun ChunkEntity.addTimelineEventFromMerge(realm: Realm, timelineEventEntity: TimelineEventEntity, direction: PaginationDirection) {
/*
val eventId = timelineEventEntity.eventId
if (timelineEvents.find(eventId) != null) {
return
@@ -162,36 +142,30 @@ private fun ChunkEntity.addTimelineEventFromMerge(realm: Realm, timelineEventEnt
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
private fun handleReadReceipts(
sessionDatabase: SessionDatabase,
roomId: String,
eventId: String,
originServerTs: Long?,
senderId: String) {
if (originServerTs != null) {
val timestampOfEvent = originServerTs.toDouble()
val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId)
val oldTimestamp = sessionDatabase.readReceiptQueries.getTimestampForUser(roomId, senderId).executeAsOneOrNull()
// If the synced RR is older, update
if (timestampOfEvent > readReceiptOfSender.originServerTs) {
val previousReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId = readReceiptOfSender.eventId).findFirst()
readReceiptOfSender.eventId = eventEntity.eventId
readReceiptOfSender.originServerTs = timestampOfEvent
previousReceiptsSummary?.readReceipts?.remove(readReceiptOfSender)
readReceiptsSummaryEntity.readReceipts.add(readReceiptOfSender)
if (oldTimestamp == null || timestampOfEvent > oldTimestamp) {
sessionDatabase.readReceiptQueries.updateReadReceipt(eventId, timestampOfEvent, roomId, senderId)
}
}
return readReceiptsSummaryEntity
}
internal fun ChunkEntity.nextDisplayIndex(direction: PaginationDirection): Int {
internal fun TimelineEventQueries.nextDisplayIndex(direction: PaginationDirection, chunkId: Long): Int {
return when (direction) {
PaginationDirection.FORWARDS -> {
(timelineEvents.where().max(TimelineEventEntityFields.DISPLAY_INDEX)?.toInt() ?: 0) + 1
}
PaginationDirection.BACKWARDS -> {
(timelineEvents.where().min(TimelineEventEntityFields.DISPLAY_INDEX)?.toInt() ?: 0) - 1
}
PaginationDirection.FORWARDS -> (getMaxDisplayIndex(chunkId).executeAsOneOrNull()?.max_display_index?.toInt()
?: 0) + 1
PaginationDirection.BACKWARDS -> (getMinDisplayIndex(chunkId).executeAsOneOrNull()?.min_display_index?.toInt()
?: 0) - 1
}
}

View File

@@ -0,0 +1,18 @@
package im.vector.matrix.android.internal.database.helper
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.sqldelight.session.EventQueries
fun EventQueries.setDecryptionResult(result: MXEventDecryptionResult, eventId: String) {
val decryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
val adapter = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java)
val json = adapter.toJson(decryptionResult)
setDecryptionResult(json, eventId)
}

View File

@@ -0,0 +1,16 @@
package im.vector.matrix.android.internal.database.helper
import com.squareup.sqldelight.ColumnAdapter
private const val SEPARATOR = "__;__"
internal class ListOfStringColumnAdapter : ColumnAdapter<List<String>, String> {
override fun decode(databaseValue: String): List<String> {
return databaseValue.split(SEPARATOR).filter { it.isNotBlank() }
}
override fun encode(value: List<String>): String {
return value.joinToString(SEPARATOR)
}
}

View File

@@ -0,0 +1,18 @@
package im.vector.matrix.android.internal.database.helper
fun List<Any>.toInParams(): String{
return if (isEmpty()) {
"()"
} else {
val list = this
buildString {
append("(")
append("\"${list.first()}\"")
for (value in list.drop(1)) {
append(",")
append("\"$value\"")
}
append(')')
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* 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.LocalEcho
import im.vector.matrix.sqldelight.session.SessionDatabase
internal fun SessionDatabase.isEventRead(userId: String?,
roomId: String?,
eventId: String?): Boolean {
if (userId.isNullOrBlank() || roomId.isNullOrBlank() || eventId.isNullOrBlank()) {
return false
}
if (LocalEcho.isLocalEchoId(eventId)) {
return true
}
val eventToCheck = timelineEventQueries.getForReadQueries(eventId).executeAsOneOrNull()
return if (eventToCheck == null || eventToCheck.sender_id == userId) {
true
} else {
val readReceipt = readReceiptQueries.getEventIdForUser(roomId, userId).executeAsOneOrNull()
?: return false
val readReceiptIndex = timelineEventQueries.getForReadQueries(readReceipt).executeAsOneOrNull()?.display_index
?: Int.MIN_VALUE
val eventToCheckIndex = eventToCheck.display_index
eventToCheckIndex <= readReceiptIndex
}
}
internal fun SessionDatabase.isReadMarkerMoreRecent(roomId: String?,
eventId: String?): Boolean {
if (roomId.isNullOrBlank() || eventId.isNullOrBlank()) {
return false
}
val eventToCheck = timelineEventQueries.getForReadQueries(eventId).executeAsOneOrNull()
val readMarker = readMarkerQueries.get(roomId).executeAsOneOrNull()
?: return false
val readMarkerEvent = timelineEventQueries.getForReadQueries(readMarker).executeAsOneOrNull()
return if (eventToCheck?.chunk_id == readMarkerEvent?.chunk_id) {
val readMarkerIndex = readMarkerEvent?.display_index ?: Int.MIN_VALUE
val eventToCheckIndex = eventToCheck?.display_index ?: Int.MAX_VALUE
eventToCheckIndex <= readMarkerIndex
} else {
val chunkId = eventToCheck?.chunk_id ?: return false
chunkQueries.isLastForward(chunkId).executeAsOneOrNull() ?: false
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.database.mapper
import im.vector.matrix.android.api.session.room.model.Breadcrumb
import javax.inject.Inject
internal class BreadcrumbMapper @Inject constructor() {
fun map(room_id: String,
display_name: String?,
avatar_url: String?,
highlight_count: Int,
is_encrypted: Boolean,
notification_count: Int,
has_unread: Boolean): Breadcrumb {
return Breadcrumb(
roomId = room_id,
avatarUrl = avatar_url ?: "",
displayName = display_name ?: "",
hasUnreadMessages = has_unread,
highlightCount = highlight_count,
isEncrypted = is_encrypted,
notificationCount = notification_count,
typingRoomMemberIds = emptyList(),
userDrafts = emptyList()
)
}
}

View File

@@ -17,29 +17,23 @@
package im.vector.matrix.android.internal.database.mapper
import im.vector.matrix.android.api.session.room.send.UserDraft
import im.vector.matrix.android.internal.database.model.DraftEntity
import im.vector.matrix.sqldelight.session.DraftMode
import javax.inject.Inject
/**
* DraftEntity <-> UserDraft
*/
internal object DraftMapper {
internal class DraftMapper @Inject constructor() {
fun map(entity: DraftEntity): UserDraft {
return when (entity.draftMode) {
DraftEntity.MODE_REGULAR -> UserDraft.REGULAR(entity.content)
DraftEntity.MODE_EDIT -> UserDraft.EDIT(entity.linkedEventId, entity.content)
DraftEntity.MODE_QUOTE -> UserDraft.QUOTE(entity.linkedEventId, entity.content)
DraftEntity.MODE_REPLY -> UserDraft.REPLY(entity.linkedEventId, entity.content)
fun map(content: String,
draft_mode: String,
linked_event_id: String): UserDraft {
return when (draft_mode) {
DraftMode.MODE_REGULAR -> UserDraft.REGULAR(content)
DraftMode.MODE_EDIT -> UserDraft.EDIT(linked_event_id, content)
DraftMode.MODE_QUOTE -> UserDraft.QUOTE(linked_event_id, content)
DraftMode.MODE_REPLY -> UserDraft.REPLY(linked_event_id, content)
else -> null
} ?: UserDraft.REGULAR("")
}
fun map(domain: UserDraft): DraftEntity {
return when (domain) {
is UserDraft.REGULAR -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_REGULAR, linkedEventId = "")
is UserDraft.EDIT -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_EDIT, linkedEventId = domain.linkedEventId)
is UserDraft.QUOTE -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_QUOTE, linkedEventId = domain.linkedEventId)
is UserDraft.REPLY -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_REPLY, linkedEventId = domain.linkedEventId)
}
}
}

View File

@@ -16,94 +16,77 @@
package im.vector.matrix.android.internal.database.mapper
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.session.room.model.ReactionAggregatedSummary
import im.vector.matrix.android.api.session.room.model.ReferencesAggregatedSummary
import im.vector.matrix.android.internal.database.model.EditAggregatedSummaryEntity
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntity
import im.vector.matrix.android.internal.database.model.ReferencesAggregatedSummaryEntity
import io.realm.RealmList
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.*
internal object EventAnnotationsSummaryMapper {
fun map(annotationsSummary: EventAnnotationsSummaryEntity): EventAnnotationsSummary {
fun mapAnnotationsSummary(eventId: String,
reactions: List<ReactionAggregatedSummary>,
edit: EditAggregatedSummary?,
references: ReferencesAggregatedSummary?,
poll: PollResponseAggregatedSummary?): EventAnnotationsSummary {
return EventAnnotationsSummary(
eventId = annotationsSummary.eventId,
reactionsSummary = annotationsSummary.reactionsSummary.toList().map {
ReactionAggregatedSummary(
it.key,
it.count,
it.addedByMe,
it.firstTimestamp,
it.sourceEvents.toList(),
it.sourceLocalEcho.toList()
)
},
editSummary = annotationsSummary.editSummary?.let {
EditAggregatedSummary(
ContentMapper.map(it.aggregatedContent),
it.sourceEvents.toList(),
it.sourceLocalEchoEvents.toList(),
it.lastEditTs
)
},
referencesAggregatedSummary = annotationsSummary.referencesSummaryEntity?.let {
ReferencesAggregatedSummary(
it.eventId,
ContentMapper.map(it.content),
it.sourceEvents.toList(),
it.sourceLocalEcho.toList()
)
},
pollResponseSummary = annotationsSummary.pollResponseSummary?.let {
PollResponseAggregatedSummaryEntityMapper.map(it)
}
eventId = eventId,
reactionsSummary = reactions,
referencesAggregatedSummary = references,
editSummary = edit,
pollResponseSummary = poll
)
}
fun map(annotationsSummary: EventAnnotationsSummary, roomId: String): EventAnnotationsSummaryEntity {
val eventAnnotationsSummaryEntity = EventAnnotationsSummaryEntity()
eventAnnotationsSummaryEntity.eventId = annotationsSummary.eventId
eventAnnotationsSummaryEntity.roomId = roomId
eventAnnotationsSummaryEntity.editSummary = annotationsSummary.editSummary?.let {
EditAggregatedSummaryEntity(
ContentMapper.map(it.aggregatedContent),
RealmList<String>().apply { addAll(it.sourceEvents) },
RealmList<String>().apply { addAll(it.localEchos) },
it.lastEditTs
fun mapReactionSummary(key: String,
count: Long,
added_by_me: Boolean,
first_timestamp: Long,
source_event_ids: List<String>,
source_local_echo_ids: List<String>): ReactionAggregatedSummary {
return ReactionAggregatedSummary(
key = key,
count = count.toInt(),
addedByMe = added_by_me,
firstTimestamp = first_timestamp,
sourceEvents = source_event_ids,
localEchoEvents = source_local_echo_ids
)
}
eventAnnotationsSummaryEntity.reactionsSummary = annotationsSummary.reactionsSummary.let {
RealmList<ReactionAggregatedSummaryEntity>().apply {
addAll(it.map {
ReactionAggregatedSummaryEntity(
it.key,
it.count,
it.addedByMe,
it.firstTimestamp,
RealmList<String>().apply { addAll(it.sourceEvents) },
RealmList<String>().apply { addAll(it.localEchoEvents) }
)
})
}
}
eventAnnotationsSummaryEntity.referencesSummaryEntity = annotationsSummary.referencesAggregatedSummary?.let {
ReferencesAggregatedSummaryEntity(
it.eventId,
ContentMapper.map(it.content),
RealmList<String>().apply { addAll(it.sourceEvents) },
RealmList<String>().apply { addAll(it.localEchos) }
)
}
eventAnnotationsSummaryEntity.pollResponseSummary = annotationsSummary.pollResponseSummary?.let {
PollResponseAggregatedSummaryEntityMapper.map(it)
}
return eventAnnotationsSummaryEntity
}
}
internal fun EventAnnotationsSummaryEntity.asDomain(): EventAnnotationsSummary {
return EventAnnotationsSummaryMapper.map(this)
fun mapEditSummary(aggregated_content: String?,
last_edit_ts: Long,
source_event_ids: List<String>,
source_local_echo_ids: List<String>): EditAggregatedSummary {
return EditAggregatedSummary(
ContentMapper.map(aggregated_content),
source_event_ids,
source_local_echo_ids,
last_edit_ts
)
}
fun mapReferencesSummary(event_id: String,
content: String?,
source_event_ids: List<String>,
source_local_echo_ids: List<String>): ReferencesAggregatedSummary {
return ReferencesAggregatedSummary(
event_id,
ContentMapper.map(content),
source_event_ids,
source_local_echo_ids
)
}
fun mapPollSummary(content: String?,
closed_time: Long?,
nb_options: Int,
source_event_ids: List<String>,
source_local_echo_ids: List<String>): PollResponseAggregatedSummary {
return PollResponseAggregatedSummary(
aggregatedContent = ContentMapper.map(content).toModel(),
closedTime = closed_time,
localEchos = source_local_echo_ids,
sourceEvents = source_event_ids,
nbOptions = nb_options
)
}
}

View File

@@ -33,7 +33,8 @@ internal object EventMapper {
else MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(event.unsignedData)
val eventEntity = EventEntity()
// TODO change this as we shouldn't use event everywhere
eventEntity.eventId = event.eventId ?: "$$roomId-${System.currentTimeMillis()}-${event.hashCode()}"
eventEntity.eventId = event.eventId
?: "$$roomId-${System.currentTimeMillis()}-${event.hashCode()}"
eventEntity.roomId = event.roomId ?: roomId
eventEntity.content = ContentMapper.map(event.content)
val resolvedPrevContent = event.prevContent ?: event.unsignedData?.prevContent
@@ -48,6 +49,34 @@ internal object EventMapper {
return eventEntity
}
fun map(event: Event, roomId: String, sendState: SendState, ageLocalTs: Long?): im.vector.matrix.sqldelight.session.EventEntity {
val uds = if (event.unsignedData == null) {
null
} else {
MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(event.unsignedData)
}
val resolvedPrevContent = event.prevContent ?: event.unsignedData?.prevContent
return im.vector.matrix.sqldelight.session.EventEntity.Impl(
// TODO change this as we shouldn't use event everywhere
event_id = event.eventId
?: "$$roomId-${System.currentTimeMillis()}-${event.hashCode()}",
room_id = event.roomId ?: roomId,
content = ContentMapper.map(event.content),
prev_content = ContentMapper.map(resolvedPrevContent),
state_key = event.stateKey,
type = event.type,
sender_id = event.senderId,
origin_server_ts = event.originServerTs,
redacts = event.redacts,
age = event.unsignedData?.age ?: event.originServerTs,
unsigned_data = uds,
age_local_ts = ageLocalTs,
send_state = sendState.name,
decryption_error_code = null,
decryption_result_json = null
)
}
fun map(eventEntity: EventEntity): Event {
val ud = eventEntity.unsignedData
?.takeIf { it.isNotBlank() }
@@ -87,12 +116,56 @@ internal object EventMapper {
}
}
}
fun map(eventEntity: im.vector.matrix.sqldelight.session.EventEntity): Event {
return Event(
type = eventEntity.type,
eventId = eventEntity.event_id,
content = ContentMapper.map(eventEntity.content),
prevContent = ContentMapper.map(eventEntity.prev_content),
originServerTs = eventEntity.origin_server_ts,
senderId = eventEntity.sender_id,
stateKey = eventEntity.state_key,
roomId = eventEntity.room_id,
unsignedData = UnsignedDataMapper.mapFromString(eventEntity.unsigned_data),
redacts = eventEntity.redacts
).also {
it.ageLocalTs = eventEntity.age_local_ts
it.sendState = SendState.valueOf(eventEntity.send_state)
it.setDecryptionValues(eventEntity.decryption_result_json, eventEntity.decryption_error_code)
}
}
}
internal fun Event.setDecryptionValues(decryptionResultJson: String?, decryptionErrorCode: String?): Event {
return apply {
decryptionResultJson?.let { json ->
try {
mxDecryptionResult = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).fromJson(json)
} catch (t: JsonDataException) {
Timber.e(t, "Failed to parse decryption result")
}
}
// TODO get the full crypto error object
mCryptoError = decryptionErrorCode?.let { errorCode ->
MXCryptoError.ErrorType.valueOf(errorCode)
}
}
}
internal fun EventEntity.asDomain(): Event {
return EventMapper.map(this)
}
internal fun im.vector.matrix.sqldelight.session.EventEntity.asDomain(): Event {
return EventMapper.map(this)
}
internal fun Event.toSQLEntity(roomId: String, sendState: SendState, ageLocalTs: Long? = null): im.vector.matrix.sqldelight.session.EventEntity {
return EventMapper.map(this, roomId, sendState, ageLocalTs)
}
internal fun Event.toEntity(roomId: String, sendState: SendState, ageLocalTs: Long? = null): EventEntity {
return EventMapper.map(this, roomId).apply {
this.sendState = sendState

View File

@@ -16,24 +16,34 @@
package im.vector.matrix.android.internal.database.mapper
import com.squareup.sqldelight.db.SqlCursor
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.internal.database.model.GroupSummaryEntity
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.sqldelight.session.Memberships
import javax.inject.Inject
internal object GroupSummaryMapper {
internal class GroupSummaryMapper @Inject constructor() {
fun map(cursor: SqlCursor): GroupSummary = GroupSummary(
groupId = cursor.getString(0)!!,
membership = Membership.valueOf(cursor.getString(4)!!),
displayName = cursor.getString(1) ?: "",
shortDescription = cursor.getString(2) ?: "",
avatarUrl = cursor.getString(3) ?: ""
)
fun map(group_id: String,
display_name: String?,
short_description: String?,
avatar_url: String?,
membership: Memberships): GroupSummary {
fun map(groupSummaryEntity: GroupSummaryEntity): GroupSummary {
return GroupSummary(
groupSummaryEntity.groupId,
groupSummaryEntity.membership,
groupSummaryEntity.displayName,
groupSummaryEntity.shortDescription,
groupSummaryEntity.avatarUrl,
groupSummaryEntity.roomIds.toList(),
groupSummaryEntity.userIds.toList()
groupId = group_id,
membership = membership.map(),
displayName = display_name ?: "",
shortDescription = short_description ?: "",
avatarUrl = avatar_url ?: ""
)
}
}
internal fun GroupSummaryEntity.asDomain(): GroupSummary {
return GroupSummaryMapper.map(this)
}

View File

@@ -0,0 +1,16 @@
package im.vector.matrix.android.internal.database.mapper
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.sqldelight.session.Memberships
fun Memberships.map(): Membership {
return Membership.valueOf(name)
}
fun Membership.map():Memberships {
return Memberships.valueOf(name)
}
fun List<Membership>.map(): List<Memberships> = this.map {
it.map()
}

View File

@@ -17,11 +17,13 @@
package im.vector.matrix.android.internal.database.mapper
import im.vector.matrix.android.api.pushrules.rest.PushCondition
import im.vector.matrix.android.internal.database.model.PushConditionEntity
import im.vector.matrix.sqldelight.session.GetPushConditions
import im.vector.matrix.sqldelight.session.PushConditionEntity
import javax.inject.Inject
internal object PushConditionMapper {
internal class PushConditionMapper @Inject constructor() {
fun map(entity: PushConditionEntity): PushCondition {
fun map(entity: GetPushConditions): PushCondition {
return PushCondition(
kind = entity.kind,
iz = entity.iz,
@@ -30,12 +32,14 @@ internal object PushConditionMapper {
)
}
fun map(domain: PushCondition): PushConditionEntity {
return PushConditionEntity(
kind = domain.kind,
iz = domain.iz,
key = domain.key,
pattern = domain.pattern
fun map(ruleId: String, pushCondition: PushCondition): PushConditionEntity {
return PushConditionEntity.Impl(
kind = pushCondition.kind,
iz = pushCondition.iz,
key = pushCondition.key,
pattern = pushCondition.pattern,
rule_id = ruleId
)
}
}

View File

@@ -17,14 +17,16 @@ package im.vector.matrix.android.internal.database.mapper
import com.squareup.moshi.Types
import im.vector.matrix.android.api.pushrules.Condition
import im.vector.matrix.android.api.pushrules.RuleKind
import im.vector.matrix.android.api.pushrules.rest.PushCondition
import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.internal.database.model.PushRuleEntity
import im.vector.matrix.android.internal.di.MoshiProvider
import io.realm.RealmList
import im.vector.matrix.sqldelight.session.GetPushConditions
import im.vector.matrix.sqldelight.session.PushRuleEntity
import timber.log.Timber
import javax.inject.Inject
internal object PushRulesMapper {
internal class PushRulesMapper @Inject constructor(private val pushConditionMapper: PushConditionMapper) {
private val moshiActionsAdapter = MoshiProvider.providesMoshi().adapter<List<Any>>(Types.newParameterizedType(List::class.java, Any::class.java))
@@ -33,10 +35,10 @@ internal object PushRulesMapper {
fun mapContentRule(pushrule: PushRuleEntity): PushRule {
return PushRule(
actions = fromActionStr(pushrule.actionsStr),
default = pushrule.default,
enabled = pushrule.enabled,
ruleId = pushrule.ruleId,
actions = fromActionStr(pushrule.action_str),
default = pushrule.is_default,
enabled = pushrule.is_enabled,
ruleId = pushrule.rule_id,
conditions = listOf(
PushCondition(Condition.Kind.EventMatch.value, "content.body", pushrule.pattern)
)
@@ -44,58 +46,60 @@ internal object PushRulesMapper {
}
private fun fromActionStr(actionsStr: String?): List<Any> {
try {
return actionsStr?.let { moshiActionsAdapter.fromJson(it) } ?: emptyList()
return try {
actionsStr?.let { moshiActionsAdapter.fromJson(it) } ?: emptyList()
} catch (e: Throwable) {
Timber.e(e, "## failed to map push rule actions <$actionsStr>")
return emptyList()
emptyList()
}
}
fun mapRoomRule(pushrule: PushRuleEntity): PushRule {
return PushRule(
actions = fromActionStr(pushrule.actionsStr),
default = pushrule.default,
enabled = pushrule.enabled,
ruleId = pushrule.ruleId,
actions = fromActionStr(pushrule.action_str),
default = pushrule.is_default,
enabled = pushrule.is_enabled,
ruleId = pushrule.rule_id,
conditions = listOf(
PushCondition(Condition.Kind.EventMatch.value, "room_id", pushrule.ruleId)
PushCondition(Condition.Kind.EventMatch.value, "room_id", pushrule.rule_id)
)
)
}
fun mapSenderRule(pushrule: PushRuleEntity): PushRule {
return PushRule(
actions = fromActionStr(pushrule.actionsStr),
default = pushrule.default,
enabled = pushrule.enabled,
ruleId = pushrule.ruleId,
actions = fromActionStr(pushrule.action_str),
default = pushrule.is_default,
enabled = pushrule.is_enabled,
ruleId = pushrule.rule_id,
conditions = listOf(
PushCondition(Condition.Kind.EventMatch.value, "user_id", pushrule.ruleId)
PushCondition(Condition.Kind.EventMatch.value, "user_id", pushrule.rule_id)
)
)
}
fun map(pushrule: PushRuleEntity): PushRule {
fun map(pushrule: PushRuleEntity, conditions: List<GetPushConditions>): PushRule {
return PushRule(
actions = fromActionStr(pushrule.actionsStr),
default = pushrule.default,
enabled = pushrule.enabled,
ruleId = pushrule.ruleId,
conditions = pushrule.conditions?.map { PushConditionMapper.map(it) }
actions = fromActionStr(pushrule.action_str),
default = pushrule.is_default,
enabled = pushrule.is_enabled,
ruleId = pushrule.rule_id,
conditions = conditions.map {
pushConditionMapper.map(it)
}
)
}
fun map(pushRule: PushRule): PushRuleEntity {
return PushRuleEntity(
actionsStr = moshiActionsAdapter.toJson(pushRule.actions),
default = pushRule.default ?: false,
enabled = pushRule.enabled,
ruleId = pushRule.ruleId,
fun map(scope: String, kind: RuleKind, pushRule: PushRule): PushRuleEntity {
return PushRuleEntity.Impl(
action_str = moshiActionsAdapter.toJson(pushRule.actions),
is_default = pushRule.default ?: false,
is_enabled = pushRule.enabled,
rule_id = pushRule.ruleId,
pattern = pushRule.pattern,
conditions = pushRule.conditions?.let {
RealmList(*pushRule.conditions.map { PushConditionMapper.map(it) }.toTypedArray())
} ?: RealmList()
scope = scope,
kind = kind.name
)
}
}

View File

@@ -17,44 +17,48 @@ package im.vector.matrix.android.internal.database.mapper
import im.vector.matrix.android.api.session.pushers.Pusher
import im.vector.matrix.android.api.session.pushers.PusherData
import im.vector.matrix.android.internal.database.model.PusherDataEntity
import im.vector.matrix.android.internal.database.model.PusherEntity
import im.vector.matrix.android.api.session.pushers.PusherState
import im.vector.matrix.android.internal.session.pushers.JsonPusher
import im.vector.matrix.sqldelight.session.PusherEntity
import javax.inject.Inject
internal object PushersMapper {
internal class PushersMapper @Inject constructor() {
fun map(pushEntity: PusherEntity): Pusher {
fun map(push_key: String,
kind: String?,
app_id: String,
app_display_name: String?,
device_display_name: String?,
profile_tag: String?,
lang: String?,
data_url: String?,
data_format: String?,
state: String): Pusher {
return Pusher(
pushKey = pushEntity.pushKey,
kind = pushEntity.kind ?: "",
appId = pushEntity.appId,
appDisplayName = pushEntity.appDisplayName,
deviceDisplayName = pushEntity.deviceDisplayName,
profileTag = pushEntity.profileTag,
lang = pushEntity.lang,
data = PusherData(pushEntity.data?.url, pushEntity.data?.format),
state = pushEntity.state
pushKey = push_key,
kind = kind ?: "",
appId = app_id,
appDisplayName = app_display_name,
deviceDisplayName = device_display_name,
profileTag = profile_tag,
lang = lang,
data = PusherData(data_url, data_format),
state = PusherState.valueOf(state)
)
}
fun map(pusher: JsonPusher): PusherEntity {
return PusherEntity(
pushKey = pusher.pushKey,
fun map(pusher: JsonPusher, state: PusherState): PusherEntity {
return PusherEntity.Impl(
push_key = pusher.pushKey,
kind = pusher.kind,
appId = pusher.appId,
appDisplayName = pusher.appDisplayName,
deviceDisplayName = pusher.deviceDisplayName,
profileTag = pusher.profileTag,
app_id = pusher.appId,
app_display_name = pusher.appDisplayName,
device_display_name = pusher.deviceDisplayName,
profile_tag = pusher.profileTag,
lang = pusher.lang,
data = PusherDataEntity(pusher.data?.url, pusher.data?.format)
data_url = pusher.data?.url,
data_format = pusher.data?.format,
state = state.name
)
}
}
internal fun PusherEntity.asDomain(): Pusher {
return PushersMapper.map(this)
}
internal fun JsonPusher.toEntity(): PusherEntity {
return PushersMapper.map(this)
}

View File

@@ -0,0 +1,19 @@
package im.vector.matrix.android.internal.database.mapper
import im.vector.matrix.android.api.session.room.model.ReadReceipt
import im.vector.matrix.android.api.session.user.model.User
import javax.inject.Inject
class ReadReceiptMapper @Inject constructor() {
fun map(user_id: String,
display_name: String?,
avatar_url: String?,
origin_server_ts: Double
): ReadReceipt {
val user = User(user_id, display_name, avatar_url)
return ReadReceipt(user, origin_server_ts.toLong())
}
}

View File

@@ -18,18 +18,16 @@ package im.vector.matrix.android.internal.database.mapper
import im.vector.matrix.android.api.session.room.model.ReadReceipt
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity
import im.vector.matrix.android.internal.database.model.UserEntity
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.SessionDatabase
import io.realm.Realm
import im.vector.matrix.android.internal.di.RealmSessionDatabase
import io.realm.RealmConfiguration
import javax.inject.Inject
internal class ReadReceiptsSummaryMapper @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration) {
internal class ReadReceiptsSummaryMapper @Inject constructor(@RealmSessionDatabase private val realmConfiguration: RealmConfiguration) {
fun map(readReceiptsSummaryEntity: ReadReceiptsSummaryEntity?): List<ReadReceipt> {
if (readReceiptsSummaryEntity == null) {
return emptyList()
/*if (readReceiptsSummaryEntity == null) {
}
return Realm.getInstance(realmConfiguration).use { realm ->
val readReceipts = readReceiptsSummaryEntity.readReceipts
@@ -39,6 +37,6 @@ internal class ReadReceiptsSummaryMapper @Inject constructor(@SessionDatabase pr
?: return@mapNotNull null
ReadReceipt(user.asDomain(), it.originServerTs.toLong())
}
}
}*/
}
}

View File

@@ -16,10 +16,13 @@
package im.vector.matrix.android.internal.database.mapper
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntity
import im.vector.matrix.sqldelight.session.Memberships
import javax.inject.Inject
internal object RoomMemberSummaryMapper {
internal class RoomMemberSummaryMapper @Inject constructor() {
fun map(roomMemberSummaryEntity: RoomMemberSummaryEntity): RoomMemberSummary {
return RoomMemberSummary(
@@ -29,8 +32,31 @@ internal object RoomMemberSummaryMapper {
membership = roomMemberSummaryEntity.membership
)
}
fun map(roomMemberSummaryEntity: im.vector.matrix.sqldelight.session.RoomMemberSummaryEntity): RoomMemberSummary {
return RoomMemberSummary(
userId = roomMemberSummaryEntity.user_id,
avatarUrl = roomMemberSummaryEntity.avatar_url,
displayName = roomMemberSummaryEntity.display_name,
membership = Membership.valueOf(roomMemberSummaryEntity.membership.name)
)
}
fun map(room_id: String, user_id: String, display_name: String?, avatar_url: String?, membership: Memberships): RoomMemberSummary {
return RoomMemberSummary(
userId = user_id,
membership = membership.map(),
displayName = display_name,
avatarUrl = avatar_url
)
}
}
internal fun im.vector.matrix.sqldelight.session.RoomMemberSummaryEntity.asDomain(): RoomMemberSummary {
return RoomMemberSummaryMapper().map(this)
}
internal fun RoomMemberSummaryEntity.asDomain(): RoomMemberSummary {
return RoomMemberSummaryMapper.map(this)
return RoomMemberSummaryMapper().map(this)
}

View File

@@ -16,47 +16,82 @@
package im.vector.matrix.android.internal.database.mapper
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
import im.vector.matrix.android.api.session.events.model.Event
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.tag.RoomTag
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.sqldelight.session.RoomSummaryWithTimeline
import javax.inject.Inject
internal class RoomSummaryMapper @Inject constructor(private val timelineEventMapper: TimelineEventMapper) {
fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary {
val tags = roomSummaryEntity.tags.map {
RoomTag(it.tagName, it.tagOrder)
}
val latestEvent = roomSummaryEntity.latestPreviewableEvent?.let {
timelineEventMapper.map(it, buildReadReceipts = false)
}
internal class RoomSummaryMapper @Inject constructor() {
fun map(roomSummaryWithTimeline: RoomSummaryWithTimeline, tags: List<RoomTag>): RoomSummary {
return RoomSummary(
roomId = roomSummaryEntity.roomId,
displayName = roomSummaryEntity.displayName ?: "",
topic = roomSummaryEntity.topic ?: "",
avatarUrl = roomSummaryEntity.avatarUrl ?: "",
isDirect = roomSummaryEntity.isDirect,
latestPreviewableEvent = latestEvent,
joinedMembersCount = roomSummaryEntity.joinedMembersCount,
invitedMembersCount = roomSummaryEntity.invitedMembersCount,
otherMemberIds = roomSummaryEntity.otherMemberIds.toList(),
highlightCount = roomSummaryEntity.highlightCount,
notificationCount = roomSummaryEntity.notificationCount,
hasUnreadMessages = roomSummaryEntity.hasUnreadMessages,
roomId = roomSummaryWithTimeline.summary_room_id,
displayName = roomSummaryWithTimeline.display_name ?: "",
topic = roomSummaryWithTimeline.topic ?: "",
avatarUrl = roomSummaryWithTimeline.avatar_url ?: "",
isDirect = roomSummaryWithTimeline.is_direct,
latestPreviewableEvent = createTimelineEvent(roomSummaryWithTimeline),
joinedMembersCount = roomSummaryWithTimeline.joined_members_count,
invitedMembersCount = roomSummaryWithTimeline.invited_members_count,
otherMemberIds = emptyList(),
highlightCount = roomSummaryWithTimeline.highlight_count,
notificationCount = roomSummaryWithTimeline.notification_count,
hasUnreadMessages = roomSummaryWithTimeline.has_unread,
versioningState = VersioningState.valueOf(roomSummaryWithTimeline.versioning_state),
tags = tags,
membership = roomSummaryEntity.membership,
versioningState = roomSummaryEntity.versioningState,
readMarkerId = roomSummaryEntity.readMarkerId,
userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList(),
canonicalAlias = roomSummaryEntity.canonicalAlias,
aliases = roomSummaryEntity.aliases.toList(),
isEncrypted = roomSummaryEntity.isEncrypted,
typingRoomMemberIds = roomSummaryEntity.typingUserIds.toList(),
breadcrumbsIndex = roomSummaryEntity.breadcrumbsIndex,
roomEncryptionTrustLevel = roomSummaryEntity.roomEncryptionTrustLevel,
inviterId = roomSummaryEntity.inviterId
membership = roomSummaryWithTimeline.membership.map(),
readMarkerId = roomSummaryWithTimeline.read_marker_id,
userDrafts = emptyList(),
canonicalAlias = roomSummaryWithTimeline.canonical_alias,
inviterId = roomSummaryWithTimeline.inviter_id,
isEncrypted = roomSummaryWithTimeline.is_encrypted,
typingRoomMemberIds = emptyList(),//roomSummaryEntity.typingUserIds.toList(),
breadcrumbsIndex = roomSummaryWithTimeline.breadcrumb_index ?: -1,
roomEncryptionTrustLevel = roomSummaryWithTimeline.room_encryption_trust_level?.let {
try {
RoomEncryptionTrustLevel.valueOf(it)
} catch (failure: Throwable) {
null
}
}
)
}
private fun createTimelineEvent(roomSummaryWithTimeline: RoomSummaryWithTimeline): TimelineEvent? {
val type = roomSummaryWithTimeline.type ?: return null
val roomId = roomSummaryWithTimeline.summary_room_id
val eventId = roomSummaryWithTimeline.event_id ?: return null
val displayIndex = roomSummaryWithTimeline.display_index ?: return null
val localId = roomSummaryWithTimeline.local_id ?: return null
val event = Event(
type = type,
roomId = roomId,
eventId = eventId,
content = ContentMapper.map(roomSummaryWithTimeline.content),
prevContent = ContentMapper.map(roomSummaryWithTimeline.prev_content),
originServerTs = roomSummaryWithTimeline.origin_server_ts,
senderId = roomSummaryWithTimeline.sender_id,
redacts = roomSummaryWithTimeline.redacts,
stateKey = roomSummaryWithTimeline.state_key,
unsignedData = null
).setDecryptionValues(roomSummaryWithTimeline.decryption_result_json, roomSummaryWithTimeline.decryption_error_code)
return TimelineEvent(
root = event,
eventId = eventId,
annotations = null,
displayIndex = displayIndex,
isUniqueDisplayName = roomSummaryWithTimeline.is_unique_display_name ?: false,
localId = localId,
readReceipts = emptyList(),
senderAvatar = roomSummaryWithTimeline.sender_avatar,
senderName = roomSummaryWithTimeline.sender_name
)
}
}

View File

@@ -16,40 +16,87 @@
package im.vector.matrix.android.internal.database.mapper
import com.squareup.sqldelight.db.SqlCursor
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.ReadReceipt
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import javax.inject.Inject
internal class TimelineEventMapper @Inject constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper) {
internal class TimelineEventMapper @Inject constructor() {
fun map(timelineEventEntity: TimelineEventEntity, buildReadReceipts: Boolean = true, correctedReadReceipts: List<ReadReceipt>? = null): TimelineEvent {
val readReceipts = if (buildReadReceipts) {
correctedReadReceipts ?: timelineEventEntity.readReceipts
?.let {
readReceiptsSummaryMapper.map(it)
}
} else {
null
fun map(cursor: SqlCursor): TimelineEvent = map(
cursor.getLong(0)!!,
cursor.getLong(1)!!,
cursor.getLong(2)!!.toInt(),
cursor.getString(3),
cursor.getString(4),
cursor.getLong(5)!! == 1L,
cursor.getString(6)!!,
cursor.getString(7)!!,
cursor.getString(8),
cursor.getString(9),
cursor.getString(10),
cursor.getString(11)!!,
cursor.getString(12)!!,
cursor.getLong(13),
cursor.getString(14),
cursor.getString(15),
cursor.getString(16),
cursor.getLong(17),
cursor.getLong(18),
cursor.getString(19),
cursor.getString(20)
)
fun map(local_id: Long,
chunk_id: Long,
display_index: Int,
sender_name: String?,
sender_avatar: String?,
is_unique_display_name: Boolean,
event_id: String,
room_id: String,
content: String?,
prev_content: String?,
state_key: String?,
send_state: String,
type: String,
origin_server_ts: Long?,
sender_id: String?,
unsigned_data: String?,
redacts: String?,
age: Long?,
age_local_ts: Long?,
decryption_result_json: String?,
decryption_error_code: String?): TimelineEvent {
val event = Event(
type = type,
roomId = room_id,
eventId = event_id,
content = ContentMapper.map(content),
prevContent = ContentMapper.map(prev_content),
originServerTs = origin_server_ts,
senderId = sender_id,
redacts = redacts,
stateKey = state_key,
unsignedData = UnsignedDataMapper.mapFromString(unsigned_data)
).also {
it.ageLocalTs = age_local_ts
it.sendState = SendState.valueOf(send_state)
it.setDecryptionValues(decryption_result_json, decryption_error_code)
}
return TimelineEvent(
root = timelineEventEntity.root?.asDomain()
?: Event("", timelineEventEntity.eventId),
eventId = timelineEventEntity.eventId,
annotations = timelineEventEntity.annotations?.asDomain(),
localId = timelineEventEntity.localId,
displayIndex = timelineEventEntity.displayIndex,
senderName = timelineEventEntity.senderName,
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
senderAvatar = timelineEventEntity.senderAvatar,
readReceipts = readReceipts
?.distinctBy {
it.user
}?.sortedByDescending {
it.originServerTs
} ?: emptyList()
root = event,
eventId = event_id,
annotations = null,
displayIndex = display_index,
isUniqueDisplayName = is_unique_display_name,
localId = local_id,
readReceipts = emptyList(),
senderAvatar = sender_avatar,
senderName = sender_name
)
}
}

View File

@@ -0,0 +1,21 @@
package im.vector.matrix.android.internal.database.mapper
import com.squareup.moshi.JsonDataException
import im.vector.matrix.android.api.session.events.model.UnsignedData
import im.vector.matrix.android.internal.di.MoshiProvider
import timber.log.Timber
internal object UnsignedDataMapper {
fun mapFromString(us: String?): UnsignedData? {
return us?.takeIf { it.isNotBlank() }
?.let {
try {
MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).fromJson(it)
} catch (t: JsonDataException) {
Timber.e(t, "Failed to parse UnsignedData")
null
}
}
}
}

View File

@@ -17,19 +17,30 @@
package im.vector.matrix.android.internal.database.mapper
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.internal.database.model.UserEntity
import im.vector.matrix.sqldelight.session.UserEntity
import javax.inject.Inject
internal object UserMapper {
internal class UserMapper @Inject constructor() {
fun map(userEntity: UserEntity): User {
return User(
userEntity.userId,
userEntity.displayName,
userEntity.avatarUrl
userEntity.user_id,
userEntity.display_name,
userEntity.avatar_url
)
}
fun map(user_id: String,
display_name: String?,
avatar_url: String?): User {
return User(
userId = user_id,
displayName = display_name,
avatarUrl = avatar_url
)
}
}
internal fun UserEntity.asDomain(): User {
return UserMapper.map(this)
return UserMapper().map(this)
}

View File

@@ -18,6 +18,6 @@ package im.vector.matrix.android.internal.database.query
internal object FilterContent {
internal const val EDIT_TYPE = """{*"m.relates_to"*"rel_type":*"m.replace"*}"""
internal const val RESPONSE_TYPE = """{*"m.relates_to"*"rel_type":*"m.response"*}"""
internal const val EDIT_TYPE = """ '{%"m.relates_to"%"rel_type":%"m.replace"%}' """
internal const val RESPONSE_TYPE = """ '{%"m.relates_to"%"rel_type":%"m.response"%}' """
}

View File

@@ -0,0 +1,43 @@
package im.vector.matrix.android.internal.database.repository
import com.squareup.sqldelight.runtime.coroutines.asFlow
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.extensions.mapToOneOptionnal
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.sqldelight.session.EventEntity
import im.vector.matrix.sqldelight.session.SessionDatabase
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
internal class CurrentStateEventDataSource @Inject constructor(private val sessionDatabase: SessionDatabase,
private val coroutineDispatchers: MatrixCoroutineDispatchers ) {
fun getCurrentMapped(roomId: String, type: String, stateKey: String): Event? {
val entity = sessionDatabase.stateEventQueries.getCurrentStateEvent(roomId, type = type, stateKey = stateKey).executeAsOneOrNull()
return entity?.asDomain()
}
fun getCurrentLiveMapped(roomId: String, type: String, stateKey: String): Flow<Optional<Event>> {
return sessionDatabase.stateEventQueries
.getCurrentStateEvent(roomId, type = type, stateKey = stateKey)
.asFlow()
.map {
it.executeAsOneOrNull()?.asDomain().toOptional()
}
}
fun getCurrentLive(roomId: String, type: String, stateKey: String): Flow<Optional<EventEntity>> {
return sessionDatabase.stateEventQueries
.getCurrentStateEvent(roomId, type = type, stateKey = stateKey)
.asFlow()
.mapToOneOptionnal(coroutineDispatchers.dbQuery)
}
fun getCurrent(roomId: String, type: String, stateKey: String): EventEntity? {
return sessionDatabase.stateEventQueries.getCurrentStateEvent(roomId, type = type, stateKey = stateKey).executeAsOneOrNull()
}
}

View File

@@ -20,12 +20,12 @@ import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class AuthDatabase
annotation class RealmAuthDatabase
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class SessionDatabase
annotation class RealmSessionDatabase
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class CryptoDatabase
annotation class RealmCryptoDatabase

View File

@@ -20,6 +20,7 @@ import android.content.Context
import android.content.res.Resources
import dagger.Module
import dagger.Provides
import im.vector.matrix.android.internal.concurrency.newNamedSingleThreadExecutor
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.createBackgroundHandler
import kotlinx.coroutines.Dispatchers
@@ -36,11 +37,14 @@ internal object MatrixModule {
@Provides
@MatrixScope
fun providesMatrixCoroutineDispatchers(): MatrixCoroutineDispatchers {
return MatrixCoroutineDispatchers(io = Dispatchers.IO,
return MatrixCoroutineDispatchers(
dbTransaction = newNamedSingleThreadExecutor("db_transaction").asCoroutineDispatcher(),
dbQuery = newNamedSingleThreadExecutor("db_query").asCoroutineDispatcher(),
io = Dispatchers.IO,
computation = Dispatchers.Default,
main = Dispatchers.Main,
crypto = createBackgroundHandler("Crypto_Thread").asCoroutineDispatcher(),
dmVerif = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
dmVerif = newNamedSingleThreadExecutor("dm_verif").asCoroutineDispatcher()
)
}

View File

@@ -0,0 +1,17 @@
package im.vector.matrix.android.internal.extensions
import com.squareup.sqldelight.Query
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext
fun <T : Any> Flow<Query<T>>.mapToOneOptionnal(context: CoroutineContext): Flow<Optional<T>> {
return map {
withContext(context) {
it.executeAsOneOrNull().toOptional()
}
}
}

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