mirror of
https://github.com/vector-im/riotX-android
synced 2025-10-06 08:12:46 +02:00
Compare commits
295 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
ec0974f72c | ||
|
1e963bc0dc | ||
|
cc832633a5 | ||
|
eadea9016b | ||
|
6422d946c9 | ||
|
5cc3dc00e3 | ||
|
5a2a9f908a | ||
|
c1f2e9f171 | ||
|
620ba279d8 | ||
|
3fcfa33364 | ||
|
546da0f173 | ||
|
001711d5a3 | ||
|
8e1a964679 | ||
|
b25a130db1 | ||
|
8a9e6497e8 | ||
|
47e3797b7e | ||
|
5cbc90e06a | ||
|
e04bf31faa | ||
|
d25cf79b07 | ||
|
faa8e6bbb2 | ||
|
d3d4deb884 | ||
|
f6b8e0c479 | ||
|
2a726f54a2 | ||
|
1197d4021d | ||
|
03f8120b7d | ||
|
acd7a709de | ||
|
5651ea515b | ||
|
9794b3a49d | ||
|
b3e1c3969d | ||
|
f24bed17a2 | ||
|
a993a30203 | ||
|
91cc78d2ad | ||
|
562acc9702 | ||
|
dfab88ed95 | ||
|
36866dd24e | ||
|
c728834273 | ||
|
f5020d0f63 | ||
|
7da9cafcc2 | ||
|
6f09eea248 | ||
|
468bd5bcc9 | ||
|
3169093c50 | ||
|
d60d766354 | ||
|
0ffb5e627e | ||
|
b4a13f9504 | ||
|
ffa8b7e73a | ||
|
528958b3de | ||
|
3ffe2f7d40 | ||
|
bf42b73713 | ||
|
ed93f4a6c1 | ||
|
b3d649a4d9 | ||
|
3739e50d46 | ||
|
9bf484cf1e | ||
|
6c2faff1f0 | ||
|
07fca0922b | ||
|
282de21708 | ||
|
ba9d119892 | ||
|
4453f0ced9 | ||
|
77168bfd6a | ||
|
25e9a179d2 | ||
|
73ec0f5a83 | ||
|
993fa74252 | ||
|
38fc4984fe | ||
|
695d8cce00 | ||
|
07e99901e1 | ||
|
20f53e9a58 | ||
|
ced72aff4f | ||
|
fdaaca49c2 | ||
|
3485f023b0 | ||
|
384dd100e9 | ||
|
1ba8a58219 | ||
|
c8010561fc | ||
|
1f127335bc | ||
|
138a210a73 | ||
|
ca6bcde82d | ||
|
6bda437f5d | ||
|
3e6b65e174 | ||
|
137dcab734 | ||
|
b22b8fba02 | ||
|
3ccdf4a244 | ||
|
5fbd271b1c | ||
|
db8ea0f5e8 | ||
|
a47a3ead1f | ||
|
05b2092ffc | ||
|
6249a59203 | ||
|
618e9a4f52 | ||
|
f2c8d4ad02 | ||
|
be524472ec | ||
|
1b82a1a24d | ||
|
cf0b331c3b | ||
|
2a92a3dc80 | ||
|
012840abba | ||
|
a5975a099e | ||
|
38da4b9ee5 | ||
|
242e60fcaa | ||
|
a23be05cbf | ||
|
ed39b02924 | ||
|
fe931b5361 | ||
|
90d9cd0587 | ||
|
9cedb18921 | ||
|
e89ba7b87b | ||
|
902657c22a | ||
|
eec2abf164 | ||
|
6879cc8ca8 | ||
|
fd6bbbd3b5 | ||
|
0ff0b014a9 | ||
|
a89f0ddd1d | ||
|
fdc9e84dd5 | ||
|
58f878fca9 | ||
|
88095e4bd9 | ||
|
47d22a3d5e | ||
|
28e82cb8ea | ||
|
35817245cb | ||
|
75266f42bb | ||
|
95c4c9ce56 | ||
|
ce5570105d | ||
|
188a9aebfa | ||
|
c95223f5d2 | ||
|
ef0362ba9c | ||
|
ea242f6737 | ||
|
cbc08d834b | ||
|
0ab6b33fb6 | ||
|
1b394527b6 | ||
|
a8f1388721 | ||
|
166be4e289 | ||
|
b49ccefe63 | ||
|
825760d17e | ||
|
b5af62c3ea | ||
|
a51d96bf00 | ||
|
7e142d201d | ||
|
2be6058971 | ||
|
49d73f360e | ||
|
bd88d85a21 | ||
|
be4fc5cce6 | ||
|
704da1be55 | ||
|
5d002532d3 | ||
|
d4161e9a1a | ||
|
7966ebef03 | ||
|
ed5faca5d2 | ||
|
8ca829d538 | ||
|
e7819ce678 | ||
|
5402902bc2 | ||
|
bc1350aaf5 | ||
|
fd74e3dfb1 | ||
|
e0628da1cb | ||
|
aa4e74e986 | ||
|
6cc0c0672e | ||
|
501474b720 | ||
|
e11c66035c | ||
|
3d2d219d79 | ||
|
63af03bedd | ||
|
d3827b8673 | ||
|
4ca2531e47 | ||
|
4e8dc72439 | ||
|
25a4240a5a | ||
|
b9cfda23b6 | ||
|
06dcf75a32 | ||
|
21deb2551d | ||
|
70639f180c | ||
|
1dbb02a80d | ||
|
825463d9cd | ||
|
c313ce78cb | ||
|
39f58d048b | ||
|
3f792c7a84 | ||
|
347dcb469a | ||
|
9cd69d1e33 | ||
|
79fb1985aa | ||
|
e216cd15a8 | ||
|
37fde374b3 | ||
|
f7b471f141 | ||
|
93fd56a7ca | ||
|
5a9d88e791 | ||
|
eaf6a9923a | ||
|
d98567045c | ||
|
b4ce8748cb | ||
|
9d5433a857 | ||
|
6d4ee83e65 | ||
|
6e44cca17d | ||
|
0a73887c70 | ||
|
7fef063e15 | ||
|
24f391dac0 | ||
|
81c7f694d6 | ||
|
80e2fc0ca3 | ||
|
9f53406e99 | ||
|
3584658c36 | ||
|
12a0cbb400 | ||
|
20437446b4 | ||
|
35229882e3 | ||
|
a04f4421f6 | ||
|
af1e81f65e | ||
|
63f6081fa5 | ||
|
ee2e575211 | ||
|
0949d29f9c | ||
|
23466fb5a4 | ||
|
7f09e64d63 | ||
|
585f0ba4b7 | ||
|
245fbe86d9 | ||
|
456908c851 | ||
|
b79fdf6a85 | ||
|
d9f448c9aa | ||
|
7a6fc4936b | ||
|
d82fd10f3b | ||
|
4009f2c176 | ||
|
15c4b03340 | ||
|
7b5dff3dcf | ||
|
357123743f | ||
|
bb04af1e2c | ||
|
2f94fbd7eb | ||
|
f2a3bdb68e | ||
|
097e9714ff | ||
|
acae0fad3e | ||
|
4deb7eb865 | ||
|
f910cd6f97 | ||
|
652ac81fa1 | ||
|
99f4196388 | ||
|
c0b94f4111 | ||
|
1462fa0484 | ||
|
dafdc1d3ad | ||
|
394b89e76b | ||
|
0db8e7da43 | ||
|
dae8b5c196 | ||
|
215324a03e | ||
|
d3ce4c491c | ||
|
ed6d28bd3b | ||
|
c2e053b62b | ||
|
c450849cc3 | ||
|
fe884dba2d | ||
|
3fa4dbaa25 | ||
|
4a74f58516 | ||
|
c413321a22 | ||
|
d696bd2830 | ||
|
a2b6bd0f62 | ||
|
9cc922a8a2 | ||
|
c36d1bcd06 | ||
|
85499c6b33 | ||
|
8076eab4b5 | ||
|
d47c0f5ebc | ||
|
fd09a1224e | ||
|
c300c50093 | ||
|
77c4355aed | ||
|
1a92562182 | ||
|
9c390dcc0c | ||
|
95089b91b8 | ||
|
eb446d7b49 | ||
|
dc4786ecf0 | ||
|
e245023add | ||
|
90fad23493 | ||
|
000db4b192 | ||
|
2a16c36a59 | ||
|
ef6c1cfc63 | ||
|
4b4156996d | ||
|
087cc0e6e3 | ||
|
f4df27c2dc | ||
|
ab25980c4e | ||
|
77b402ce70 | ||
|
2763fbb496 | ||
|
6deba31111 | ||
|
ff6ce8a4b7 | ||
|
d0cff219aa | ||
|
65f0af918f | ||
|
ac38a6461c | ||
|
9a1e16a170 | ||
|
9e5c70dda3 | ||
|
0255696c88 | ||
|
76a9625f25 | ||
|
5af6bf3762 | ||
|
507bc2f622 | ||
|
125eacb20b | ||
|
6176520805 | ||
|
3aea0a50ca | ||
|
ab87a3caea | ||
|
c58328f94e | ||
|
03974c8bdf | ||
|
151ae7f4dd | ||
|
a34b053efe | ||
|
02e342849f | ||
|
b59017938b | ||
|
2c81e41288 | ||
|
cb44ab547c | ||
|
6d01a570fd | ||
|
4a2bf0d6c6 | ||
|
36af8a6a9f | ||
|
40a68c3e9f | ||
|
1a4ec34bb2 | ||
|
10490e3aa6 | ||
|
cd6624a8a6 | ||
|
3965218bf9 | ||
|
d78ff7ab08 | ||
|
cb274d6a33 | ||
|
c00dbce536 | ||
|
db88caf7fa | ||
|
c3d945d6bb | ||
|
4c128602b2 | ||
|
001603cf9a | ||
|
4341b0d0f5 | ||
|
838003b68a |
@@ -1,6 +1,7 @@
|
||||
# Use Docker file from https://hub.docker.com/r/runmymind/docker-android-sdk
|
||||
# Last docker plugin version can be found here:
|
||||
# https://github.com/buildkite-plugins/docker-buildkite-plugin/releases
|
||||
# We propagate the environment to the container (sse https://github.com/buildkite-plugins/docker-buildkite-plugin#propagate-environment-optional-boolean)
|
||||
|
||||
# Build debug version of the RiotX application, from the develop branch and the features branches
|
||||
|
||||
@@ -18,6 +19,7 @@ steps:
|
||||
plugins:
|
||||
- docker#v3.1.0:
|
||||
image: "runmymind/docker-android-sdk"
|
||||
propagate-environment: true
|
||||
|
||||
- label: "Assemble FDroid Debug version"
|
||||
agents:
|
||||
@@ -32,6 +34,7 @@ steps:
|
||||
plugins:
|
||||
- docker#v3.1.0:
|
||||
image: "runmymind/docker-android-sdk"
|
||||
propagate-environment: true
|
||||
|
||||
- label: "Build Google Play unsigned APK"
|
||||
agents:
|
||||
@@ -46,6 +49,7 @@ steps:
|
||||
plugins:
|
||||
- docker#v3.1.0:
|
||||
image: "runmymind/docker-android-sdk"
|
||||
propagate-environment: true
|
||||
|
||||
# Code quality
|
||||
|
||||
|
104
CHANGES.md
104
CHANGES.md
@@ -1,3 +1,105 @@
|
||||
Changes in RiotX 0.6.1 (2019-09-24)
|
||||
===================================================
|
||||
|
||||
Bugfix:
|
||||
- Fix crash: MergedHeaderItem was missing dimensionConverter
|
||||
|
||||
Changes in RiotX 0.6.0 (2019-09-24)
|
||||
===================================================
|
||||
|
||||
Features:
|
||||
- Save draft of a message when exiting a room with non empty composer (#329)
|
||||
|
||||
Improvements:
|
||||
- Add unread indent on room list (#485)
|
||||
- Message Editing: Update notifications (#128)
|
||||
- Remove any notification of a redacted event (#563)
|
||||
|
||||
Other changes:
|
||||
- Fix a few accessibility issues
|
||||
|
||||
Bugfix:
|
||||
- Fix characters erased from the Search field when the result are coming (#545)
|
||||
- "No connection" banner was displayed by mistake
|
||||
- Leaving community (from another client) has no effect on RiotX (#497)
|
||||
- Push rules was not retrieved after a clear cache
|
||||
- m.notice messages trigger push notifications (#238)
|
||||
- Embiggen messages with multiple emojis also for edited messages (#458)
|
||||
|
||||
Build:
|
||||
- Fix (again) issue with bad versionCode generated by Buildkite (#553)
|
||||
|
||||
Changes in RiotX 0.5.0 (2019-09-17)
|
||||
===================================================
|
||||
|
||||
Features:
|
||||
- Implementation of login to homeserver with SSO (#557)
|
||||
- Handle M_CONSENT_NOT_GIVEN error (#64)
|
||||
- Auto configure homeserver and identity server URLs of LoginActivity with a magic link
|
||||
|
||||
Improvements:
|
||||
- Reduce default release build log level, and lab option to enable more logs.
|
||||
- Display a no network indicator when there is no network (#559)
|
||||
|
||||
Bugfix:
|
||||
- Fix crash due to missing informationData (#535)
|
||||
- Progress in initial sync dialog is decreasing for a step and should not (#532)
|
||||
- Fix rendering issue of accepted third party invitation event
|
||||
- All current notifications were dismissed by mistake when the app is launched from the launcher
|
||||
|
||||
Build:
|
||||
- Fix issue with version name (#533)
|
||||
- Fix issue with bad versionCode generated by Buildkite (#553)
|
||||
|
||||
Changes in RiotX 0.4.0 (2019-08-30)
|
||||
===================================================
|
||||
|
||||
Features:
|
||||
- Display read receipts in timeline (#81)
|
||||
|
||||
Improvements:
|
||||
- Reactions: Reinstate the ability to react with non-unicode keys (#307)
|
||||
|
||||
Bugfix:
|
||||
- Fix text diff linebreak display (#441)
|
||||
- Date change message repeats for each redaction until a normal message (#358)
|
||||
- Slide-in reply icon is distorted (#423)
|
||||
- Regression / e2e replies not encrypted
|
||||
- Some video won't play
|
||||
- Privacy: remove log of notifiable event (#519)
|
||||
- Fix crash with EmojiCompat (#530)
|
||||
|
||||
Changes in RiotX 0.3.0 (2019-08-08)
|
||||
===================================================
|
||||
|
||||
Features:
|
||||
- Create Direct Room flow
|
||||
- Handle `/markdown` command
|
||||
|
||||
Improvements:
|
||||
- UI for pending edits (#193)
|
||||
- UX image preview screen transition (#393)
|
||||
- Basic support for resending failed messages (retry/remove)
|
||||
- Enable proper cancellation of suspending functions (including db transaction)
|
||||
- Enhances network connectivity checks in SDK
|
||||
- Add "View Edit History" item in the message bottom sheet (#401)
|
||||
- Cancel sync request on pause and timeout to 0 after pause (#404)
|
||||
|
||||
Other changes:
|
||||
- Show sync progress also in room detail screen (#403)
|
||||
|
||||
Bugfix:
|
||||
- Edited message: link confusion when (edited) appears in body (#398)
|
||||
- Close detail room screen when the room is left with another client (#256)
|
||||
- Clear notification for a room left on another client
|
||||
- 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)
|
||||
|
||||
Build:
|
||||
- Split APK: generate one APK per arch, to reduce APK size of about 30%
|
||||
|
||||
|
||||
Changes in RiotX 0.2.0 (2019-07-18)
|
||||
===================================================
|
||||
|
||||
@@ -36,7 +138,7 @@ Mode details here: https://medium.com/@RiotChat/introducing-the-riotx-beta-for-a
|
||||
=======================================================
|
||||
|
||||
|
||||
Changes in RiotX 0.XX (2019-XX-XX)
|
||||
Changes in RiotX 0.0.0 (2019-XX-XX)
|
||||
===================================================
|
||||
|
||||
Features:
|
||||
|
@@ -33,11 +33,12 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation project(":matrix-sdk-android")
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0-beta01'
|
||||
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
||||
// Paging
|
||||
implementation "androidx.paging:paging-runtime-ktx:2.1.0"
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
|
@@ -20,6 +20,8 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.android.MainThreadDisposable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
|
||||
private class LiveDataObservable<T>(
|
||||
private val liveData: LiveData<T>,
|
||||
@@ -57,5 +59,5 @@ private class LiveDataObservable<T>(
|
||||
}
|
||||
|
||||
fun <T> LiveData<T>.asObservable(): Observable<T> {
|
||||
return LiveDataObservable(this)
|
||||
return LiveDataObservable(this).observeOn(Schedulers.computation())
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.rx
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import io.reactivex.CompletableEmitter
|
||||
import io.reactivex.SingleEmitter
|
||||
|
||||
internal class MatrixCallbackCompletable<T>(private val completableEmitter: CompletableEmitter) : MatrixCallback<T> {
|
||||
|
||||
override fun onSuccess(data: T) {
|
||||
completableEmitter.onComplete()
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
completableEmitter.tryOnError(failure)
|
||||
}
|
||||
}
|
||||
|
||||
fun Cancelable.toCompletable(completableEmitter: CompletableEmitter) {
|
||||
completableEmitter.setCancellable {
|
||||
this.cancel()
|
||||
}
|
||||
}
|
@@ -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.rx
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import io.reactivex.SingleEmitter
|
||||
|
||||
internal class MatrixCallbackSingle<T>(private val singleEmitter: SingleEmitter<T>) : MatrixCallback<T> {
|
||||
|
||||
override fun onSuccess(data: T) {
|
||||
singleEmitter.onSuccess(data)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
singleEmitter.tryOnError(failure)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Cancelable.toSingle(singleEmitter: SingleEmitter<T>) {
|
||||
singleEmitter.setCancellable {
|
||||
this.cancel()
|
||||
}
|
||||
}
|
@@ -18,27 +18,45 @@ package im.vector.matrix.rx
|
||||
|
||||
import im.vector.matrix.android.api.session.room.Room
|
||||
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
||||
import im.vector.matrix.android.api.session.room.model.ReadReceipt
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.send.UserDraft
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import io.reactivex.Single
|
||||
|
||||
class RxRoom(private val room: Room) {
|
||||
|
||||
fun liveRoomSummary(): Observable<RoomSummary> {
|
||||
return room.liveRoomSummary().asObservable().observeOn(Schedulers.computation())
|
||||
return room.liveRoomSummary().asObservable()
|
||||
}
|
||||
|
||||
fun liveRoomMemberIds(): Observable<List<String>> {
|
||||
return room.getRoomMemberIdsLive().asObservable().observeOn(Schedulers.computation())
|
||||
return room.getRoomMemberIdsLive().asObservable()
|
||||
}
|
||||
|
||||
fun liveAnnotationSummary(eventId: String): Observable<EventAnnotationsSummary> {
|
||||
return room.getEventSummaryLive(eventId).asObservable().observeOn(Schedulers.computation())
|
||||
return room.getEventSummaryLive(eventId).asObservable()
|
||||
}
|
||||
|
||||
fun liveTimelineEvent(eventId: String): Observable<TimelineEvent> {
|
||||
return room.liveTimeLineEvent(eventId).asObservable().observeOn(Schedulers.computation())
|
||||
return room.liveTimeLineEvent(eventId).asObservable()
|
||||
}
|
||||
|
||||
fun loadRoomMembersIfNeeded(): Single<Unit> = Single.create {
|
||||
room.loadRoomMembersIfNeeded(MatrixCallbackSingle(it)).toSingle(it)
|
||||
}
|
||||
|
||||
fun joinRoom(viaServers: List<String> = emptyList()): Single<Unit> = Single.create {
|
||||
room.join(viaServers, MatrixCallbackSingle(it)).toSingle(it)
|
||||
}
|
||||
|
||||
fun liveEventReadReceipts(eventId: String): Observable<List<ReadReceipt>> {
|
||||
return room.getEventReadReceiptsLive(eventId).asObservable()
|
||||
}
|
||||
|
||||
fun liveDrafts(): Observable<List<UserDraft>> {
|
||||
return room.getDraftsLive().asObservable()
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -16,30 +16,55 @@
|
||||
|
||||
package im.vector.matrix.rx
|
||||
|
||||
import androidx.paging.PagedList
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
import im.vector.matrix.android.api.session.pushers.Pusher
|
||||
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
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import io.reactivex.Single
|
||||
|
||||
class RxSession(private val session: Session) {
|
||||
|
||||
fun liveRoomSummaries(): Observable<List<RoomSummary>> {
|
||||
return session.liveRoomSummaries().asObservable().observeOn(Schedulers.computation())
|
||||
return session.liveRoomSummaries().asObservable()
|
||||
}
|
||||
|
||||
fun liveGroupSummaries(): Observable<List<GroupSummary>> {
|
||||
return session.liveGroupSummaries().asObservable().observeOn(Schedulers.computation())
|
||||
return session.liveGroupSummaries().asObservable()
|
||||
}
|
||||
|
||||
fun liveSyncState(): Observable<SyncState> {
|
||||
return session.syncState().asObservable().observeOn(Schedulers.computation())
|
||||
return session.syncState().asObservable()
|
||||
}
|
||||
|
||||
fun livePushers(): Observable<List<Pusher>> {
|
||||
return session.livePushers().asObservable().observeOn(Schedulers.computation())
|
||||
return session.livePushers().asObservable()
|
||||
}
|
||||
|
||||
fun liveUsers(): Observable<List<User>> {
|
||||
return session.liveUsers().asObservable()
|
||||
}
|
||||
|
||||
fun livePagedUsers(filter: String? = null): Observable<PagedList<User>> {
|
||||
return session.livePagedUsers(filter).asObservable()
|
||||
}
|
||||
|
||||
fun createRoom(roomParams: CreateRoomParams): Single<String> = Single.create {
|
||||
session.createRoom(roomParams, MatrixCallbackSingle(it)).toSingle(it)
|
||||
}
|
||||
|
||||
fun searchUsersDirectory(search: String,
|
||||
limit: Int,
|
||||
excludedUserIds: Set<String>): Single<List<User>> = Single.create {
|
||||
session.searchUsersDirectory(search, limit, excludedUserIds, MatrixCallbackSingle(it)).toSingle(it)
|
||||
}
|
||||
|
||||
fun joinRoom(roomId: String, viaServers: List<String> = emptyList()): Single<Unit> = Single.create {
|
||||
session.joinRoom(roomId, viaServers, MatrixCallbackSingle(it)).toSingle(it)
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -94,7 +94,6 @@ dependencies {
|
||||
def markwon_version = '3.0.0'
|
||||
def daggerVersion = '2.23.1'
|
||||
|
||||
implementation fileTree(dir: 'libs', include: ['*.aar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||
@@ -110,7 +109,7 @@ dependencies {
|
||||
implementation 'com.squareup.retrofit2:converter-moshi:2.4.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.14.1'
|
||||
implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0'
|
||||
implementation 'com.novoda:merlin:1.1.6'
|
||||
implementation 'com.novoda:merlin:1.2.0'
|
||||
implementation "com.squareup.moshi:moshi-adapters:$moshi_version"
|
||||
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
|
||||
|
||||
@@ -126,9 +125,6 @@ dependencies {
|
||||
// FP
|
||||
implementation "io.arrow-kt:arrow-core:$arrow_version"
|
||||
implementation "io.arrow-kt:arrow-instances-core:$arrow_version"
|
||||
implementation "io.arrow-kt:arrow-effects:$arrow_version"
|
||||
implementation "io.arrow-kt:arrow-effects-instances:$arrow_version"
|
||||
implementation "io.arrow-kt:arrow-integration-retrofit-adapter:$arrow_version"
|
||||
|
||||
// olm lib is now hosted by jitpack: https://jitpack.io/#org.matrix.gitlab.matrix-org/olm
|
||||
implementation 'org.matrix.gitlab.matrix-org:olm:3.1.2'
|
||||
@@ -143,6 +139,9 @@ dependencies {
|
||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||
implementation 'com.facebook.stetho:stetho-okhttp3:1.5.0'
|
||||
|
||||
// Bus
|
||||
implementation 'org.greenrobot:eventbus:3.1.1'
|
||||
|
||||
debugImplementation 'com.airbnb.okreplay:okreplay:1.4.0'
|
||||
releaseImplementation 'com.airbnb.okreplay:noop:1.4.0'
|
||||
androidTestImplementation 'com.airbnb.okreplay:espresso:1.4.0'
|
||||
|
Binary file not shown.
@@ -16,10 +16,10 @@
|
||||
|
||||
package im.vector.matrix.android;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
@@ -21,7 +21,7 @@ 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.RealmCryptoStoreModule
|
||||
import io.realm.RealmConfiguration
|
||||
import java.util.*
|
||||
import kotlin.random.Random
|
||||
|
||||
internal class CryptoStoreHelper {
|
||||
|
||||
@@ -35,7 +35,7 @@ internal class CryptoStoreHelper {
|
||||
}
|
||||
|
||||
fun createCredential() = Credentials(
|
||||
userId = "userId_" + Random().nextInt(),
|
||||
userId = "userId_" + Random.nextInt(),
|
||||
homeServer = "http://matrix.org",
|
||||
accessToken = "access_token",
|
||||
refreshToken = null,
|
||||
|
@@ -19,11 +19,7 @@ package im.vector.matrix.android.session.room.timeline
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.InstrumentedTest
|
||||
import im.vector.matrix.android.internal.database.helper.add
|
||||
import im.vector.matrix.android.internal.database.helper.addAll
|
||||
import im.vector.matrix.android.internal.database.helper.isUnlinked
|
||||
import im.vector.matrix.android.internal.database.helper.lastStateIndex
|
||||
import im.vector.matrix.android.internal.database.helper.merge
|
||||
import im.vector.matrix.android.internal.database.helper.*
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
||||
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeListOfEvents
|
||||
|
@@ -16,7 +16,6 @@
|
||||
|
||||
package im.vector.matrix.android.session.room.timeline
|
||||
|
||||
import arrow.core.Try
|
||||
import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask
|
||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
||||
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
|
||||
@@ -24,7 +23,7 @@ import kotlin.random.Random
|
||||
|
||||
internal class FakeGetContextOfEventTask constructor(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : GetContextOfEventTask {
|
||||
|
||||
override suspend fun execute(params: GetContextOfEventTask.Params): Try<TokenChunkEventPersistor.Result> {
|
||||
override suspend fun execute(params: GetContextOfEventTask.Params): TokenChunkEventPersistor.Result {
|
||||
val fakeEvents = RoomDataHelper.createFakeListOfEvents(30)
|
||||
val tokenChunkEvent = FakeTokenChunkEvent(
|
||||
Random.nextLong(System.currentTimeMillis()).toString(),
|
||||
|
@@ -16,7 +16,6 @@
|
||||
|
||||
package im.vector.matrix.android.session.room.timeline
|
||||
|
||||
import arrow.core.Try
|
||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationTask
|
||||
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
|
||||
import javax.inject.Inject
|
||||
@@ -24,7 +23,7 @@ import kotlin.random.Random
|
||||
|
||||
internal class FakePaginationTask @Inject constructor(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : PaginationTask {
|
||||
|
||||
override suspend fun execute(params: PaginationTask.Params): Try<TokenChunkEventPersistor.Result> {
|
||||
override suspend fun execute(params: PaginationTask.Params): TokenChunkEventPersistor.Result {
|
||||
val fakeEvents = RoomDataHelper.createFakeListOfEvents(30)
|
||||
val tokenChunkEvent = FakeTokenChunkEvent(params.from, Random.nextLong(System.currentTimeMillis()).toString(), fakeEvents)
|
||||
return tokenChunkEventPersistor.insertInDb(tokenChunkEvent, params.roomId, params.direction)
|
||||
|
@@ -28,7 +28,7 @@ object MatrixPatterns {
|
||||
// regex pattern to find matrix user ids in a string.
|
||||
// See https://matrix.org/speculator/spec/HEAD/appendices.html#historical-user-ids
|
||||
private const val MATRIX_USER_IDENTIFIER_REGEX = "@[A-Z0-9\\x21-\\x39\\x3B-\\x7F]+$DOMAIN_REGEX"
|
||||
private val PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER = MATRIX_USER_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE)
|
||||
val PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER = MATRIX_USER_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE)
|
||||
|
||||
// regex pattern to find room ids in a string.
|
||||
private const val MATRIX_ROOM_IDENTIFIER_REGEX = "![A-Z0-9]+$DOMAIN_REGEX"
|
||||
@@ -123,9 +123,9 @@ object MatrixPatterns {
|
||||
*/
|
||||
fun isEventId(str: String?): Boolean {
|
||||
return str != null
|
||||
&& (str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER
|
||||
|| str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3
|
||||
|| str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4)
|
||||
&& (str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER
|
||||
|| str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3
|
||||
|| str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,4 +137,23 @@ object MatrixPatterns {
|
||||
fun isGroupId(str: String?): Boolean {
|
||||
return str != null && str matches PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract server name from a matrix id
|
||||
*
|
||||
* @param matrixId
|
||||
* @return null if not found or if matrixId is null
|
||||
*/
|
||||
fun extractServerNameFromId(matrixId: String?): String? {
|
||||
if (matrixId == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val index = matrixId.indexOf(":")
|
||||
|
||||
return if (index == -1) {
|
||||
null
|
||||
} else matrixId.substring(index + 1)
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -17,16 +17,23 @@
|
||||
package im.vector.matrix.android.api.auth
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
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.session.Session
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
|
||||
|
||||
/**
|
||||
* This interface defines methods to authenticate to a matrix server.
|
||||
*/
|
||||
interface Authenticator {
|
||||
|
||||
/**
|
||||
* Request the supported login flows for this homeserver
|
||||
*/
|
||||
fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResponse>): Cancelable
|
||||
|
||||
/**
|
||||
* @param homeServerConnectionConfig this param is used to configure the Homeserver
|
||||
* @param login the login field
|
||||
@@ -56,4 +63,9 @@ interface Authenticator {
|
||||
* @return the associated session if any, or null
|
||||
*/
|
||||
fun getSession(sessionParams: SessionParams): Session?
|
||||
|
||||
/**
|
||||
* Create a session after a SSO successful login
|
||||
*/
|
||||
fun createSessionFromSso(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session
|
||||
}
|
@@ -31,7 +31,7 @@ import okhttp3.TlsVersion
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class HomeServerConnectionConfig(
|
||||
val homeServerUri: Uri,
|
||||
val identityServerUri: Uri,
|
||||
val identityServerUri: Uri? = null,
|
||||
val antiVirusServerUri: Uri? = null,
|
||||
val allowedFingerprints: MutableList<Fingerprint> = ArrayList(),
|
||||
val shouldPin: Boolean = false,
|
||||
@@ -48,7 +48,7 @@ data class HomeServerConnectionConfig(
|
||||
class Builder {
|
||||
|
||||
private lateinit var homeServerUri: Uri
|
||||
private lateinit var identityServerUri: Uri
|
||||
private var identityServerUri: Uri? = null
|
||||
private var antiVirusServerUri: Uri? = null
|
||||
private val allowedFingerprints: MutableList<Fingerprint> = ArrayList()
|
||||
private var shouldPin: Boolean = false
|
||||
|
@@ -17,7 +17,6 @@
|
||||
package im.vector.matrix.android.api.comparators
|
||||
|
||||
import im.vector.matrix.android.api.interfaces.DatedObject
|
||||
import java.util.*
|
||||
|
||||
object DatedObjectComparators {
|
||||
|
||||
|
@@ -19,7 +19,7 @@ package im.vector.matrix.android.api.extensions
|
||||
import im.vector.matrix.android.api.comparators.DatedObjectComparators
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||
import java.util.*
|
||||
import java.util.Collections
|
||||
|
||||
/* ==========================================================================================
|
||||
* MXDeviceInfo
|
||||
|
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.failure
|
||||
|
||||
// This data class will be sent to the bus
|
||||
data class ConsentNotGivenError(
|
||||
val consentUri: String
|
||||
)
|
@@ -31,6 +31,7 @@ import java.io.IOException
|
||||
*/
|
||||
sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) {
|
||||
data class Unknown(val throwable: Throwable? = null) : Failure(throwable)
|
||||
data class Cancelled(val throwable: Throwable? = null) : Failure(throwable)
|
||||
data class NetworkConnection(val ioException: IOException? = null) : Failure(ioException)
|
||||
data class ServerError(val error: MatrixError, val httpCode: Int) : Failure(RuntimeException(error.toString()))
|
||||
// When server send an error, but it cannot be interpreted as a MatrixError
|
||||
|
@@ -26,8 +26,13 @@ import com.squareup.moshi.JsonClass
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MatrixError(
|
||||
@Json(name = "errcode") val code: String,
|
||||
@Json(name = "error") val message: String
|
||||
) {
|
||||
@Json(name = "error") val message: String,
|
||||
|
||||
@Json(name = "consent_uri") val consentUri: String? = null,
|
||||
// RESOURCE_LIMIT_EXCEEDED data
|
||||
@Json(name = "limit_type") val limitType: String? = null,
|
||||
@Json(name = "admin_contact") val adminUri: String? = null) {
|
||||
|
||||
|
||||
companion object {
|
||||
const val FORBIDDEN = "M_FORBIDDEN"
|
||||
@@ -55,5 +60,8 @@ data class MatrixError(
|
||||
const val M_CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN"
|
||||
const val RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED"
|
||||
const val WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"
|
||||
|
||||
// Possible value for "limit_type"
|
||||
const val LIMIT_TYPE_MAU = "monthly_active_user"
|
||||
}
|
||||
}
|
@@ -18,6 +18,7 @@ package im.vector.matrix.android.api.permalinks
|
||||
|
||||
import android.text.style.ClickableSpan
|
||||
import android.view.View
|
||||
import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan.Callback
|
||||
|
||||
/**
|
||||
* This MatrixPermalinkSpan is a clickable span which use a [Callback] to communicate back.
|
||||
|
@@ -19,77 +19,92 @@ import im.vector.matrix.android.api.pushrules.rest.PushRule
|
||||
import timber.log.Timber
|
||||
|
||||
|
||||
class Action(val type: Type) {
|
||||
|
||||
enum class Type(val value: String) {
|
||||
NOTIFY("notify"),
|
||||
DONT_NOTIFY("dont_notify"),
|
||||
COALESCE("coalesce"),
|
||||
SET_TWEAK("set_tweak");
|
||||
|
||||
companion object {
|
||||
|
||||
fun safeValueOf(value: String): Type? {
|
||||
try {
|
||||
return valueOf(value)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var tweak_action: String? = null
|
||||
var stringValue: String? = null
|
||||
var boolValue: Boolean? = null
|
||||
|
||||
companion object {
|
||||
fun mapFrom(pushRule: PushRule): List<Action>? {
|
||||
val actions = ArrayList<Action>()
|
||||
pushRule.actions.forEach { actionStrOrObj ->
|
||||
if (actionStrOrObj is String) {
|
||||
when (actionStrOrObj) {
|
||||
Action.Type.NOTIFY.value -> Action(Action.Type.NOTIFY)
|
||||
Action.Type.DONT_NOTIFY.value -> Action(Action.Type.DONT_NOTIFY)
|
||||
else -> {
|
||||
Timber.w("Unsupported action type ${actionStrOrObj}")
|
||||
null
|
||||
}
|
||||
}?.let {
|
||||
actions.add(it)
|
||||
}
|
||||
} else if (actionStrOrObj is Map<*, *>) {
|
||||
val tweakAction = actionStrOrObj["set_tweak"] as? String
|
||||
when (tweakAction) {
|
||||
"sound" -> {
|
||||
(actionStrOrObj["value"] as? String)?.let { stringValue ->
|
||||
Action(Action.Type.SET_TWEAK).also {
|
||||
it.tweak_action = "sound"
|
||||
it.stringValue = stringValue
|
||||
actions.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
"highlight" -> {
|
||||
(actionStrOrObj["value"] as? Boolean)?.let { boolValue ->
|
||||
Action(Action.Type.SET_TWEAK).also {
|
||||
it.tweak_action = "highlight"
|
||||
it.boolValue = boolValue
|
||||
actions.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Timber.w("Unsupported action type ${actionStrOrObj}")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Timber.w("Unsupported action type ${actionStrOrObj}")
|
||||
return null
|
||||
}
|
||||
}
|
||||
return if (actions.isEmpty()) null else actions
|
||||
}
|
||||
}
|
||||
sealed class Action {
|
||||
object Notify : Action()
|
||||
object DoNotNotify : Action()
|
||||
data class Sound(val sound: String) : Action()
|
||||
data class Highlight(val highlight: Boolean) : Action()
|
||||
}
|
||||
|
||||
|
||||
private const val ACTION_NOTIFY = "notify"
|
||||
private const val ACTION_DONT_NOTIFY = "dont_notify"
|
||||
private const val ACTION_COALESCE = "coalesce"
|
||||
|
||||
// Ref: https://matrix.org/docs/spec/client_server/latest#tweaks
|
||||
private const val ACTION_OBJECT_SET_TWEAK_KEY = "set_tweak"
|
||||
|
||||
private const val ACTION_OBJECT_SET_TWEAK_VALUE_SOUND = "sound"
|
||||
private const val ACTION_OBJECT_SET_TWEAK_VALUE_HIGHLIGHT = "highlight"
|
||||
|
||||
private const val ACTION_OBJECT_VALUE_KEY = "value"
|
||||
private const val ACTION_OBJECT_VALUE_VALUE_DEFAULT = "default"
|
||||
|
||||
/**
|
||||
* Ref: https://matrix.org/docs/spec/client_server/latest#actions
|
||||
*
|
||||
* Convert
|
||||
* <pre>
|
||||
* "actions": [
|
||||
* "notify",
|
||||
* {
|
||||
* "set_tweak": "sound",
|
||||
* "value": "default"
|
||||
* },
|
||||
* {
|
||||
* "set_tweak": "highlight"
|
||||
* }
|
||||
* ]
|
||||
*
|
||||
* To
|
||||
* [
|
||||
* Action.Notify,
|
||||
* Action.Sound("default"),
|
||||
* Action.Highlight(true)
|
||||
* ]
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
fun PushRule.getActions(): List<Action> {
|
||||
val result = ArrayList<Action>()
|
||||
|
||||
actions.forEach { actionStrOrObj ->
|
||||
when (actionStrOrObj) {
|
||||
ACTION_NOTIFY -> Action.Notify
|
||||
ACTION_DONT_NOTIFY -> Action.DoNotNotify
|
||||
is Map<*, *> -> {
|
||||
when (actionStrOrObj[ACTION_OBJECT_SET_TWEAK_KEY]) {
|
||||
ACTION_OBJECT_SET_TWEAK_VALUE_SOUND -> {
|
||||
(actionStrOrObj[ACTION_OBJECT_VALUE_KEY] as? String)?.let { stringValue ->
|
||||
Action.Sound(stringValue)
|
||||
}
|
||||
// When the value is not there, default sound (not specified by the spec)
|
||||
?: Action.Sound(ACTION_OBJECT_VALUE_VALUE_DEFAULT)
|
||||
|
||||
}
|
||||
ACTION_OBJECT_SET_TWEAK_VALUE_HIGHLIGHT -> {
|
||||
(actionStrOrObj[ACTION_OBJECT_VALUE_KEY] as? Boolean)?.let { boolValue ->
|
||||
Action.Highlight(boolValue)
|
||||
}
|
||||
// When the value is not there, default is true, says the spec
|
||||
?: Action.Highlight(true)
|
||||
}
|
||||
else -> {
|
||||
Timber.w("Unsupported set_tweak value ${actionStrOrObj[ACTION_OBJECT_SET_TWEAK_KEY]}")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Timber.w("Unsupported action type $actionStrOrObj")
|
||||
null
|
||||
}
|
||||
}?.let {
|
||||
result.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
@@ -18,20 +18,21 @@ package im.vector.matrix.android.api.pushrules
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.pushrules.rest.PushRule
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
|
||||
interface PushRuleService {
|
||||
|
||||
/**
|
||||
* Fetch the push rules from the server
|
||||
*/
|
||||
fun fetchPushRules(scope: String = "global")
|
||||
fun fetchPushRules(scope: String = RuleScope.GLOBAL)
|
||||
|
||||
//TODO get push rule set
|
||||
fun getPushRules(scope: String = "global"): List<PushRule>
|
||||
fun getPushRules(scope: String = RuleScope.GLOBAL): List<PushRule>
|
||||
|
||||
//TODO update rule
|
||||
|
||||
fun updatePushRuleEnableStatus(kind: String, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback<Unit>)
|
||||
fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
fun addPushRuleListener(listener: PushRuleListener)
|
||||
|
||||
@@ -41,6 +42,8 @@ interface PushRuleService {
|
||||
|
||||
interface PushRuleListener {
|
||||
fun onMatchRule(event: Event, actions: List<Action>)
|
||||
fun onRoomLeft(roomId: String)
|
||||
fun onEventRedacted(redactedEventId: String)
|
||||
fun batchFinish()
|
||||
}
|
||||
}
|
@@ -18,18 +18,17 @@ package im.vector.matrix.android.api.pushrules
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.room.RoomService
|
||||
import timber.log.Timber
|
||||
import java.util.regex.Pattern
|
||||
|
||||
private val regex = Pattern.compile("^(==|<=|>=|<|>)?(\\d*)$")
|
||||
private val regex = Regex("^(==|<=|>=|<|>)?(\\d*)$")
|
||||
|
||||
class RoomMemberCountCondition(val `is`: String) : Condition(Kind.room_member_count) {
|
||||
class RoomMemberCountCondition(val iz: String) : Condition(Kind.room_member_count) {
|
||||
|
||||
override fun isSatisfied(conditionResolver: ConditionResolver): Boolean {
|
||||
return conditionResolver.resolveRoomMemberCountCondition(this)
|
||||
}
|
||||
|
||||
override fun technicalDescription(): String {
|
||||
return "Room member count is $`is`"
|
||||
return "Room member count is $iz"
|
||||
}
|
||||
|
||||
fun isSatisfied(event: Event, session: RoomService?): Boolean {
|
||||
@@ -56,12 +55,9 @@ class RoomMemberCountCondition(val `is`: String) : Condition(Kind.room_member_co
|
||||
*/
|
||||
private fun parseIsField(): Pair<String?, Int>? {
|
||||
try {
|
||||
val match = regex.matcher(`is`)
|
||||
if (match.find()) {
|
||||
val prefix = match.group(1)
|
||||
val count = match.group(2).toInt()
|
||||
return prefix to count
|
||||
}
|
||||
val match = regex.find(iz) ?: return null
|
||||
val (prefix, count) = match.destructured
|
||||
return prefix to count.toInt()
|
||||
} catch (t: Throwable) {
|
||||
Timber.d(t)
|
||||
}
|
||||
|
@@ -37,7 +37,7 @@ object RuleIds {
|
||||
|
||||
// Default Underride Rules
|
||||
const val RULE_ID_CALL = ".m.rule.call"
|
||||
const val RULE_ID_one_to_one_encrypted_room = ".m.rule.encrypted_room_one_to_one"
|
||||
const val RULE_ID_ONE_TO_ONE_ENCRYPTED_ROOM = ".m.rule.encrypted_room_one_to_one"
|
||||
const val RULE_ID_ONE_TO_ONE_ROOM = ".m.rule.room_one_to_one"
|
||||
const val RULE_ID_ALL_OTHER_MESSAGES_ROOMS = ".m.rule.message"
|
||||
const val RULE_ID_ENCRYPTED = ".m.rule.encrypted"
|
||||
|
@@ -15,12 +15,6 @@
|
||||
*/
|
||||
package im.vector.matrix.android.api.pushrules
|
||||
|
||||
|
||||
enum class RulesetKey(val value: String) {
|
||||
CONTENT("content"),
|
||||
OVERRIDE("override"),
|
||||
ROOM("room"),
|
||||
SENDER("sender"),
|
||||
UNDERRIDE("underride"),
|
||||
UNKNOWN("")
|
||||
object RuleScope {
|
||||
const val GLOBAL = "global"
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.pushrules
|
||||
|
||||
/**
|
||||
* Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushrules
|
||||
*/
|
||||
enum class RuleSetKey(val value: String) {
|
||||
CONTENT("content"),
|
||||
OVERRIDE("override"),
|
||||
ROOM("room"),
|
||||
SENDER("sender"),
|
||||
UNDERRIDE("underride")
|
||||
}
|
||||
|
||||
/**
|
||||
* Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushrules-scope-kind-ruleid
|
||||
*/
|
||||
typealias RuleKind = RuleSetKey
|
@@ -71,7 +71,7 @@ data class PushCondition(
|
||||
this.key?.let { SenderNotificationPermissionCondition(it) }
|
||||
}
|
||||
Condition.Kind.UNRECOGNIZE -> {
|
||||
Timber.e("Unknwon kind $kind")
|
||||
Timber.e("Unknown kind $kind")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
@@ -20,10 +20,10 @@ import androidx.lifecycle.LiveData
|
||||
|
||||
interface InitialSyncProgressService {
|
||||
|
||||
fun getLiveStatus() : LiveData<Status?>
|
||||
fun getInitialSyncProgressStatus() : LiveData<Status?>
|
||||
|
||||
data class Status(
|
||||
@StringRes val statusText: Int?,
|
||||
@StringRes val statusText: Int,
|
||||
val percentProgress: Int = 0
|
||||
)
|
||||
}
|
@@ -29,6 +29,7 @@ import im.vector.matrix.android.api.session.group.GroupService
|
||||
import im.vector.matrix.android.api.session.pushers.PushersService
|
||||
import im.vector.matrix.android.api.session.room.RoomDirectoryService
|
||||
import im.vector.matrix.android.api.session.room.RoomService
|
||||
import im.vector.matrix.android.api.session.securestorage.SecureStorageService
|
||||
import im.vector.matrix.android.api.session.signout.SignOutService
|
||||
import im.vector.matrix.android.api.session.sync.FilterService
|
||||
import im.vector.matrix.android.api.session.sync.SyncState
|
||||
@@ -50,7 +51,8 @@ interface Session :
|
||||
FileService,
|
||||
PushRuleService,
|
||||
PushersService,
|
||||
InitialSyncProgressService {
|
||||
InitialSyncProgressService,
|
||||
SecureStorageService {
|
||||
|
||||
/**
|
||||
* The params associated to the session
|
||||
@@ -87,7 +89,7 @@ interface Session :
|
||||
/**
|
||||
* This method start the sync thread.
|
||||
*/
|
||||
fun startSync(fromForeground : Boolean)
|
||||
fun startSync(fromForeground: Boolean)
|
||||
|
||||
/**
|
||||
* This method stop the sync thread.
|
||||
|
@@ -26,14 +26,12 @@ import im.vector.matrix.android.api.session.events.model.Content
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
|
||||
import im.vector.matrix.android.internal.crypto.NewSessionListener
|
||||
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
|
||||
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||
import java.io.File
|
||||
|
||||
interface CryptoService {
|
||||
|
||||
@@ -111,8 +109,6 @@ interface CryptoService {
|
||||
|
||||
fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>>)
|
||||
|
||||
fun clearCryptoCache(callback: MatrixCallback<Unit>)
|
||||
|
||||
fun addNewSessionListener(newSessionListener: NewSessionListener)
|
||||
|
||||
fun removeSessionListener(listener: NewSessionListener)
|
||||
|
@@ -20,6 +20,9 @@ import android.text.TextUtils
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import im.vector.matrix.android.api.util.JsonDict
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
@@ -79,9 +82,15 @@ data class Event(
|
||||
) {
|
||||
|
||||
|
||||
@Transient
|
||||
var mxDecryptionResult: OlmDecryptionResult? = null
|
||||
|
||||
@Transient
|
||||
var mCryptoError: MXCryptoError.ErrorType? = null
|
||||
|
||||
@Transient
|
||||
var sendState: SendState = SendState.UNKNOWN
|
||||
|
||||
|
||||
/**
|
||||
* Check if event is a state event.
|
||||
@@ -95,42 +104,6 @@ data class Event(
|
||||
// Crypto
|
||||
//==============================================================================================================
|
||||
|
||||
// /**
|
||||
// * For encrypted events, the plaintext payload for the event.
|
||||
// * This is a small MXEvent instance with typically value for `type` and 'content' fields.
|
||||
// */
|
||||
// @Transient
|
||||
// var mClearEvent: Event? = null
|
||||
// private set
|
||||
//
|
||||
// /**
|
||||
// * Curve25519 key which we believe belongs to the sender of the event.
|
||||
// * See `senderKey` property.
|
||||
// */
|
||||
// @Transient
|
||||
// private var mSenderCurve25519Key: String? = null
|
||||
//
|
||||
// /**
|
||||
// * Ed25519 key which the sender of this event (for olm) or the creator of the megolm session (for megolm) claims to own.
|
||||
// * See `claimedEd25519Key` property.
|
||||
// */
|
||||
// @Transient
|
||||
// private var mClaimedEd25519Key: String? = null
|
||||
//
|
||||
// /**
|
||||
// * Curve25519 keys of devices involved in telling us about the senderCurve25519Key and claimedEd25519Key.
|
||||
// * See `forwardingCurve25519KeyChain` property.
|
||||
// */
|
||||
// @Transient
|
||||
// private var mForwardingCurve25519KeyChain: List<String> = ArrayList()
|
||||
//
|
||||
// /**
|
||||
// * Decryption error
|
||||
// */
|
||||
// @Transient
|
||||
// var mCryptoError: MXCryptoError? = null
|
||||
// private set
|
||||
|
||||
/**
|
||||
* @return true if this event is encrypted.
|
||||
*/
|
||||
@@ -138,51 +111,11 @@ data class Event(
|
||||
return TextUtils.equals(type, EventType.ENCRYPTED)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the clear data on this event.
|
||||
* This is used after decrypting an event; it should not be used by applications.
|
||||
*
|
||||
* @param decryptionResult the decryption result, including the plaintext and some key info.
|
||||
*/
|
||||
// internal fun setClearData(decryptionResult: MXEventDecryptionResult?) {
|
||||
// mClearEvent = null
|
||||
// if (decryptionResult != null) {
|
||||
// if (decryptionResult.clearEvent != null) {
|
||||
// val adapter = MoshiProvider.providesMoshi().adapter(Event::class.java)
|
||||
// mClearEvent = adapter.fromJsonValue(decryptionResult.clearEvent)
|
||||
//
|
||||
// if (mClearEvent != null) {
|
||||
// mSenderCurve25519Key = decryptionResult.senderCurve25519Key
|
||||
// mClaimedEd25519Key = decryptionResult.claimedEd25519Key
|
||||
// mForwardingCurve25519KeyChain = decryptionResult.forwardingCurve25519KeyChain
|
||||
//
|
||||
// // For encrypted events with relation, the m.relates_to is kept in clear, so we need to put it back
|
||||
// // in the clear event
|
||||
// try {
|
||||
// content?.get("m.relates_to")?.let { clearRelates ->
|
||||
// mClearEvent = mClearEvent?.copy(
|
||||
// content = HashMap(mClearEvent!!.content).apply {
|
||||
// this["m.relates_to"] = clearRelates
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
// } catch (e: Exception) {
|
||||
// Timber.e(e, "Unable to restore 'm.relates_to' the clear event")
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// }
|
||||
// }
|
||||
// mCryptoError = null
|
||||
// }
|
||||
|
||||
/**
|
||||
* @return The curve25519 key that sent this event.
|
||||
*/
|
||||
fun getSenderKey(): String? {
|
||||
return mxDecryptionResult?.senderKey
|
||||
// return mClearEvent?.mSenderCurve25519Key ?: mSenderCurve25519Key
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -190,23 +123,13 @@ data class Event(
|
||||
*/
|
||||
fun getKeysClaimed(): Map<String, String> {
|
||||
return mxDecryptionResult?.keysClaimed ?: HashMap()
|
||||
// val res = HashMap<String, String>()
|
||||
//
|
||||
// val claimedEd25519Key = if (null != mClearEvent) mClearEvent!!.mClaimedEd25519Key else mClaimedEd25519Key
|
||||
//
|
||||
// if (null != claimedEd25519Key) {
|
||||
// res["ed25519"] = claimedEd25519Key
|
||||
// }
|
||||
//
|
||||
// return res
|
||||
}
|
||||
//
|
||||
|
||||
/**
|
||||
* @return the event type
|
||||
*/
|
||||
fun getClearType(): String {
|
||||
return mxDecryptionResult?.payload?.get("type")?.toString()
|
||||
?: type//get("type")?.toString() ?: type
|
||||
return mxDecryptionResult?.payload?.get("type")?.toString() ?: type
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -216,30 +139,8 @@ data class Event(
|
||||
return mxDecryptionResult?.payload?.get("content") as? Content ?: content
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return the linked crypto error
|
||||
// */
|
||||
// fun getCryptoError(): MXCryptoError? {
|
||||
// return mCryptoError
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Update the linked crypto error
|
||||
// *
|
||||
// * @param error the new crypto error.
|
||||
// */
|
||||
// fun setCryptoError(error: MXCryptoError?) {
|
||||
// mCryptoError = error
|
||||
// if (null != error) {
|
||||
// mClearEvent = null
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
fun toContentStringWithIndent(): String {
|
||||
val contentMap = this.toContent()?.toMutableMap() ?: HashMap()
|
||||
contentMap.remove("mxDecryptionResult")
|
||||
contentMap.remove("mCryptoError")
|
||||
val contentMap = toContent()?.toMutableMap() ?: HashMap()
|
||||
return JSONObject(contentMap).toString(4)
|
||||
}
|
||||
|
||||
@@ -272,6 +173,7 @@ data class Event(
|
||||
if (redacts != other.redacts) return false
|
||||
if (mxDecryptionResult != other.mxDecryptionResult) return false
|
||||
if (mCryptoError != other.mCryptoError) return false
|
||||
if (sendState != other.sendState) return false
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -289,6 +191,27 @@ data class Event(
|
||||
result = 31 * result + (redacts?.hashCode() ?: 0)
|
||||
result = 31 * result + (mxDecryptionResult?.hashCode() ?: 0)
|
||||
result = 31 * result + (mCryptoError?.hashCode() ?: 0)
|
||||
result = 31 * result + sendState.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
fun Event.isTextMessage(): Boolean {
|
||||
return getClearType() == EventType.MESSAGE
|
||||
&& when (getClearContent()?.toModel<MessageContent>()?.type) {
|
||||
MessageType.MSGTYPE_TEXT,
|
||||
MessageType.MSGTYPE_EMOTE,
|
||||
MessageType.MSGTYPE_NOTICE -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun Event.isImageMessage(): Boolean {
|
||||
return getClearType() == EventType.MESSAGE
|
||||
&& when (getClearContent()?.toModel<MessageContent>()?.type) {
|
||||
MessageType.MSGTYPE_IMAGE -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
@@ -16,12 +16,15 @@
|
||||
|
||||
package im.vector.matrix.android.api.session.group.model
|
||||
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
|
||||
/**
|
||||
* This class holds some data of a group.
|
||||
* It can be retrieved through [im.vector.matrix.android.api.session.group.GroupService]
|
||||
*/
|
||||
data class GroupSummary(
|
||||
val groupId: String,
|
||||
val membership: Membership,
|
||||
val displayName: String = "",
|
||||
val shortDescription: String = "",
|
||||
val avatarUrl: String = "",
|
||||
|
@@ -16,9 +16,6 @@
|
||||
package im.vector.matrix.android.api.session.pushers
|
||||
|
||||
data class Pusher(
|
||||
|
||||
val userId: String,
|
||||
|
||||
val pushKey: String,
|
||||
val kind: String,
|
||||
val appId: String,
|
||||
|
@@ -17,7 +17,7 @@ package im.vector.matrix.android.api.session.pushers
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
|
||||
|
||||
interface PushersService {
|
||||
|
@@ -22,6 +22,7 @@ import im.vector.matrix.android.api.session.room.members.MembershipService
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.model.relation.RelationService
|
||||
import im.vector.matrix.android.api.session.room.read.ReadService
|
||||
import im.vector.matrix.android.api.session.room.send.DraftService
|
||||
import im.vector.matrix.android.api.session.room.send.SendService
|
||||
import im.vector.matrix.android.api.session.room.state.StateService
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineService
|
||||
@@ -32,6 +33,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineService
|
||||
interface Room :
|
||||
TimelineService,
|
||||
SendService,
|
||||
DraftService,
|
||||
ReadService,
|
||||
MembershipService,
|
||||
StateService,
|
||||
|
@@ -30,20 +30,17 @@ interface RoomDirectoryService {
|
||||
/**
|
||||
* Get rooms from directory
|
||||
*/
|
||||
fun getPublicRooms(server: String?,
|
||||
publicRoomsParams: PublicRoomsParams,
|
||||
callback: MatrixCallback<PublicRoomsResponse>): Cancelable
|
||||
fun getPublicRooms(server: String?, publicRoomsParams: PublicRoomsParams, callback: MatrixCallback<PublicRoomsResponse>): Cancelable
|
||||
|
||||
/**
|
||||
* Join a room by id
|
||||
*/
|
||||
fun joinRoom(roomId: String,
|
||||
callback: MatrixCallback<Unit>)
|
||||
fun joinRoom(roomId: String, callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Fetches the overall metadata about protocols supported by the homeserver.
|
||||
* Includes both the available protocols and all fields required for queries against each protocol.
|
||||
*/
|
||||
fun getThirdPartyProtocol(callback: MatrixCallback<Map<String, ThirdPartyProtocol>>)
|
||||
fun getThirdPartyProtocol(callback: MatrixCallback<Map<String, ThirdPartyProtocol>>): Cancelable
|
||||
|
||||
}
|
@@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
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
|
||||
|
||||
/**
|
||||
* This interface defines methods to get rooms. It's implemented at the session level.
|
||||
@@ -27,10 +28,18 @@ import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||
interface RoomService {
|
||||
|
||||
/**
|
||||
* Create a room
|
||||
* Create a room asynchronously
|
||||
*/
|
||||
fun createRoom(createRoomParams: CreateRoomParams,
|
||||
callback: MatrixCallback<String>)
|
||||
fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>): Cancelable
|
||||
|
||||
/**
|
||||
* Join a room by id
|
||||
* @param roomId the roomId of the room to join
|
||||
* @param viaServers the servers to attempt to join the room through. One of the servers must be participating in the room.
|
||||
*/
|
||||
fun joinRoom(roomId: String,
|
||||
viaServers: List<String> = emptyList(),
|
||||
callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Get a room from a roomId
|
||||
|
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.room.failure
|
||||
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
|
||||
sealed class CreateRoomFailure : Failure.FeatureFailure() {
|
||||
|
||||
object CreatedWithTimeout: CreateRoomFailure()
|
||||
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.room.failure
|
||||
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
|
||||
sealed class JoinRoomFailure : Failure.FeatureFailure() {
|
||||
|
||||
object JoinedWithTimeout : JoinRoomFailure()
|
||||
|
||||
}
|
@@ -30,7 +30,7 @@ interface MembershipService {
|
||||
* This methods load all room members if it was done yet.
|
||||
* @return a [Cancelable]
|
||||
*/
|
||||
fun loadRoomMembersIfNeeded(): Cancelable
|
||||
fun loadRoomMembersIfNeeded(matrixCallback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Return the roomMember with userId or null.
|
||||
@@ -52,16 +52,17 @@ interface MembershipService {
|
||||
/**
|
||||
* Invite a user in the room
|
||||
*/
|
||||
fun invite(userId: String, callback: MatrixCallback<Unit>)
|
||||
fun invite(userId: String, callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Join the room, or accept an invitation.
|
||||
*/
|
||||
fun join(callback: MatrixCallback<Unit>)
|
||||
|
||||
fun join(viaServers: List<String> = emptyList(), callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Leave the room, or reject an invitation.
|
||||
*/
|
||||
fun leave(callback: MatrixCallback<Unit>)
|
||||
fun leave(callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
}
|
@@ -16,8 +16,9 @@
|
||||
|
||||
package im.vector.matrix.android.api.session.room.model
|
||||
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
|
||||
data class ReadReceipt(
|
||||
val userId: String,
|
||||
val eventId: String,
|
||||
val user: User,
|
||||
val originServerTs: Long
|
||||
)
|
@@ -17,6 +17,7 @@
|
||||
package im.vector.matrix.android.api.session.room.model
|
||||
|
||||
import im.vector.matrix.android.api.session.room.model.tag.RoomTag
|
||||
import im.vector.matrix.android.api.session.room.send.UserDraft
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
|
||||
/**
|
||||
@@ -29,10 +30,17 @@ data class RoomSummary(
|
||||
val topic: String = "",
|
||||
val avatarUrl: String = "",
|
||||
val isDirect: Boolean = false,
|
||||
val latestEvent: TimelineEvent? = null,
|
||||
val latestPreviewableEvent: TimelineEvent? = null,
|
||||
val otherMemberIds: List<String> = emptyList(),
|
||||
val notificationCount: Int = 0,
|
||||
val highlightCount: Int = 0,
|
||||
val hasUnreadMessages: Boolean = false,
|
||||
val tags: List<RoomTag> = emptyList(),
|
||||
val membership: Membership = Membership.NONE
|
||||
)
|
||||
val membership: Membership = Membership.NONE,
|
||||
val versioningState: VersioningState = VersioningState.NONE,
|
||||
val userDrafts: List<UserDraft> = emptyList()
|
||||
) {
|
||||
|
||||
val isVersioned: Boolean
|
||||
get() = versioningState != VersioningState.NONE
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.room.model
|
||||
|
||||
enum class VersioningState {
|
||||
NONE,
|
||||
UPGRADED_ROOM_NOT_JOINED,
|
||||
UPGRADED_ROOM_JOINED
|
||||
}
|
@@ -20,7 +20,6 @@ import android.util.Patterns
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.api.MatrixPatterns.isUserId
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
@@ -29,7 +28,6 @@ import im.vector.matrix.android.api.session.room.model.PowerLevels
|
||||
import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
|
||||
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
|
||||
import im.vector.matrix.android.internal.auth.data.ThreePidMedium
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Parameter to create a room, with facilities functions to configure it
|
||||
@@ -128,12 +126,12 @@ class CreateRoomParams {
|
||||
contentMap["algorithm"] = algorithm
|
||||
|
||||
val algoEvent = Event(type = EventType.ENCRYPTION,
|
||||
stateKey = "",
|
||||
content = contentMap.toContent()
|
||||
stateKey = "",
|
||||
content = contentMap.toContent()
|
||||
)
|
||||
|
||||
if (null == initialStates) {
|
||||
initialStates = Arrays.asList<Event>(algoEvent)
|
||||
initialStates = mutableListOf(algoEvent)
|
||||
} else {
|
||||
initialStates!!.add(algoEvent)
|
||||
}
|
||||
@@ -162,11 +160,11 @@ class CreateRoomParams {
|
||||
contentMap["history_visibility"] = historyVisibility
|
||||
|
||||
val historyVisibilityEvent = Event(type = EventType.STATE_HISTORY_VISIBILITY,
|
||||
stateKey = "",
|
||||
content = contentMap.toContent())
|
||||
stateKey = "",
|
||||
content = contentMap.toContent())
|
||||
|
||||
if (null == initialStates) {
|
||||
initialStates = Arrays.asList<Event>(historyVisibilityEvent)
|
||||
initialStates = mutableListOf(historyVisibilityEvent)
|
||||
} else {
|
||||
initialStates!!.add(historyVisibilityEvent)
|
||||
}
|
||||
@@ -202,8 +200,8 @@ class CreateRoomParams {
|
||||
*/
|
||||
fun isDirect(): Boolean {
|
||||
return preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT
|
||||
&& isDirect == true
|
||||
&& (1 == getInviteCount() || 1 == getInvite3PidCount())
|
||||
&& isDirect == true
|
||||
&& (1 == getInviteCount() || 1 == getInvite3PidCount())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -220,22 +218,21 @@ class CreateRoomParams {
|
||||
* @param ids the participant ids to add.
|
||||
*/
|
||||
fun addParticipantIds(hsConfig: HomeServerConnectionConfig,
|
||||
credentials: Credentials,
|
||||
userId: String,
|
||||
ids: List<String>) {
|
||||
for (id in ids) {
|
||||
if (Patterns.EMAIL_ADDRESS.matcher(id).matches()) {
|
||||
if (Patterns.EMAIL_ADDRESS.matcher(id).matches() && hsConfig.identityServerUri != null) {
|
||||
if (null == invite3pids) {
|
||||
invite3pids = ArrayList()
|
||||
}
|
||||
|
||||
val pid = Invite3Pid(idServer = hsConfig.identityServerUri.host!!,
|
||||
medium = ThreePidMedium.EMAIL,
|
||||
address = id)
|
||||
medium = ThreePidMedium.EMAIL,
|
||||
address = id)
|
||||
|
||||
invite3pids!!.add(pid)
|
||||
} else if (isUserId(id)) {
|
||||
// do not invite oneself
|
||||
if (credentials.userId != id) {
|
||||
if (userId != id) {
|
||||
if (null == invitedUserIds) {
|
||||
invitedUserIds = ArrayList()
|
||||
}
|
||||
|
@@ -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.api.session.room.model.create
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* A link to an old room in case of room versioning
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Predecessor(
|
||||
@Json(name = "room_id") val roomId: String? = null,
|
||||
@Json(name = "event_id") val eventId: String? = null
|
||||
)
|
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.room.model.create
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Content of a m.room.create type event
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class RoomCreateContent(
|
||||
@Json(name = "creator") val creator: String? = null,
|
||||
@Json(name = "room_version") val roomVersion: String? = null,
|
||||
@Json(name = "predecessor") val predecessor: Predecessor? = null
|
||||
)
|
||||
|
||||
|
@@ -29,5 +29,5 @@ interface MessageContent {
|
||||
|
||||
|
||||
fun MessageContent?.isReply(): Boolean {
|
||||
return this?.relatesTo?.inReplyTo != null
|
||||
return this?.relatesTo?.inReplyTo?.eventId != null
|
||||
}
|
@@ -21,5 +21,5 @@ import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ReplyToContent(
|
||||
@Json(name = "event_id") val eventId: String
|
||||
@Json(name = "event_id") val eventId: String? = null
|
||||
)
|
@@ -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.api.session.room.model.tombstone
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Class to contains Tombstone information
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class RoomTombstoneContent(
|
||||
@Json(name = "body") val body: String? = null,
|
||||
@Json(name = "replacement_room") val replacementRoom: String?
|
||||
)
|
@@ -16,7 +16,9 @@
|
||||
|
||||
package im.vector.matrix.android.api.session.room.read
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.room.model.ReadReceipt
|
||||
|
||||
/**
|
||||
* This interface defines methods to handle read receipts and read marker in a room. It's implemented at the room level.
|
||||
@@ -39,4 +41,6 @@ interface ReadService {
|
||||
fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback<Unit>)
|
||||
|
||||
fun isEventRead(eventId: String): Boolean
|
||||
|
||||
fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>>
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.room.send
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
|
||||
interface DraftService {
|
||||
|
||||
/**
|
||||
* Save or update a draft to the room
|
||||
*/
|
||||
fun saveDraft(draft: UserDraft)
|
||||
|
||||
/**
|
||||
* Delete the last draft, basically just after sending the message
|
||||
*/
|
||||
fun deleteDraft()
|
||||
|
||||
/**
|
||||
* 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>>
|
||||
|
||||
}
|
@@ -19,6 +19,7 @@ package im.vector.matrix.android.api.session.room.send
|
||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
|
||||
|
||||
@@ -65,4 +66,31 @@ interface SendService {
|
||||
*/
|
||||
fun redactEvent(event: Event, reason: String?): Cancelable
|
||||
|
||||
|
||||
/**
|
||||
* Schedule this message to be resent
|
||||
* @param localEcho the unsent local echo
|
||||
*/
|
||||
fun resendTextMessage(localEcho: TimelineEvent): Cancelable?
|
||||
|
||||
/**
|
||||
* Schedule this message to be resent
|
||||
* @param localEcho the unsent local echo
|
||||
*/
|
||||
fun resendMediaMessage(localEcho: TimelineEvent): Cancelable?
|
||||
|
||||
|
||||
/**
|
||||
* Remove this failed message from the timeline
|
||||
* @param localEcho the unsent local echo
|
||||
*/
|
||||
fun deleteFailedEcho(localEcho: TimelineEvent)
|
||||
|
||||
fun clearSendingQueue()
|
||||
|
||||
/**
|
||||
* Resend all failed messages one by one (and keep order)
|
||||
*/
|
||||
fun resendAllFailedMessages()
|
||||
|
||||
}
|
@@ -16,6 +16,7 @@
|
||||
|
||||
package im.vector.matrix.android.api.session.room.send
|
||||
|
||||
|
||||
enum class SendState {
|
||||
UNKNOWN,
|
||||
// the event has not been sent
|
||||
@@ -33,12 +34,19 @@ enum class SendState {
|
||||
// the event failed to be sent because some unknown devices have been found while encrypting it
|
||||
FAILED_UNKNOWN_DEVICES;
|
||||
|
||||
fun isSent(): Boolean {
|
||||
return this == SENT || this == SYNCED
|
||||
internal companion object {
|
||||
val HAS_FAILED_STATES = listOf(UNDELIVERED, FAILED_UNKNOWN_DEVICES)
|
||||
val IS_SENT_STATES = listOf(SENT, SYNCED)
|
||||
val IS_SENDING_STATES = listOf(UNSENT, ENCRYPTING, SENDING)
|
||||
val PENDING_STATES = IS_SENDING_STATES + HAS_FAILED_STATES
|
||||
}
|
||||
|
||||
fun hasFailed(): Boolean {
|
||||
return this == UNDELIVERED || this == FAILED_UNKNOWN_DEVICES
|
||||
}
|
||||
fun isSent() = IS_SENT_STATES.contains(this)
|
||||
|
||||
fun hasFailed() = HAS_FAILED_STATES.contains(this)
|
||||
|
||||
fun isSending() = IS_SENDING_STATES.contains(this)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.room.send
|
||||
|
||||
/**
|
||||
* Describes a user draft:
|
||||
* REGULAR: draft of a classical message
|
||||
* QUOTE: draft of a message which quotes another message
|
||||
* EDIT: draft of an edition of a message
|
||||
* REPLY: draft of a reply of another message
|
||||
*/
|
||||
sealed class UserDraft(open val text: String) {
|
||||
data class REGULAR(override val text: String) : UserDraft(text)
|
||||
data class QUOTE(val linkedEventId: String, override val text: String) : UserDraft(text)
|
||||
data class EDIT(val linkedEventId: String, override val text: String) : UserDraft(text)
|
||||
data class REPLY(val linkedEventId: String, override val text: String) : UserDraft(text)
|
||||
|
||||
fun isValid(): Boolean {
|
||||
return when (this) {
|
||||
is REGULAR -> text.isNotBlank()
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
}
|
@@ -17,6 +17,7 @@
|
||||
package im.vector.matrix.android.api.session.room.state
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
|
||||
interface StateService {
|
||||
|
||||
@@ -25,4 +26,6 @@ interface StateService {
|
||||
*/
|
||||
fun updateTopic(topic: String, callback: MatrixCallback<Unit>)
|
||||
|
||||
fun getStateEvent(eventType: String): Event?
|
||||
|
||||
}
|
@@ -56,6 +56,9 @@ interface Timeline {
|
||||
*/
|
||||
fun paginate(direction: Direction, count: Int)
|
||||
|
||||
fun pendingEventCount() : Int
|
||||
|
||||
fun failedToDeliverEventCount() : Int
|
||||
|
||||
interface Listener {
|
||||
/**
|
||||
|
@@ -18,11 +18,12 @@ package im.vector.matrix.android.api.session.room.timeline
|
||||
|
||||
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.RelationType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
||||
import im.vector.matrix.android.api.session.room.model.ReadReceipt
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.isReply
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply
|
||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||
|
||||
@@ -38,8 +39,8 @@ data class TimelineEvent(
|
||||
val senderName: String?,
|
||||
val isUniqueDisplayName: Boolean,
|
||||
val senderAvatar: String?,
|
||||
val sendState: SendState,
|
||||
val annotations: EventAnnotationsSummary? = null
|
||||
val annotations: EventAnnotationsSummary? = null,
|
||||
val readReceipts: List<ReadReceipt> = emptyList()
|
||||
) {
|
||||
|
||||
val metadata = HashMap<String, Any>()
|
||||
@@ -67,8 +68,8 @@ data class TimelineEvent(
|
||||
"$name (${root.senderId})"
|
||||
}
|
||||
}
|
||||
?: root.senderId
|
||||
?: ""
|
||||
?: root.senderId
|
||||
?: ""
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,11 +87,38 @@ data class TimelineEvent(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tells if the event has been edited
|
||||
*/
|
||||
fun TimelineEvent.hasBeenEdited() = annotations?.editSummary != null
|
||||
|
||||
/**
|
||||
* Get the eventId which was edited by this event if any
|
||||
*/
|
||||
fun TimelineEvent.getEditedEventId(): String? {
|
||||
return root.getClearContent().toModel<MessageContent>()?.relatesTo?.takeIf { it.type == RelationType.REPLACE }?.eventId
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last MessageContent, after a possible edition
|
||||
*/
|
||||
fun TimelineEvent.getLastMessageContent(): MessageContent? = annotations?.editSummary?.aggregatedContent?.toModel()
|
||||
?: root.getClearContent().toModel()
|
||||
?: root.getClearContent().toModel()
|
||||
|
||||
|
||||
/**
|
||||
* Get last Message body, after a possible edition
|
||||
*/
|
||||
fun TimelineEvent.getLastMessageBody(): String? {
|
||||
val lastMessageContent = getLastMessageContent()
|
||||
|
||||
if (lastMessageContent != null) {
|
||||
return lastMessageContent.newContent?.toModel<MessageContent>()?.body ?: lastMessageContent.body
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
fun TimelineEvent.getTextEditableContent(): String? {
|
||||
|
@@ -25,12 +25,12 @@ interface TimelineService {
|
||||
|
||||
/**
|
||||
* Instantiate a [Timeline] with an optional initial eventId, to be used with permalink.
|
||||
* You can filter the type you want to grab with the allowedTypes param.
|
||||
* You can also configure some settings with the [settings] param.
|
||||
* @param eventId the optional initial eventId.
|
||||
* @param allowedTypes the optional filter types
|
||||
* @param settings settings to configure the timeline.
|
||||
* @return the instantiated timeline
|
||||
*/
|
||||
fun createTimeline(eventId: String?, allowedTypes: List<String>? = null): Timeline
|
||||
fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline
|
||||
|
||||
|
||||
fun getTimeLineEvent(eventId: String): TimelineEvent?
|
||||
|
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.room.timeline
|
||||
|
||||
/**
|
||||
* Data class holding setting values for a [Timeline] instance.
|
||||
*/
|
||||
data class TimelineSettings(
|
||||
/**
|
||||
* The initial number of events to retrieve from cache. You might get less events if you don't have loaded enough yet.
|
||||
*/
|
||||
val initialSize: Int,
|
||||
/**
|
||||
* A flag to filter edit events
|
||||
*/
|
||||
val filterEdits: Boolean = false,
|
||||
/**
|
||||
* A flag to filter by types. It should be used with [allowedTypes] field
|
||||
*/
|
||||
val filterTypes: Boolean = false,
|
||||
/**
|
||||
* If [filterTypes] is true, the list of types allowed by the list.
|
||||
*/
|
||||
val allowedTypes: List<String> = emptyList(),
|
||||
/**
|
||||
* If true, will build read receipts for each event.
|
||||
*/
|
||||
val buildReadReceipts: Boolean = true
|
||||
|
||||
)
|
@@ -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.api.session.securestorage
|
||||
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
interface SecureStorageService {
|
||||
|
||||
fun securelyStoreObject(any: Any, keyAlias: String, outputStream: OutputStream)
|
||||
|
||||
fun <T> loadSecureSecret(inputStream: InputStream, keyAlias: String): T?
|
||||
|
||||
}
|
@@ -22,4 +22,5 @@ sealed class SyncState {
|
||||
object PAUSED : SyncState()
|
||||
object KILLING : SyncState()
|
||||
object KILLED : SyncState()
|
||||
object NO_NETWORK : SyncState()
|
||||
}
|
@@ -17,7 +17,10 @@
|
||||
package im.vector.matrix.android.api.session.user
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.paging.PagedList
|
||||
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
|
||||
|
||||
/**
|
||||
* This interface defines methods to get users. It's implemented at the session level.
|
||||
@@ -31,11 +34,34 @@ interface UserService {
|
||||
*/
|
||||
fun getUser(userId: String): User?
|
||||
|
||||
/**
|
||||
* Search list of users on server directory.
|
||||
* @param search the searched term
|
||||
* @param limit the max number of users to return
|
||||
* @param excludedUserIds the user ids to filter from the search
|
||||
* @param callback the async callback
|
||||
* @return Cancelable
|
||||
*/
|
||||
fun searchUsersDirectory(search: String, limit: Int, excludedUserIds: Set<String>, callback: MatrixCallback<List<User>>): Cancelable
|
||||
|
||||
/**
|
||||
* Observe a live user from a userId
|
||||
* @param userId the userId to look for.
|
||||
* @return a Livedata of user with userId
|
||||
*/
|
||||
fun observeUser(userId: String): LiveData<User?>
|
||||
fun liveUser(userId: String): LiveData<User?>
|
||||
|
||||
/**
|
||||
* Observe a live list of users sorted alphabetically
|
||||
* @return a Livedata of users
|
||||
*/
|
||||
fun liveUsers(): LiveData<List<User>>
|
||||
|
||||
/**
|
||||
* Observe a live [PagedList] of users sorted alphabetically. You can filter the users.
|
||||
* @param filter the filter. It will look into userId and displayName.
|
||||
* @return a Livedata of users
|
||||
*/
|
||||
fun livePagedUsers(filter: String? = null): LiveData<PagedList<User>>
|
||||
|
||||
}
|
@@ -19,5 +19,6 @@ package im.vector.matrix.android.api.util
|
||||
class CancelableBag : Cancelable, MutableList<Cancelable> by ArrayList() {
|
||||
override fun cancel() {
|
||||
forEach { it.cancel() }
|
||||
clear()
|
||||
}
|
||||
}
|
||||
|
@@ -17,10 +17,12 @@
|
||||
package im.vector.matrix.android.internal.auth
|
||||
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
|
||||
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
|
||||
import im.vector.matrix.android.internal.network.NetworkConstants
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Headers
|
||||
import retrofit2.http.POST
|
||||
|
||||
@@ -29,6 +31,13 @@ import retrofit2.http.POST
|
||||
*/
|
||||
internal interface AuthAPI {
|
||||
|
||||
/**
|
||||
* Get the supported login flow
|
||||
* Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-login
|
||||
*/
|
||||
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login")
|
||||
fun getLoginFlows(): Call<LoginFlowResponse>
|
||||
|
||||
/**
|
||||
* Pass params to the server for the current login phase.
|
||||
* Set all the timeouts to 1 minute
|
||||
|
@@ -23,7 +23,7 @@ import dagger.Provides
|
||||
import im.vector.matrix.android.api.auth.Authenticator
|
||||
import im.vector.matrix.android.internal.auth.db.AuthRealmModule
|
||||
import im.vector.matrix.android.internal.auth.db.RealmSessionParamsStore
|
||||
import im.vector.matrix.android.internal.database.configureEncryption
|
||||
import im.vector.matrix.android.internal.database.RealmKeysUtils
|
||||
import im.vector.matrix.android.internal.di.AuthDatabase
|
||||
import io.realm.RealmConfiguration
|
||||
import java.io.File
|
||||
@@ -33,16 +33,21 @@ internal abstract class AuthModule {
|
||||
|
||||
@Module
|
||||
companion object {
|
||||
private const val DB_ALIAS = "matrix-sdk-auth"
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@AuthDatabase
|
||||
fun providesRealmConfiguration(context: Context): RealmConfiguration {
|
||||
fun providesRealmConfiguration(context: Context, realmKeysUtils: RealmKeysUtils): RealmConfiguration {
|
||||
val old = File(context.filesDir, "matrix-sdk-auth")
|
||||
if (old.exists()) {
|
||||
old.renameTo(File(context.filesDir, "matrix-sdk-auth.realm"))
|
||||
}
|
||||
|
||||
return RealmConfiguration.Builder()
|
||||
.configureEncryption("matrix-sdk-auth", context)
|
||||
.apply {
|
||||
realmKeysUtils.configureEncryption(this, DB_ALIAS)
|
||||
}
|
||||
.name("matrix-sdk-auth.realm")
|
||||
.modules(AuthRealmModule())
|
||||
.deleteRealmIfMigrationNeeded()
|
||||
|
@@ -25,6 +25,7 @@ import im.vector.matrix.android.api.auth.data.SessionParams
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
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.PasswordLoginParams
|
||||
import im.vector.matrix.android.internal.auth.data.ThreePidMedium
|
||||
import im.vector.matrix.android.internal.di.Unauthenticated
|
||||
@@ -62,17 +63,35 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated
|
||||
return sessionManager.getOrCreateSession(sessionParams)
|
||||
}
|
||||
|
||||
override fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResponse>): Cancelable {
|
||||
val job = GlobalScope.launch(coroutineDispatchers.main) {
|
||||
val result = runCatching {
|
||||
getLoginFlowInternal(homeServerConnectionConfig)
|
||||
}
|
||||
result.foldToCallback(callback)
|
||||
}
|
||||
return CancelableCoroutine(job)
|
||||
}
|
||||
|
||||
override fun authenticate(homeServerConnectionConfig: HomeServerConnectionConfig,
|
||||
login: String,
|
||||
password: String,
|
||||
callback: MatrixCallback<Session>): Cancelable {
|
||||
|
||||
val job = GlobalScope.launch(coroutineDispatchers.main) {
|
||||
val sessionOrFailure = authenticate(homeServerConnectionConfig, login, password)
|
||||
val sessionOrFailure = runCatching {
|
||||
authenticate(homeServerConnectionConfig, login, password)
|
||||
}
|
||||
sessionOrFailure.foldToCallback(callback)
|
||||
}
|
||||
return CancelableCoroutine(job)
|
||||
}
|
||||
|
||||
private suspend fun getLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig) = withContext(coroutineDispatchers.io) {
|
||||
val authAPI = buildAuthAPI(homeServerConnectionConfig)
|
||||
|
||||
executeRequest<LoginFlowResponse> {
|
||||
apiCall = authAPI.getLoginFlows()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun authenticate(homeServerConnectionConfig: HomeServerConnectionConfig,
|
||||
@@ -85,16 +104,18 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated
|
||||
} else {
|
||||
PasswordLoginParams.userIdentifier(login, password, "Mobile")
|
||||
}
|
||||
executeRequest<Credentials> {
|
||||
val credentials = executeRequest<Credentials> {
|
||||
apiCall = authAPI.login(loginParams)
|
||||
}.map {
|
||||
val sessionParams = SessionParams(it, homeServerConnectionConfig)
|
||||
sessionParamsStore.save(sessionParams)
|
||||
sessionParams
|
||||
}.map {
|
||||
sessionManager.getOrCreateSession(it)
|
||||
}
|
||||
val sessionParams = SessionParams(credentials, homeServerConnectionConfig)
|
||||
sessionParamsStore.save(sessionParams)
|
||||
sessionManager.getOrCreateSession(sessionParams)
|
||||
}
|
||||
|
||||
override fun createSessionFromSso(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session {
|
||||
val sessionParams = SessionParams(credentials, homeServerConnectionConfig)
|
||||
sessionParamsStore.save(sessionParams)
|
||||
return sessionManager.getOrCreateSession(sessionParams)
|
||||
}
|
||||
|
||||
private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI {
|
||||
|
@@ -27,7 +27,7 @@ internal interface SessionParamsStore {
|
||||
|
||||
fun getAll(): List<SessionParams>
|
||||
|
||||
fun save(sessionParams: SessionParams): Try<SessionParams>
|
||||
fun save(sessionParams: SessionParams): Try<Unit>
|
||||
|
||||
fun delete(userId: String): Try<Unit>
|
||||
|
||||
|
@@ -30,4 +30,12 @@ data class InteractiveAuthenticationFlow(
|
||||
|
||||
@Json(name = "stages")
|
||||
val stages: List<String>? = null
|
||||
)
|
||||
) {
|
||||
|
||||
companion object {
|
||||
// Possible values for type
|
||||
const val TYPE_LOGIN_SSO = "m.login.sso"
|
||||
const val TYPE_LOGIN_TOKEN = "m.login.token"
|
||||
const val TYPE_LOGIN_PASSWORD = "m.login.password"
|
||||
}
|
||||
}
|
@@ -20,7 +20,7 @@ import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class LoginFlowResponse(
|
||||
data class LoginFlowResponse(
|
||||
@Json(name = "flows")
|
||||
val flows: List<InteractiveAuthenticationFlow>
|
||||
)
|
@@ -62,7 +62,7 @@ internal class RealmSessionParamsStore @Inject constructor(private val mapper: S
|
||||
return sessionParams
|
||||
}
|
||||
|
||||
override fun save(sessionParams: SessionParams): Try<SessionParams> {
|
||||
override fun save(sessionParams: SessionParams): Try<Unit> {
|
||||
return Try {
|
||||
val entity = mapper.map(sessionParams)
|
||||
if (entity != null) {
|
||||
@@ -72,7 +72,6 @@ internal class RealmSessionParamsStore @Inject constructor(private val mapper: S
|
||||
}
|
||||
realm.close()
|
||||
}
|
||||
sessionParams
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -20,7 +20,6 @@ import com.squareup.moshi.Moshi
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||
import im.vector.matrix.android.internal.di.MatrixScope
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
|
||||
|
@@ -16,7 +16,6 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import android.content.Context
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
@@ -30,12 +29,13 @@ 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.tasks.*
|
||||
import im.vector.matrix.android.internal.database.configureEncryption
|
||||
import im.vector.matrix.android.internal.database.RealmKeysUtils
|
||||
import im.vector.matrix.android.internal.di.CryptoDatabase
|
||||
import im.vector.matrix.android.internal.di.UserCacheDirectory
|
||||
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.android.internal.util.md5
|
||||
import io.realm.RealmConfiguration
|
||||
import retrofit2.Retrofit
|
||||
import java.io.File
|
||||
@@ -45,17 +45,20 @@ internal abstract class CryptoModule {
|
||||
|
||||
@Module
|
||||
companion object {
|
||||
internal const val DB_ALIAS_PREFIX = "crypto_module_"
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@CryptoDatabase
|
||||
@SessionScope
|
||||
fun providesRealmConfiguration(context: Context, credentials: Credentials): RealmConfiguration {
|
||||
val userIDHash = credentials.userId.md5()
|
||||
|
||||
fun providesRealmConfiguration(@UserCacheDirectory directory: File,
|
||||
@UserMd5 userMd5: String,
|
||||
realmKeysUtils: RealmKeysUtils): RealmConfiguration {
|
||||
return RealmConfiguration.Builder()
|
||||
.directory(File(context.filesDir, userIDHash))
|
||||
.configureEncryption("crypto_module_$userIDHash", context)
|
||||
.directory(directory)
|
||||
.apply {
|
||||
realmKeysUtils.configureEncryption(this, "$DB_ALIAS_PREFIX$userMd5")
|
||||
}
|
||||
.name("crypto_store.realm")
|
||||
.modules(RealmCryptoStoreModule())
|
||||
.schemaVersion(RealmCryptoStoreMigration.CRYPTO_STORE_SCHEMA_VERSION)
|
||||
@@ -105,7 +108,7 @@ internal abstract class CryptoModule {
|
||||
}
|
||||
|
||||
@Binds
|
||||
abstract fun bindCryptoService(cryptoManager: CryptoManager): CryptoService
|
||||
abstract fun bindCryptoService(cryptoService: DefaultCryptoService): CryptoService
|
||||
|
||||
@Binds
|
||||
abstract fun bindDeleteDeviceTask(deleteDeviceTask: DefaultDeleteDeviceTask): DeleteDeviceTask
|
||||
|
@@ -62,17 +62,14 @@ import im.vector.matrix.android.internal.crypto.tasks.*
|
||||
import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.di.CryptoDatabase
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import im.vector.matrix.android.internal.extensions.foldToCallback
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.session.cache.ClearCacheTask
|
||||
import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask
|
||||
import im.vector.matrix.android.internal.session.room.membership.RoomMembers
|
||||
import im.vector.matrix.android.internal.session.sync.model.SyncResponse
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import im.vector.matrix.android.internal.task.toConfigurableTask
|
||||
import im.vector.matrix.android.internal.util.JsonCanonicalizer
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import im.vector.matrix.android.internal.util.fetchCopied
|
||||
@@ -94,7 +91,7 @@ import kotlin.math.max
|
||||
* Specially, it tracks all room membership changes events in order to do keys updates.
|
||||
*/
|
||||
@SessionScope
|
||||
internal class CryptoManager @Inject constructor(
|
||||
internal class DefaultCryptoService @Inject constructor(
|
||||
// Olm Manager
|
||||
private val olmManager: OlmManager,
|
||||
// The credentials,
|
||||
@@ -136,7 +133,6 @@ internal class CryptoManager @Inject constructor(
|
||||
private val setDeviceNameTask: SetDeviceNameTask,
|
||||
private val uploadKeysTask: UploadKeysTask,
|
||||
private val loadRoomMembersTask: LoadRoomMembersTask,
|
||||
@CryptoDatabase private val clearCryptoDataTask: ClearCacheTask,
|
||||
private val monarchy: Monarchy,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val taskExecutor: TaskExecutor
|
||||
@@ -167,22 +163,25 @@ internal class CryptoManager @Inject constructor(
|
||||
|
||||
override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) {
|
||||
setDeviceNameTask
|
||||
.configureWith(SetDeviceNameTask.Params(deviceId, deviceName))
|
||||
.dispatchTo(callback)
|
||||
.configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun deleteDevice(deviceId: String, callback: MatrixCallback<Unit>) {
|
||||
deleteDeviceTask
|
||||
.configureWith(DeleteDeviceTask.Params(deviceId))
|
||||
.dispatchTo(callback)
|
||||
.configureWith(DeleteDeviceTask.Params(deviceId)) {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback<Unit>) {
|
||||
deleteDeviceWithUserPasswordTask
|
||||
.configureWith(DeleteDeviceWithUserPasswordTask.Params(deviceId, authSession, password))
|
||||
.dispatchTo(callback)
|
||||
.configureWith(DeleteDeviceWithUserPasswordTask.Params(deviceId, authSession, password)) {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
@@ -196,8 +195,9 @@ internal class CryptoManager @Inject constructor(
|
||||
|
||||
override fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) {
|
||||
getDevicesTask
|
||||
.toConfigurableTask()
|
||||
.dispatchTo(callback)
|
||||
.configureWith {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
@@ -254,35 +254,36 @@ internal class CryptoManager @Inject constructor(
|
||||
private suspend fun internalStart(isInitialSync: Boolean) {
|
||||
// Open the store
|
||||
cryptoStore.open()
|
||||
uploadDeviceKeys()
|
||||
.flatMap { oneTimeKeysUploader.maybeUploadOneTimeKeys() }
|
||||
.fold(
|
||||
{
|
||||
Timber.e("Start failed: $it")
|
||||
delay(1000)
|
||||
isStarting.set(false)
|
||||
internalStart(isInitialSync)
|
||||
},
|
||||
{
|
||||
outgoingRoomKeyRequestManager.start()
|
||||
keysBackup.checkAndStartKeysBackup()
|
||||
if (isInitialSync) {
|
||||
// refresh the devices list for each known room members
|
||||
deviceListManager.invalidateAllDeviceLists()
|
||||
deviceListManager.refreshOutdatedDeviceLists()
|
||||
} else {
|
||||
incomingRoomKeyRequestManager.processReceivedRoomKeyRequests()
|
||||
}
|
||||
isStarting.set(false)
|
||||
isStarted.set(true)
|
||||
}
|
||||
)
|
||||
runCatching {
|
||||
uploadDeviceKeys()
|
||||
oneTimeKeysUploader.maybeUploadOneTimeKeys()
|
||||
outgoingRoomKeyRequestManager.start()
|
||||
keysBackup.checkAndStartKeysBackup()
|
||||
if (isInitialSync) {
|
||||
// refresh the devices list for each known room members
|
||||
deviceListManager.invalidateAllDeviceLists()
|
||||
deviceListManager.refreshOutdatedDeviceLists()
|
||||
} else {
|
||||
incomingRoomKeyRequestManager.processReceivedRoomKeyRequests()
|
||||
}
|
||||
}.fold(
|
||||
{
|
||||
isStarting.set(false)
|
||||
isStarted.set(true)
|
||||
},
|
||||
{
|
||||
Timber.e("Start failed: $it")
|
||||
delay(1000)
|
||||
isStarting.set(false)
|
||||
internalStart(isInitialSync)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the crypto
|
||||
*/
|
||||
fun close() {
|
||||
fun close() = runBlocking(coroutineDispatchers.crypto) {
|
||||
olmDevice.release()
|
||||
cryptoStore.close()
|
||||
outgoingRoomKeyRequestManager.stop()
|
||||
@@ -556,13 +557,16 @@ internal class CryptoManager @Inject constructor(
|
||||
if (safeAlgorithm != null) {
|
||||
val t0 = System.currentTimeMillis()
|
||||
Timber.v("## encryptEventContent() starts")
|
||||
safeAlgorithm.encryptEventContent(eventContent, eventType, userIds)
|
||||
runCatching {
|
||||
safeAlgorithm.encryptEventContent(eventContent, eventType, userIds)
|
||||
}
|
||||
.fold(
|
||||
{ callback.onFailure(it) },
|
||||
{
|
||||
Timber.v("## encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms")
|
||||
callback.onSuccess(MXEncryptEventContentResult(it, EventType.ENCRYPTED))
|
||||
}
|
||||
},
|
||||
{ callback.onFailure(it) }
|
||||
|
||||
)
|
||||
} else {
|
||||
val algorithm = getEncryptionAlgorithm(roomId)
|
||||
@@ -584,10 +588,7 @@ internal class CryptoManager @Inject constructor(
|
||||
@Throws(MXCryptoError::class)
|
||||
override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||
return runBlocking {
|
||||
internalDecryptEvent(event, timeline).fold(
|
||||
{ throw it },
|
||||
{ it }
|
||||
)
|
||||
internalDecryptEvent(event, timeline)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -600,8 +601,10 @@ internal class CryptoManager @Inject constructor(
|
||||
*/
|
||||
override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) {
|
||||
GlobalScope.launch {
|
||||
val result = withContext(coroutineDispatchers.crypto) {
|
||||
internalDecryptEvent(event, timeline)
|
||||
val result = runCatching {
|
||||
withContext(coroutineDispatchers.crypto) {
|
||||
internalDecryptEvent(event, timeline)
|
||||
}
|
||||
}
|
||||
result.foldToCallback(callback)
|
||||
}
|
||||
@@ -612,22 +615,22 @@ internal class CryptoManager @Inject constructor(
|
||||
*
|
||||
* @param event the raw event.
|
||||
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
|
||||
* @return the MXEventDecryptionResult data, or null in case of error wrapped into [Try]
|
||||
* @return the MXEventDecryptionResult data, or null in case of error
|
||||
*/
|
||||
private suspend fun internalDecryptEvent(event: Event, timeline: String): Try<MXEventDecryptionResult> {
|
||||
private suspend fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||
val eventContent = event.content
|
||||
return if (eventContent == null) {
|
||||
if (eventContent == null) {
|
||||
Timber.e("## decryptEvent : empty event content")
|
||||
Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON))
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
|
||||
} else {
|
||||
val algorithm = eventContent["algorithm"]?.toString()
|
||||
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)
|
||||
if (alg == null) {
|
||||
val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm)
|
||||
Timber.e("## decryptEvent() : $reason")
|
||||
Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason))
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason)
|
||||
} else {
|
||||
alg.decryptEvent(event, timeline)
|
||||
return alg.decryptEvent(event, timeline)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -689,12 +692,13 @@ internal class CryptoManager @Inject constructor(
|
||||
private fun onRoomEncryptionEvent(roomId: String, event: Event) {
|
||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||
val params = LoadRoomMembersTask.Params(roomId)
|
||||
loadRoomMembersTask
|
||||
.execute(params)
|
||||
.map { _ ->
|
||||
val userIds = getRoomUserIds(roomId)
|
||||
setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds)
|
||||
}
|
||||
try {
|
||||
loadRoomMembersTask.execute(params)
|
||||
val userIds = getRoomUserIds(roomId)
|
||||
setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds)
|
||||
} catch (throwable: Throwable) {
|
||||
Timber.e(throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -761,7 +765,7 @@ internal class CryptoManager @Inject constructor(
|
||||
/**
|
||||
* Upload my user's device keys.
|
||||
*/
|
||||
private suspend fun uploadDeviceKeys(): Try<KeysUploadResponse> {
|
||||
private suspend fun uploadDeviceKeys(): KeysUploadResponse {
|
||||
// Prepare the device keys data to send
|
||||
// Sign it
|
||||
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, getMyDevice().signalableJSONDictionary())
|
||||
@@ -868,10 +872,8 @@ internal class CryptoManager @Inject constructor(
|
||||
fun checkUnknownDevices(userIds: List<String>, callback: MatrixCallback<Unit>) {
|
||||
// force the refresh to ensure that the devices list is up-to-date
|
||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||
deviceListManager
|
||||
.downloadKeys(userIds, true)
|
||||
runCatching { deviceListManager.downloadKeys(userIds, true) }
|
||||
.fold(
|
||||
{ callback.onFailure(it) },
|
||||
{
|
||||
val unknownDevices = getUnknownDevices(it)
|
||||
if (unknownDevices.map.isEmpty()) {
|
||||
@@ -880,7 +882,8 @@ internal class CryptoManager @Inject constructor(
|
||||
// trigger an an unknown devices exception
|
||||
callback.onFailure(Failure.CryptoError(MXCryptoError.UnknownDevice(unknownDevices)))
|
||||
}
|
||||
}
|
||||
},
|
||||
{ callback.onFailure(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1035,19 +1038,12 @@ internal class CryptoManager @Inject constructor(
|
||||
|
||||
override fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>>) {
|
||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||
deviceListManager
|
||||
.downloadKeys(userIds, forceDownload)
|
||||
.foldToCallback(callback)
|
||||
runCatching {
|
||||
deviceListManager.downloadKeys(userIds, forceDownload)
|
||||
}.foldToCallback(callback)
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearCryptoCache(callback: MatrixCallback<Unit>) {
|
||||
clearCryptoDataTask
|
||||
.toConfigurableTask()
|
||||
.dispatchTo(callback)
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun addNewSessionListener(newSessionListener: NewSessionListener) {
|
||||
roomDecryptorProvider.addNewSessionListener(newSessionListener)
|
||||
}
|
||||
@@ -1060,6 +1056,6 @@ internal class CryptoManager @Inject constructor(
|
||||
* ========================================================================================== */
|
||||
|
||||
override fun toString(): String {
|
||||
return "CryptoManager of " + credentials.userId + " (" + credentials.deviceId + ")"
|
||||
return "DefaultCryptoService of " + credentials.userId + " (" + credentials.deviceId + ")"
|
||||
}
|
||||
}
|
@@ -18,14 +18,12 @@
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import android.text.TextUtils
|
||||
import arrow.core.Try
|
||||
import im.vector.matrix.android.api.MatrixPatterns
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DownloadKeysForUsersTask
|
||||
import im.vector.matrix.android.internal.extensions.onError
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
|
||||
import timber.log.Timber
|
||||
@@ -237,7 +235,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||
* @param forceDownload Always download the keys even if cached.
|
||||
* @param callback the asynchronous callback
|
||||
*/
|
||||
suspend fun downloadKeys(userIds: List<String>?, forceDownload: Boolean): Try<MXUsersDevicesMap<MXDeviceInfo>> {
|
||||
suspend fun downloadKeys(userIds: List<String>?, forceDownload: Boolean): MXUsersDevicesMap<MXDeviceInfo> {
|
||||
Timber.v("## downloadKeys() : forceDownload $forceDownload : $userIds")
|
||||
// Map from userId -> deviceId -> MXDeviceInfo
|
||||
val stored = MXUsersDevicesMap<MXDeviceInfo>()
|
||||
@@ -268,16 +266,15 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||
}
|
||||
return if (downloadUsers.isEmpty()) {
|
||||
Timber.v("## downloadKeys() : no new user device")
|
||||
Try.just(stored)
|
||||
stored
|
||||
} else {
|
||||
Timber.v("## downloadKeys() : starts")
|
||||
val t0 = System.currentTimeMillis()
|
||||
doKeyDownloadForUsers(downloadUsers)
|
||||
.map {
|
||||
Timber.v("## downloadKeys() : doKeyDownloadForUsers succeeds after " + (System.currentTimeMillis() - t0) + " ms")
|
||||
it.addEntriesFromMap(stored)
|
||||
it
|
||||
}
|
||||
val result = doKeyDownloadForUsers(downloadUsers)
|
||||
Timber.v("## downloadKeys() : doKeyDownloadForUsers succeeds after " + (System.currentTimeMillis() - t0) + " ms")
|
||||
result.also {
|
||||
it.addEntriesFromMap(stored)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,60 +283,60 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||
*
|
||||
* @param downloadUsers the user ids list
|
||||
*/
|
||||
private suspend fun doKeyDownloadForUsers(downloadUsers: MutableList<String>): Try<MXUsersDevicesMap<MXDeviceInfo>> {
|
||||
private suspend fun doKeyDownloadForUsers(downloadUsers: MutableList<String>): MXUsersDevicesMap<MXDeviceInfo> {
|
||||
Timber.v("## doKeyDownloadForUsers() : doKeyDownloadForUsers $downloadUsers")
|
||||
// get the user ids which did not already trigger a keys download
|
||||
val filteredUsers = downloadUsers.filter { MatrixPatterns.isUserId(it) }
|
||||
if (filteredUsers.isEmpty()) {
|
||||
// trigger nothing
|
||||
return Try.just(MXUsersDevicesMap())
|
||||
return MXUsersDevicesMap()
|
||||
}
|
||||
val params = DownloadKeysForUsersTask.Params(filteredUsers, syncTokenStore.getLastToken())
|
||||
return downloadKeysForUsersTask.execute(params)
|
||||
.map { response ->
|
||||
Timber.v("## doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users")
|
||||
for (userId in filteredUsers) {
|
||||
val devices = response.deviceKeys?.get(userId)
|
||||
Timber.v("## doKeyDownloadForUsers() : Got keys for $userId : $devices")
|
||||
if (devices != null) {
|
||||
val mutableDevices = HashMap(devices)
|
||||
val deviceIds = ArrayList(mutableDevices.keys)
|
||||
for (deviceId in deviceIds) {
|
||||
// Get the potential previously store device keys for this device
|
||||
val previouslyStoredDeviceKeys = cryptoStore.getUserDevice(deviceId, userId)
|
||||
val deviceInfo = mutableDevices[deviceId]
|
||||
val response = try {
|
||||
downloadKeysForUsersTask.execute(params)
|
||||
} catch (throwable: Throwable) {
|
||||
Timber.e(throwable, "##doKeyDownloadForUsers(): error")
|
||||
onKeysDownloadFailed(filteredUsers)
|
||||
throw throwable
|
||||
}
|
||||
Timber.v("## doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users")
|
||||
for (userId in filteredUsers) {
|
||||
val devices = response.deviceKeys?.get(userId)
|
||||
Timber.v("## doKeyDownloadForUsers() : Got keys for $userId : $devices")
|
||||
if (devices != null) {
|
||||
val mutableDevices = HashMap(devices)
|
||||
val deviceIds = ArrayList(mutableDevices.keys)
|
||||
for (deviceId in deviceIds) {
|
||||
// Get the potential previously store device keys for this device
|
||||
val previouslyStoredDeviceKeys = cryptoStore.getUserDevice(deviceId, userId)
|
||||
val deviceInfo = mutableDevices[deviceId]
|
||||
|
||||
// in some race conditions (like unit tests)
|
||||
// the self device must be seen as verified
|
||||
if (TextUtils.equals(deviceInfo!!.deviceId, credentials.deviceId) && TextUtils.equals(userId, credentials.userId)) {
|
||||
deviceInfo.verified = MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED
|
||||
}
|
||||
// Validate received keys
|
||||
if (!validateDeviceKeys(deviceInfo, userId, deviceId, previouslyStoredDeviceKeys)) {
|
||||
// New device keys are not valid. Do not store them
|
||||
mutableDevices.remove(deviceId)
|
||||
if (null != previouslyStoredDeviceKeys) {
|
||||
// But keep old validated ones if any
|
||||
mutableDevices[deviceId] = previouslyStoredDeviceKeys
|
||||
}
|
||||
} else if (null != previouslyStoredDeviceKeys) {
|
||||
// The verified status is not sync'ed with hs.
|
||||
// This is a client side information, valid only for this client.
|
||||
// So, transfer its previous value
|
||||
mutableDevices[deviceId]!!.verified = previouslyStoredDeviceKeys.verified
|
||||
}
|
||||
}
|
||||
// Update the store
|
||||
// Note that devices which aren't in the response will be removed from the stores
|
||||
cryptoStore.storeUserDevices(userId, mutableDevices)
|
||||
}
|
||||
// in some race conditions (like unit tests)
|
||||
// the self device must be seen as verified
|
||||
if (TextUtils.equals(deviceInfo!!.deviceId, credentials.deviceId) && TextUtils.equals(userId, credentials.userId)) {
|
||||
deviceInfo.verified = MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED
|
||||
}
|
||||
// Validate received keys
|
||||
if (!validateDeviceKeys(deviceInfo, userId, deviceId, previouslyStoredDeviceKeys)) {
|
||||
// New device keys are not valid. Do not store them
|
||||
mutableDevices.remove(deviceId)
|
||||
if (null != previouslyStoredDeviceKeys) {
|
||||
// But keep old validated ones if any
|
||||
mutableDevices[deviceId] = previouslyStoredDeviceKeys
|
||||
}
|
||||
} else if (null != previouslyStoredDeviceKeys) {
|
||||
// The verified status is not sync'ed with hs.
|
||||
// This is a client side information, valid only for this client.
|
||||
// So, transfer its previous value
|
||||
mutableDevices[deviceId]!!.verified = previouslyStoredDeviceKeys.verified
|
||||
}
|
||||
onKeysDownloadSucceed(filteredUsers, response.failures)
|
||||
}
|
||||
.onError {
|
||||
Timber.e(it, "##doKeyDownloadForUsers(): error")
|
||||
onKeysDownloadFailed(filteredUsers)
|
||||
}
|
||||
// Update the store
|
||||
// Note that devices which aren't in the response will be removed from the stores
|
||||
cryptoStore.storeUserDevices(userId, mutableDevices)
|
||||
}
|
||||
}
|
||||
return onKeysDownloadSucceed(filteredUsers, response.failures)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -465,15 +462,16 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||
}
|
||||
|
||||
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
||||
doKeyDownloadForUsers(users)
|
||||
.fold(
|
||||
{
|
||||
Timber.e(it, "## refreshOutdatedDeviceLists() : ERROR updating device keys for users $users")
|
||||
},
|
||||
{
|
||||
Timber.v("## refreshOutdatedDeviceLists() : done")
|
||||
}
|
||||
)
|
||||
runCatching {
|
||||
doKeyDownloadForUsers(users)
|
||||
}.fold(
|
||||
{
|
||||
Timber.v("## refreshOutdatedDeviceLists() : done")
|
||||
},
|
||||
{
|
||||
Timber.e(it, "## refreshOutdatedDeviceLists() : ERROR updating device keys for users $users")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@@ -18,7 +18,6 @@
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import android.text.TextUtils
|
||||
import arrow.core.Try
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.util.JSON_DICT_PARAMETERIZED_TYPE
|
||||
import im.vector.matrix.android.api.util.JsonDict
|
||||
@@ -80,8 +79,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||
//
|
||||
// The first level keys are timeline ids.
|
||||
// The second level keys are strings of form "<senderKey>|<session_id>|<message_index>"
|
||||
// Values are true.
|
||||
private val inboundGroupSessionMessageIndexes: MutableMap<String, MutableMap<String, Boolean>> = HashMap()
|
||||
private val inboundGroupSessionMessageIndexes: MutableMap<String, MutableSet<String>> = HashMap()
|
||||
|
||||
init {
|
||||
// Retrieve the account from the store
|
||||
@@ -506,25 +504,25 @@ internal class MXOlmDevice @Inject constructor(
|
||||
keysClaimed: Map<String, String>,
|
||||
exportFormat: Boolean): Boolean {
|
||||
val session = OlmInboundGroupSessionWrapper(sessionKey, exportFormat)
|
||||
runCatching { getInboundGroupSession(sessionId, senderKey, roomId) }
|
||||
.fold(
|
||||
{
|
||||
// If we already have this session, consider updating it
|
||||
Timber.e("## addInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
|
||||
|
||||
getInboundGroupSession(sessionId, senderKey, roomId).fold(
|
||||
{
|
||||
// Nothing to do in case of error
|
||||
},
|
||||
{
|
||||
// If we already have this session, consider updating it
|
||||
Timber.e("## addInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
|
||||
val existingFirstKnown = it.firstKnownIndex!!
|
||||
val newKnownFirstIndex = session.firstKnownIndex
|
||||
|
||||
val existingFirstKnown = it.firstKnownIndex!!
|
||||
val newKnownFirstIndex = session.firstKnownIndex
|
||||
|
||||
//If our existing session is better we keep it
|
||||
if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) {
|
||||
session.olmInboundGroupSession?.releaseSession()
|
||||
return false
|
||||
}
|
||||
}
|
||||
)
|
||||
//If our existing session is better we keep it
|
||||
if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) {
|
||||
session.olmInboundGroupSession?.releaseSession()
|
||||
return false
|
||||
}
|
||||
},
|
||||
{
|
||||
// Nothing to do in case of error
|
||||
}
|
||||
)
|
||||
|
||||
// sanity check
|
||||
if (null == session.olmInboundGroupSession) {
|
||||
@@ -595,12 +593,8 @@ internal class MXOlmDevice @Inject constructor(
|
||||
continue
|
||||
}
|
||||
|
||||
getInboundGroupSession(sessionId, senderKey, roomId)
|
||||
runCatching { getInboundGroupSession(sessionId, senderKey, roomId) }
|
||||
.fold(
|
||||
{
|
||||
// Session does not already exist, add it
|
||||
sessions.add(session)
|
||||
},
|
||||
{
|
||||
// If we already have this session, consider updating it
|
||||
Timber.e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
|
||||
@@ -613,7 +607,12 @@ internal class MXOlmDevice @Inject constructor(
|
||||
sessions.add(session)
|
||||
}
|
||||
Unit
|
||||
},
|
||||
{
|
||||
// Session does not already exist, add it
|
||||
sessions.add(session)
|
||||
}
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
@@ -648,61 +647,55 @@ internal class MXOlmDevice @Inject constructor(
|
||||
roomId: String,
|
||||
timeline: String?,
|
||||
sessionId: String,
|
||||
senderKey: String): Try<OlmDecryptionResult> {
|
||||
return getInboundGroupSession(sessionId, senderKey, roomId)
|
||||
.flatMap { session ->
|
||||
// Check that the room id matches the original one for the session. This stops
|
||||
// the HS pretending a message was targeting a different room.
|
||||
if (roomId == session.roomId) {
|
||||
var decryptResult: OlmInboundGroupSession.DecryptMessageResult? = null
|
||||
try {
|
||||
decryptResult = session.olmInboundGroupSession!!.decryptMessage(body)
|
||||
} catch (e: OlmException) {
|
||||
Timber.e(e, "## decryptGroupMessage () : decryptMessage failed")
|
||||
return@flatMap Try.Failure(MXCryptoError.OlmError(e))
|
||||
}
|
||||
senderKey: String): OlmDecryptionResult {
|
||||
val session = getInboundGroupSession(sessionId, senderKey, roomId)
|
||||
// Check that the room id matches the original one for the session. This stops
|
||||
// the HS pretending a message was targeting a different room.
|
||||
if (roomId == session.roomId) {
|
||||
var decryptResult: OlmInboundGroupSession.DecryptMessageResult? = null
|
||||
try {
|
||||
decryptResult = session.olmInboundGroupSession!!.decryptMessage(body)
|
||||
} catch (e: OlmException) {
|
||||
Timber.e(e, "## decryptGroupMessage () : decryptMessage failed")
|
||||
throw MXCryptoError.OlmError(e)
|
||||
}
|
||||
|
||||
if (null != timeline) {
|
||||
if (!inboundGroupSessionMessageIndexes.containsKey(timeline)) {
|
||||
inboundGroupSessionMessageIndexes[timeline] = HashMap()
|
||||
}
|
||||
if (null != timeline) {
|
||||
val timelineSet = inboundGroupSessionMessageIndexes.getOrPut(timeline) { mutableSetOf() }
|
||||
|
||||
val messageIndexKey = senderKey + "|" + sessionId + "|" + decryptResult.mIndex
|
||||
val messageIndexKey = senderKey + "|" + sessionId + "|" + decryptResult.mIndex
|
||||
|
||||
if (inboundGroupSessionMessageIndexes[timeline]?.get(messageIndexKey) != null) {
|
||||
val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex)
|
||||
Timber.e("## decryptGroupMessage() : $reason")
|
||||
return@flatMap Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason))
|
||||
}
|
||||
|
||||
inboundGroupSessionMessageIndexes[timeline]!!.put(messageIndexKey, true)
|
||||
}
|
||||
|
||||
store.storeInboundGroupSessions(listOf(session))
|
||||
val payload = try {
|
||||
val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
|
||||
val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage)
|
||||
adapter.fromJson(payloadString)
|
||||
} catch (e: Exception) {
|
||||
Timber.e("## decryptGroupMessage() : fails to parse the payload")
|
||||
return@flatMap Try.Failure(
|
||||
MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON))
|
||||
}
|
||||
|
||||
return@flatMap Try.just(
|
||||
OlmDecryptionResult(
|
||||
payload,
|
||||
session.keysClaimed,
|
||||
senderKey,
|
||||
session.forwardingCurve25519KeyChain
|
||||
)
|
||||
)
|
||||
} else {
|
||||
val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId)
|
||||
Timber.e("## decryptGroupMessage() : $reason")
|
||||
return@flatMap Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, reason))
|
||||
}
|
||||
if (timelineSet.contains(messageIndexKey)) {
|
||||
val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex)
|
||||
Timber.e("## decryptGroupMessage() : $reason")
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason)
|
||||
}
|
||||
|
||||
timelineSet.add(messageIndexKey)
|
||||
}
|
||||
|
||||
store.storeInboundGroupSessions(listOf(session))
|
||||
val payload = try {
|
||||
val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
|
||||
val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage)
|
||||
adapter.fromJson(payloadString)
|
||||
} catch (e: Exception) {
|
||||
Timber.e("## decryptGroupMessage() : fails to parse the payload")
|
||||
throw
|
||||
MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)
|
||||
}
|
||||
|
||||
return OlmDecryptionResult(
|
||||
payload,
|
||||
session.keysClaimed,
|
||||
senderKey,
|
||||
session.forwardingCurve25519KeyChain
|
||||
)
|
||||
} else {
|
||||
val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId)
|
||||
Timber.e("## decryptGroupMessage() : $reason")
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, reason)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -766,26 +759,26 @@ internal class MXOlmDevice @Inject constructor(
|
||||
* @param senderKey the base64-encoded curve25519 key of the sender.
|
||||
* @return the inbound group session.
|
||||
*/
|
||||
fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): Try<OlmInboundGroupSessionWrapper> {
|
||||
fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): OlmInboundGroupSessionWrapper {
|
||||
if (sessionId.isNullOrBlank() || senderKey.isNullOrBlank()) {
|
||||
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON))
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON)
|
||||
}
|
||||
|
||||
val session = store.getInboundGroupSession(sessionId, senderKey)
|
||||
|
||||
return if (null != session) {
|
||||
if (session != null) {
|
||||
// Check that the room id matches the original one for the session. This stops
|
||||
// the HS pretending a message was targeting a different room.
|
||||
if (!TextUtils.equals(roomId, session.roomId)) {
|
||||
val errorDescription = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId)
|
||||
Timber.e("## getInboundGroupSession() : $errorDescription")
|
||||
Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, errorDescription))
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, errorDescription)
|
||||
} else {
|
||||
Try.just(session)
|
||||
return session
|
||||
}
|
||||
} else {
|
||||
Timber.e("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId")
|
||||
Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON))
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -798,6 +791,6 @@ internal class MXOlmDevice @Inject constructor(
|
||||
* @return true if the unbound session keys are known.
|
||||
*/
|
||||
fun hasInboundSessionKeys(roomId: String, senderKey: String, sessionId: String): Boolean {
|
||||
return getInboundGroupSession(sessionId, senderKey, roomId).isSuccess()
|
||||
return runCatching { getInboundGroupSession(sessionId, senderKey, roomId) }.isSuccess
|
||||
}
|
||||
}
|
||||
|
@@ -17,12 +17,10 @@
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class ObjectSigner @Inject constructor(private val credentials: Credentials,
|
||||
private val olmDevice: MXOlmDevice) {
|
||||
private val olmDevice: MXOlmDevice) {
|
||||
|
||||
/**
|
||||
* Sign Object
|
||||
|
@@ -16,8 +16,6 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import arrow.core.Try
|
||||
import arrow.instances.`try`.applicativeError.handleError
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.internal.crypto.model.MXKey
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse
|
||||
@@ -59,13 +57,13 @@ internal class OneTimeKeysUploader @Inject constructor(
|
||||
/**
|
||||
* Check if the OTK must be uploaded.
|
||||
*/
|
||||
suspend fun maybeUploadOneTimeKeys(): Try<Unit> {
|
||||
suspend fun maybeUploadOneTimeKeys() {
|
||||
if (oneTimeKeyCheckInProgress) {
|
||||
return Try.just(Unit)
|
||||
return
|
||||
}
|
||||
if (System.currentTimeMillis() - lastOneTimeKeyCheck < ONE_TIME_KEY_UPLOAD_PERIOD) {
|
||||
// we've done a key upload recently.
|
||||
return Try.just(Unit)
|
||||
return
|
||||
}
|
||||
|
||||
lastOneTimeKeyCheck = System.currentTimeMillis()
|
||||
@@ -81,41 +79,31 @@ internal class OneTimeKeysUploader @Inject constructor(
|
||||
// discard the oldest private keys first. This will eventually clean
|
||||
// out stale private keys that won't receive a message.
|
||||
val keyLimit = Math.floor(maxOneTimeKeys / 2.0).toInt()
|
||||
val result = if (oneTimeKeyCount != null) {
|
||||
if (oneTimeKeyCount != null) {
|
||||
uploadOTK(oneTimeKeyCount!!, keyLimit)
|
||||
} else {
|
||||
// ask the server how many keys we have
|
||||
val uploadKeysParams = UploadKeysTask.Params(null, null, credentials.deviceId!!)
|
||||
uploadKeysTask.execute(uploadKeysParams)
|
||||
.flatMap {
|
||||
// We need to keep a pool of one time public keys on the server so that
|
||||
// other devices can start conversations with us. But we can only store
|
||||
// a finite number of private keys in the olm Account object.
|
||||
// To complicate things further then can be a delay between a device
|
||||
// claiming a public one time key from the server and it sending us a
|
||||
// message. We need to keep the corresponding private key locally until
|
||||
// we receive the message.
|
||||
// But that message might never arrive leaving us stuck with duff
|
||||
// private keys clogging up our local storage.
|
||||
// So we need some kind of engineering compromise to balance all of
|
||||
// these factors.
|
||||
// TODO Why we do not set oneTimeKeyCount here?
|
||||
// TODO This is not needed anymore, see https://github.com/matrix-org/matrix-js-sdk/pull/493 (TODO on iOS also)
|
||||
val keyCount = it.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)
|
||||
uploadOTK(keyCount, keyLimit)
|
||||
}
|
||||
val response = uploadKeysTask.execute(uploadKeysParams)
|
||||
// We need to keep a pool of one time public keys on the server so that
|
||||
// other devices can start conversations with us. But we can only store
|
||||
// a finite number of private keys in the olm Account object.
|
||||
// To complicate things further then can be a delay between a device
|
||||
// claiming a public one time key from the server and it sending us a
|
||||
// message. We need to keep the corresponding private key locally until
|
||||
// we receive the message.
|
||||
// But that message might never arrive leaving us stuck with duff
|
||||
// private keys clogging up our local storage.
|
||||
// So we need some kind of engineering compromise to balance all of
|
||||
// these factors.
|
||||
// TODO Why we do not set oneTimeKeyCount here?
|
||||
// TODO This is not needed anymore, see https://github.com/matrix-org/matrix-js-sdk/pull/493 (TODO on iOS also)
|
||||
val keyCount = response.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)
|
||||
uploadOTK(keyCount, keyLimit)
|
||||
}
|
||||
return result
|
||||
.map {
|
||||
Timber.v("## uploadKeys() : success")
|
||||
oneTimeKeyCount = null
|
||||
oneTimeKeyCheckInProgress = false
|
||||
}
|
||||
.handleError {
|
||||
Timber.e(it, "## uploadKeys() : failed")
|
||||
oneTimeKeyCount = null
|
||||
oneTimeKeyCheckInProgress = false
|
||||
}
|
||||
Timber.v("## uploadKeys() : success")
|
||||
oneTimeKeyCount = null
|
||||
oneTimeKeyCheckInProgress = false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,29 +112,26 @@ internal class OneTimeKeysUploader @Inject constructor(
|
||||
* @param keyCount the key count
|
||||
* @param keyLimit the limit
|
||||
*/
|
||||
private suspend fun uploadOTK(keyCount: Int, keyLimit: Int): Try<Unit> {
|
||||
private suspend fun uploadOTK(keyCount: Int, keyLimit: Int) {
|
||||
if (keyLimit <= keyCount) {
|
||||
// If we don't need to generate any more keys then we are done.
|
||||
return Try.just(Unit)
|
||||
return
|
||||
}
|
||||
|
||||
val keysThisLoop = Math.min(keyLimit - keyCount, ONE_TIME_KEY_GENERATION_MAX_NUMBER)
|
||||
olmDevice.generateOneTimeKeys(keysThisLoop)
|
||||
return uploadOneTimeKeys()
|
||||
.flatMap {
|
||||
if (it.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) {
|
||||
uploadOTK(it.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE), keyLimit)
|
||||
} else {
|
||||
Timber.e("## uploadLoop() : response for uploading keys does not contain one_time_key_counts.signed_curve25519")
|
||||
Try.raise(Exception("response for uploading keys does not contain one_time_key_counts.signed_curve25519"))
|
||||
}
|
||||
}
|
||||
val response = uploadOneTimeKeys()
|
||||
if (response.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) {
|
||||
uploadOTK(response.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE), keyLimit)
|
||||
} else {
|
||||
Timber.e("## uploadLoop() : response for uploading keys does not contain one_time_key_counts.signed_curve25519")
|
||||
throw Exception("response for uploading keys does not contain one_time_key_counts.signed_curve25519")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload my user's one time keys.
|
||||
*/
|
||||
private suspend fun uploadOneTimeKeys(): Try<KeysUploadResponse> {
|
||||
private suspend fun uploadOneTimeKeys(): KeysUploadResponse {
|
||||
val oneTimeKeys = olmDevice.getOneTimeKeys()
|
||||
val oneTimeJson = HashMap<String, Any>()
|
||||
|
||||
@@ -169,13 +154,10 @@ internal class OneTimeKeysUploader @Inject constructor(
|
||||
// For now, we set the device id explicitly, as we may not be using the
|
||||
// same one as used in login.
|
||||
val uploadParams = UploadKeysTask.Params(null, oneTimeJson, credentials.deviceId!!)
|
||||
return uploadKeysTask
|
||||
.execute(uploadParams)
|
||||
.map {
|
||||
lastPublishedOneTimeKeys = oneTimeKeys
|
||||
olmDevice.markKeysAsPublished()
|
||||
it
|
||||
}
|
||||
val response = uploadKeysTask.execute(uploadParams)
|
||||
lastPublishedOneTimeKeys = oneTimeKeys
|
||||
olmDevice.markKeysAsPublished()
|
||||
return response
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@@ -299,10 +299,12 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor(
|
||||
// TODO Change this two hard coded key to something better
|
||||
contentMap.setObject(recipient["userId"], recipient["deviceId"], message)
|
||||
}
|
||||
sendToDeviceTask.configureWith(SendToDeviceTask.Params(EventType.ROOM_KEY_REQUEST, contentMap, transactionId))
|
||||
.dispatchTo(callback)
|
||||
.executeOn(TaskThread.CALLER)
|
||||
.callbackOn(TaskThread.CALLER)
|
||||
sendToDeviceTask
|
||||
.configureWith(SendToDeviceTask.Params(EventType.ROOM_KEY_REQUEST, contentMap, transactionId)) {
|
||||
this.callback = callback
|
||||
this.callbackThread = TaskThread.CALLER
|
||||
this.executionThread = TaskThread.CALLER
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
|
@@ -17,7 +17,6 @@
|
||||
package im.vector.matrix.android.internal.crypto.actions
|
||||
|
||||
import android.text.TextUtils
|
||||
import arrow.core.Try
|
||||
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.MXKey
|
||||
@@ -32,7 +31,7 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val
|
||||
private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask) {
|
||||
|
||||
|
||||
suspend fun handle(devicesByUser: Map<String, List<MXDeviceInfo>>): Try<MXUsersDevicesMap<MXOlmSessionResult>> {
|
||||
suspend fun handle(devicesByUser: Map<String, List<MXDeviceInfo>>): MXUsersDevicesMap<MXOlmSessionResult> {
|
||||
val devicesWithoutSession = ArrayList<MXDeviceInfo>()
|
||||
|
||||
val results = MXUsersDevicesMap<MXOlmSessionResult>()
|
||||
@@ -58,7 +57,7 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val
|
||||
}
|
||||
|
||||
if (devicesWithoutSession.size == 0) {
|
||||
return Try.just(results)
|
||||
return results
|
||||
}
|
||||
|
||||
// Prepare the request for claiming one-time keys
|
||||
@@ -79,39 +78,36 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val
|
||||
Timber.v("## claimOneTimeKeysForUsersDevices() : $usersDevicesToClaim")
|
||||
|
||||
val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim)
|
||||
return oneTimeKeysForUsersDeviceTask
|
||||
.execute(claimParams)
|
||||
.map {
|
||||
Timber.v("## claimOneTimeKeysForUsersDevices() : keysClaimResponse.oneTimeKeys: $it")
|
||||
for (userId in userIds) {
|
||||
val deviceInfos = devicesByUser[userId]
|
||||
for (deviceInfo in deviceInfos!!) {
|
||||
var oneTimeKey: MXKey? = null
|
||||
val deviceIds = it.getUserDeviceIds(userId)
|
||||
if (null != deviceIds) {
|
||||
for (deviceId in deviceIds) {
|
||||
val olmSessionResult = results.getObject(userId, deviceId)
|
||||
if (olmSessionResult!!.sessionId != null) {
|
||||
// We already have a result for this device
|
||||
continue
|
||||
}
|
||||
val key = it.getObject(userId, deviceId)
|
||||
if (key?.type == oneTimeKeyAlgorithm) {
|
||||
oneTimeKey = key
|
||||
}
|
||||
if (oneTimeKey == null) {
|
||||
Timber.v("## ensureOlmSessionsForDevices() : No one-time keys " + oneTimeKeyAlgorithm
|
||||
+ " for device " + userId + " : " + deviceId)
|
||||
continue
|
||||
}
|
||||
// Update the result for this device in results
|
||||
olmSessionResult.sessionId = verifyKeyAndStartSession(oneTimeKey, userId, deviceInfo)
|
||||
}
|
||||
}
|
||||
val oneTimeKeys = oneTimeKeysForUsersDeviceTask.execute(claimParams)
|
||||
Timber.v("## claimOneTimeKeysForUsersDevices() : keysClaimResponse.oneTimeKeys: $oneTimeKeys")
|
||||
for (userId in userIds) {
|
||||
val deviceInfos = devicesByUser[userId]
|
||||
for (deviceInfo in deviceInfos!!) {
|
||||
var oneTimeKey: MXKey? = null
|
||||
val deviceIds = oneTimeKeys.getUserDeviceIds(userId)
|
||||
if (null != deviceIds) {
|
||||
for (deviceId in deviceIds) {
|
||||
val olmSessionResult = results.getObject(userId, deviceId)
|
||||
if (olmSessionResult!!.sessionId != null) {
|
||||
// We already have a result for this device
|
||||
continue
|
||||
}
|
||||
val key = oneTimeKeys.getObject(userId, deviceId)
|
||||
if (key?.type == oneTimeKeyAlgorithm) {
|
||||
oneTimeKey = key
|
||||
}
|
||||
if (oneTimeKey == null) {
|
||||
Timber.v("## ensureOlmSessionsForDevices() : No one-time keys " + oneTimeKeyAlgorithm
|
||||
+ " for device " + userId + " : " + deviceId)
|
||||
continue
|
||||
}
|
||||
// Update the result for this device in results
|
||||
olmSessionResult.sessionId = verifyKeyAndStartSession(oneTimeKey, userId, deviceInfo)
|
||||
}
|
||||
results
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
private fun verifyKeyAndStartSession(oneTimeKey: MXKey, userId: String, deviceInfo: MXDeviceInfo): String? {
|
||||
|
@@ -17,27 +17,24 @@
|
||||
package im.vector.matrix.android.internal.crypto.actions
|
||||
|
||||
import android.text.TextUtils
|
||||
import arrow.core.Try
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.MXOlmSessionResult
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class EnsureOlmSessionsForUsersAction @Inject constructor(private val olmDevice: MXOlmDevice,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction) {
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction) {
|
||||
|
||||
/**
|
||||
* Try to make sure we have established olm sessions for the given users.
|
||||
* @param users a list of user ids.
|
||||
*/
|
||||
suspend fun handle(users: List<String>) : Try<MXUsersDevicesMap<MXOlmSessionResult>> {
|
||||
suspend fun handle(users: List<String>): MXUsersDevicesMap<MXOlmSessionResult> {
|
||||
Timber.v("## ensureOlmSessionsForUsers() : ensureOlmSessionsForUsers $users")
|
||||
val devicesByUser = HashMap<String /* userId */, MutableList<MXDeviceInfo>>()
|
||||
|
||||
|
@@ -26,7 +26,6 @@ import im.vector.matrix.android.internal.crypto.RoomDecryptorProvider
|
||||
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@@ -16,16 +16,15 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.actions
|
||||
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class SetDeviceVerificationAction @Inject constructor(private val cryptoStore: IMXCryptoStore,
|
||||
private val credentials: Credentials,
|
||||
private val keysBackup: KeysBackup) {
|
||||
@UserId private val userId: String,
|
||||
private val keysBackup: KeysBackup) {
|
||||
|
||||
fun handle(verificationStatus: Int, deviceId: String, userId: String) {
|
||||
val device = cryptoStore.getUserDevice(deviceId, userId)
|
||||
@@ -40,7 +39,7 @@ internal class SetDeviceVerificationAction @Inject constructor(private val crypt
|
||||
device.verified = verificationStatus
|
||||
cryptoStore.storeUserDevice(userId, device)
|
||||
|
||||
if (userId == credentials.userId) {
|
||||
if (userId == this.userId) {
|
||||
// If one of the user's own devices is being marked as verified / unverified,
|
||||
// check the key backup status, since whether or not we use this depends on
|
||||
// whether it has a signature from a verified device
|
||||
|
@@ -17,7 +17,6 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.algorithms
|
||||
|
||||
import arrow.core.Try
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
||||
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
|
||||
@@ -35,7 +34,7 @@ internal interface IMXDecrypting {
|
||||
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
|
||||
* @return the decryption information, or an error
|
||||
*/
|
||||
suspend fun decryptEvent(event: Event, timeline: String): Try<MXEventDecryptionResult>
|
||||
suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult
|
||||
|
||||
/**
|
||||
* Handle a key event.
|
||||
|
@@ -17,7 +17,6 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.algorithms
|
||||
|
||||
import arrow.core.Try
|
||||
import im.vector.matrix.android.api.session.events.model.Content
|
||||
|
||||
/**
|
||||
@@ -31,7 +30,7 @@ internal interface IMXEncrypting {
|
||||
* @param eventContent the content of the event.
|
||||
* @param eventType the type of the event.
|
||||
* @param userIds the room members the event will be sent to.
|
||||
* @return the encrypted content wrapped by [Try]
|
||||
* @return the encrypted content
|
||||
*/
|
||||
suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List<String>): Try<Content>
|
||||
suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List<String>): Content
|
||||
}
|
||||
|
@@ -18,8 +18,6 @@
|
||||
package im.vector.matrix.android.internal.crypto.algorithms.megolm
|
||||
|
||||
import android.text.TextUtils
|
||||
import arrow.core.Try
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
@@ -40,9 +38,8 @@ import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
internal class MXMegolmDecryption(private val credentials: Credentials,
|
||||
internal class MXMegolmDecryption(private val userId: String,
|
||||
private val olmDevice: MXOlmDevice,
|
||||
private val deviceListManager: DeviceListManager,
|
||||
private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager,
|
||||
@@ -61,30 +58,46 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
|
||||
*/
|
||||
private var pendingEvents: MutableMap<String /* senderKey|sessionId */, MutableMap<String /* timelineId */, MutableList<Event>>> = HashMap()
|
||||
|
||||
override suspend fun decryptEvent(event: Event, timeline: String): Try<MXEventDecryptionResult> {
|
||||
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||
return decryptEvent(event, timeline, true)
|
||||
}
|
||||
|
||||
private fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): Try<MXEventDecryptionResult> {
|
||||
private suspend fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult {
|
||||
if (event.roomId.isNullOrBlank()) {
|
||||
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON))
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
|
||||
}
|
||||
|
||||
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
|
||||
?: return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON))
|
||||
?: throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
|
||||
|
||||
if (encryptedEventContent.senderKey.isNullOrBlank()
|
||||
|| encryptedEventContent.sessionId.isNullOrBlank()
|
||||
|| encryptedEventContent.ciphertext.isNullOrBlank()) {
|
||||
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON))
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
|
||||
}
|
||||
|
||||
return olmDevice.decryptGroupMessage(encryptedEventContent.ciphertext,
|
||||
event.roomId,
|
||||
timeline,
|
||||
encryptedEventContent.sessionId,
|
||||
encryptedEventContent.senderKey)
|
||||
return runCatching {
|
||||
olmDevice.decryptGroupMessage(encryptedEventContent.ciphertext,
|
||||
event.roomId,
|
||||
timeline,
|
||||
encryptedEventContent.sessionId,
|
||||
encryptedEventContent.senderKey)
|
||||
}
|
||||
.fold(
|
||||
{ olmDecryptionResult ->
|
||||
// the decryption succeeds
|
||||
if (olmDecryptionResult.payload != null) {
|
||||
MXEventDecryptionResult(
|
||||
clearEvent = olmDecryptionResult.payload,
|
||||
senderCurve25519Key = olmDecryptionResult.senderKey,
|
||||
claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"),
|
||||
forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain
|
||||
?: emptyList()
|
||||
)
|
||||
} else {
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
|
||||
}
|
||||
},
|
||||
{ throwable ->
|
||||
if (throwable is MXCryptoError.OlmError) {
|
||||
// TODO Check the value of .message
|
||||
@@ -98,10 +111,10 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
|
||||
val reason = String.format(MXCryptoError.OLM_REASON, throwable.olmException.message)
|
||||
val detailedReason = String.format(MXCryptoError.DETAILED_OLM_REASON, encryptedEventContent.ciphertext, reason)
|
||||
|
||||
Try.Failure(MXCryptoError.Base(
|
||||
throw MXCryptoError.Base(
|
||||
MXCryptoError.ErrorType.OLM,
|
||||
reason,
|
||||
detailedReason))
|
||||
detailedReason)
|
||||
}
|
||||
if (throwable is MXCryptoError.Base) {
|
||||
if (throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
|
||||
@@ -111,23 +124,7 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Try.Failure(throwable)
|
||||
},
|
||||
{ olmDecryptionResult ->
|
||||
// the decryption succeeds
|
||||
if (olmDecryptionResult.payload != null) {
|
||||
Try.just(
|
||||
MXEventDecryptionResult(
|
||||
clearEvent = olmDecryptionResult.payload,
|
||||
senderCurve25519Key = olmDecryptionResult.senderKey,
|
||||
claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"),
|
||||
forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain ?: emptyList()
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON))
|
||||
}
|
||||
throw throwable
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -148,11 +145,11 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
|
||||
|
||||
val selfMap = HashMap<String, String>()
|
||||
// TODO Replace this hard coded keys (see OutgoingRoomKeyRequestManager)
|
||||
selfMap["userId"] = credentials.userId
|
||||
selfMap["userId"] = userId
|
||||
selfMap["deviceId"] = "*"
|
||||
recipients.add(selfMap)
|
||||
|
||||
if (!TextUtils.equals(sender, credentials.userId)) {
|
||||
if (!TextUtils.equals(sender, userId)) {
|
||||
val senderMap = HashMap<String, String>()
|
||||
senderMap["userId"] = sender
|
||||
senderMap["deviceId"] = encryptedEventContent.deviceId!!
|
||||
@@ -311,51 +308,48 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
|
||||
}
|
||||
val userId = request.userId ?: return
|
||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||
deviceListManager
|
||||
.downloadKeys(listOf(userId), false)
|
||||
.flatMap {
|
||||
runCatching { deviceListManager.downloadKeys(listOf(userId), false) }
|
||||
.mapCatching {
|
||||
val deviceId = request.deviceId
|
||||
val deviceInfo = cryptoStore.getUserDevice(deviceId ?: "", userId)
|
||||
if (deviceInfo == null) {
|
||||
throw RuntimeException()
|
||||
} else {
|
||||
val devicesByUser = mapOf(userId to listOf(deviceInfo))
|
||||
ensureOlmSessionsForDevicesAction
|
||||
.handle(devicesByUser)
|
||||
.flatMap {
|
||||
val body = request.requestBody
|
||||
val olmSessionResult = it.getObject(userId, deviceId)
|
||||
if (olmSessionResult?.sessionId == null) {
|
||||
// no session with this device, probably because there
|
||||
// were no one-time keys.
|
||||
Try.just(Unit)
|
||||
}
|
||||
Timber.v("## shareKeysWithDevice() : sharing keys for session" +
|
||||
" ${body?.senderKey}|${body?.sessionId} with device $userId:$deviceId")
|
||||
val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
||||
val body = request.requestBody
|
||||
val olmSessionResult = usersDeviceMap.getObject(userId, deviceId)
|
||||
if (olmSessionResult?.sessionId == null) {
|
||||
// no session with this device, probably because there
|
||||
// were no one-time keys.
|
||||
return@mapCatching
|
||||
}
|
||||
Timber.v("## shareKeysWithDevice() : sharing keys for session" +
|
||||
" ${body?.senderKey}|${body?.sessionId} with device $userId:$deviceId")
|
||||
|
||||
val payloadJson = mutableMapOf<String, Any>("type" to EventType.FORWARDED_ROOM_KEY)
|
||||
val payloadJson = mutableMapOf<String, Any>("type" to EventType.FORWARDED_ROOM_KEY)
|
||||
runCatching { olmDevice.getInboundGroupSession(body?.sessionId, body?.senderKey, body?.roomId) }
|
||||
.fold(
|
||||
{
|
||||
// TODO
|
||||
payloadJson["content"] = it.exportKeys()
|
||||
?: ""
|
||||
},
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
||||
olmDevice.getInboundGroupSession(body?.sessionId, body?.senderKey, body?.roomId)
|
||||
.fold(
|
||||
{
|
||||
// TODO
|
||||
},
|
||||
{
|
||||
// TODO
|
||||
payloadJson["content"] = it.exportKeys() ?: ""
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
||||
sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
|
||||
Timber.v("## shareKeysWithDevice() : sending to $userId:$deviceId")
|
||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
||||
sendToDeviceTask.execute(sendToDeviceParams)
|
||||
}
|
||||
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
||||
sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
|
||||
Timber.v("## shareKeysWithDevice() : sending to $userId:$deviceId")
|
||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
||||
sendToDeviceTask.execute(sendToDeviceParams)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@@ -16,7 +16,6 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.algorithms.megolm
|
||||
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.internal.crypto.DeviceListManager
|
||||
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
||||
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequestManager
|
||||
@@ -24,23 +23,23 @@ import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevi
|
||||
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class MXMegolmDecryptionFactory @Inject constructor(private val credentials: Credentials,
|
||||
private val olmDevice: MXOlmDevice,
|
||||
private val deviceListManager: DeviceListManager,
|
||||
private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager,
|
||||
private val messageEncrypter: MessageEncrypter,
|
||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val sendToDeviceTask: SendToDeviceTask,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers) {
|
||||
internal class MXMegolmDecryptionFactory @Inject constructor(@UserId private val userId: String,
|
||||
private val olmDevice: MXOlmDevice,
|
||||
private val deviceListManager: DeviceListManager,
|
||||
private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager,
|
||||
private val messageEncrypter: MessageEncrypter,
|
||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val sendToDeviceTask: SendToDeviceTask,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers) {
|
||||
|
||||
fun create(): MXMegolmDecryption {
|
||||
return MXMegolmDecryption(
|
||||
credentials,
|
||||
userId,
|
||||
olmDevice,
|
||||
deviceListManager,
|
||||
outgoingRoomKeyRequestManager,
|
||||
|
@@ -19,7 +19,6 @@
|
||||
package im.vector.matrix.android.internal.crypto.algorithms.megolm
|
||||
|
||||
import android.text.TextUtils
|
||||
import arrow.core.Try
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.events.model.Content
|
||||
@@ -69,12 +68,10 @@ internal class MXMegolmEncryption(
|
||||
|
||||
override suspend fun encryptEventContent(eventContent: Content,
|
||||
eventType: String,
|
||||
userIds: List<String>): Try<Content> {
|
||||
return getDevicesInRoom(userIds)
|
||||
.flatMap { ensureOutboundSession(it) }
|
||||
.flatMap {
|
||||
encryptContent(it, eventType, eventContent)
|
||||
}
|
||||
userIds: List<String>): Content {
|
||||
val devices = getDevicesInRoom(userIds)
|
||||
val outboundSession = ensureOutboundSession(devices)
|
||||
return encryptContent(outboundSession, eventType, eventContent)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,7 +98,7 @@ internal class MXMegolmEncryption(
|
||||
*
|
||||
* @param devicesInRoom the devices list
|
||||
*/
|
||||
private suspend fun ensureOutboundSession(devicesInRoom: MXUsersDevicesMap<MXDeviceInfo>): Try<MXOutboundSessionInfo> {
|
||||
private suspend fun ensureOutboundSession(devicesInRoom: MXUsersDevicesMap<MXDeviceInfo>): MXOutboundSessionInfo {
|
||||
var session = outboundSession
|
||||
if (session == null
|
||||
// Need to make a brand new session?
|
||||
@@ -126,7 +123,8 @@ internal class MXMegolmEncryption(
|
||||
}
|
||||
}
|
||||
}
|
||||
return shareKey(safeSession, shareMap).map { safeSession!! }
|
||||
shareKey(safeSession, shareMap)
|
||||
return safeSession
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,11 +134,11 @@ internal class MXMegolmEncryption(
|
||||
* @param devicesByUsers the devices map
|
||||
*/
|
||||
private suspend fun shareKey(session: MXOutboundSessionInfo,
|
||||
devicesByUsers: Map<String, List<MXDeviceInfo>>): Try<Unit> {
|
||||
devicesByUsers: Map<String, List<MXDeviceInfo>>) {
|
||||
// nothing to send, the task is done
|
||||
if (devicesByUsers.isEmpty()) {
|
||||
Timber.v("## shareKey() : nothing more to do")
|
||||
return Try.just(Unit)
|
||||
return
|
||||
}
|
||||
// reduce the map size to avoid request timeout when there are too many devices (Users size * devices per user)
|
||||
val subMap = HashMap<String, List<MXDeviceInfo>>()
|
||||
@@ -157,11 +155,9 @@ internal class MXMegolmEncryption(
|
||||
}
|
||||
}
|
||||
Timber.v("## shareKey() ; userId $userIds")
|
||||
return shareUserDevicesKey(session, subMap)
|
||||
.flatMap {
|
||||
val remainingDevices = devicesByUsers.filterKeys { userIds.contains(it).not() }
|
||||
shareKey(session, remainingDevices)
|
||||
}
|
||||
shareUserDevicesKey(session, subMap)
|
||||
val remainingDevices = devicesByUsers.filterKeys { userIds.contains(it).not() }
|
||||
shareKey(session, remainingDevices)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -172,7 +168,7 @@ internal class MXMegolmEncryption(
|
||||
* @param callback the asynchronous callback
|
||||
*/
|
||||
private suspend fun shareUserDevicesKey(session: MXOutboundSessionInfo,
|
||||
devicesByUser: Map<String, List<MXDeviceInfo>>): Try<Unit> {
|
||||
devicesByUser: Map<String, List<MXDeviceInfo>>) {
|
||||
val sessionKey = olmDevice.getSessionKey(session.sessionId)
|
||||
val chainIndex = olmDevice.getMessageIndex(session.sessionId)
|
||||
|
||||
@@ -190,94 +186,86 @@ internal class MXMegolmEncryption(
|
||||
var t0 = System.currentTimeMillis()
|
||||
Timber.v("## shareUserDevicesKey() : starts")
|
||||
|
||||
return ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
||||
.flatMap {
|
||||
Timber.v("## shareUserDevicesKey() : ensureOlmSessionsForDevices succeeds after "
|
||||
+ (System.currentTimeMillis() - t0) + " ms")
|
||||
val contentMap = MXUsersDevicesMap<Any>()
|
||||
var haveTargets = false
|
||||
val userIds = it.userIds
|
||||
for (userId in userIds) {
|
||||
val devicesToShareWith = devicesByUser[userId]
|
||||
for ((deviceID) in devicesToShareWith!!) {
|
||||
val sessionResult = it.getObject(userId, deviceID)
|
||||
if (sessionResult?.sessionId == null) {
|
||||
// no session with this device, probably because there
|
||||
// were no one-time keys.
|
||||
//
|
||||
// we could send them a to_device message anyway, as a
|
||||
// signal that they have missed out on the key sharing
|
||||
// message because of the lack of keys, but there's not
|
||||
// much point in that really; it will mostly serve to clog
|
||||
// up to_device inboxes.
|
||||
//
|
||||
// ensureOlmSessionsForUsers has already done the logging,
|
||||
// so just skip it.
|
||||
continue
|
||||
}
|
||||
Timber.v("## shareUserDevicesKey() : Sharing keys with device $userId:$deviceID")
|
||||
//noinspection ArraysAsListWithZeroOrOneArgument,ArraysAsListWithZeroOrOneArgument
|
||||
contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, Arrays.asList(sessionResult.deviceInfo)))
|
||||
haveTargets = true
|
||||
}
|
||||
}
|
||||
if (haveTargets) {
|
||||
t0 = System.currentTimeMillis()
|
||||
Timber.v("## shareUserDevicesKey() : has target")
|
||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap)
|
||||
sendToDeviceTask.execute(sendToDeviceParams)
|
||||
.map {
|
||||
Timber.v("## shareUserDevicesKey() : sendToDevice succeeds after "
|
||||
+ (System.currentTimeMillis() - t0) + " ms")
|
||||
|
||||
// Add the devices we have shared with to session.sharedWithDevices.
|
||||
// we deliberately iterate over devicesByUser (ie, the devices we
|
||||
// attempted to share with) rather than the contentMap (those we did
|
||||
// share with), because we don't want to try to claim a one-time-key
|
||||
// for dead devices on every message.
|
||||
for (userId in devicesByUser.keys) {
|
||||
val devicesToShareWith = devicesByUser[userId]
|
||||
for ((deviceId) in devicesToShareWith!!) {
|
||||
session.sharedWithDevices.setObject(userId, deviceId, chainIndex)
|
||||
}
|
||||
}
|
||||
Unit
|
||||
}
|
||||
} else {
|
||||
Timber.v("## shareUserDevicesKey() : no need to sharekey")
|
||||
Try.just(Unit)
|
||||
}
|
||||
val results = ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
||||
Timber.v("## shareUserDevicesKey() : ensureOlmSessionsForDevices succeeds after "
|
||||
+ (System.currentTimeMillis() - t0) + " ms")
|
||||
val contentMap = MXUsersDevicesMap<Any>()
|
||||
var haveTargets = false
|
||||
val userIds = results.userIds
|
||||
for (userId in userIds) {
|
||||
val devicesToShareWith = devicesByUser[userId]
|
||||
for ((deviceID) in devicesToShareWith!!) {
|
||||
val sessionResult = results.getObject(userId, deviceID)
|
||||
if (sessionResult?.sessionId == null) {
|
||||
// no session with this device, probably because there
|
||||
// were no one-time keys.
|
||||
//
|
||||
// we could send them a to_device message anyway, as a
|
||||
// signal that they have missed out on the key sharing
|
||||
// message because of the lack of keys, but there's not
|
||||
// much point in that really; it will mostly serve to clog
|
||||
// up to_device inboxes.
|
||||
//
|
||||
// ensureOlmSessionsForUsers has already done the logging,
|
||||
// so just skip it.
|
||||
continue
|
||||
}
|
||||
Timber.v("## shareUserDevicesKey() : Sharing keys with device $userId:$deviceID")
|
||||
//noinspection ArraysAsListWithZeroOrOneArgument,ArraysAsListWithZeroOrOneArgument
|
||||
contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, Arrays.asList(sessionResult.deviceInfo)))
|
||||
haveTargets = true
|
||||
}
|
||||
}
|
||||
if (haveTargets) {
|
||||
t0 = System.currentTimeMillis()
|
||||
Timber.v("## shareUserDevicesKey() : has target")
|
||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap)
|
||||
sendToDeviceTask.execute(sendToDeviceParams)
|
||||
Timber.v("## shareUserDevicesKey() : sendToDevice succeeds after "
|
||||
+ (System.currentTimeMillis() - t0) + " ms")
|
||||
|
||||
// Add the devices we have shared with to session.sharedWithDevices.
|
||||
// we deliberately iterate over devicesByUser (ie, the devices we
|
||||
// attempted to share with) rather than the contentMap (those we did
|
||||
// share with), because we don't want to try to claim a one-time-key
|
||||
// for dead devices on every message.
|
||||
for (userId in devicesByUser.keys) {
|
||||
val devicesToShareWith = devicesByUser[userId]
|
||||
for ((deviceId) in devicesToShareWith!!) {
|
||||
session.sharedWithDevices.setObject(userId, deviceId, chainIndex)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Timber.v("## shareUserDevicesKey() : no need to sharekey")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* process the pending encryptions
|
||||
*/
|
||||
private fun encryptContent(session: MXOutboundSessionInfo, eventType: String, eventContent: Content): Try<Content> {
|
||||
return Try<Content> {
|
||||
// Everything is in place, encrypt all pending events
|
||||
val payloadJson = HashMap<String, Any>()
|
||||
payloadJson["room_id"] = roomId
|
||||
payloadJson["type"] = eventType
|
||||
payloadJson["content"] = eventContent
|
||||
private fun encryptContent(session: MXOutboundSessionInfo, eventType: String, eventContent: Content): Content {
|
||||
// Everything is in place, encrypt all pending events
|
||||
val payloadJson = HashMap<String, Any>()
|
||||
payloadJson["room_id"] = roomId
|
||||
payloadJson["type"] = eventType
|
||||
payloadJson["content"] = eventContent
|
||||
|
||||
// Get canonical Json from
|
||||
// Get canonical Json from
|
||||
|
||||
val payloadString = convertToUTF8(JsonCanonicalizer.getCanonicalJson(Map::class.java, payloadJson))
|
||||
val ciphertext = olmDevice.encryptGroupMessage(session.sessionId, payloadString!!)
|
||||
val payloadString = convertToUTF8(JsonCanonicalizer.getCanonicalJson(Map::class.java, payloadJson))
|
||||
val ciphertext = olmDevice.encryptGroupMessage(session.sessionId, payloadString!!)
|
||||
|
||||
val map = HashMap<String, Any>()
|
||||
map["algorithm"] = MXCRYPTO_ALGORITHM_MEGOLM
|
||||
map["sender_key"] = olmDevice.deviceCurve25519Key!!
|
||||
map["ciphertext"] = ciphertext!!
|
||||
map["session_id"] = session.sessionId
|
||||
val map = HashMap<String, Any>()
|
||||
map["algorithm"] = MXCRYPTO_ALGORITHM_MEGOLM
|
||||
map["sender_key"] = olmDevice.deviceCurve25519Key!!
|
||||
map["ciphertext"] = ciphertext!!
|
||||
map["session_id"] = session.sessionId
|
||||
|
||||
// Include our device ID so that recipients can send us a
|
||||
// m.new_device message if they don't have our session key.
|
||||
map["device_id"] = credentials.deviceId!!
|
||||
session.useCount++
|
||||
map
|
||||
}
|
||||
// Include our device ID so that recipients can send us a
|
||||
// m.new_device message if they don't have our session key.
|
||||
map["device_id"] = credentials.deviceId!!
|
||||
session.useCount++
|
||||
return map
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -287,50 +275,47 @@ internal class MXMegolmEncryption(
|
||||
* @param userIds the user ids whose devices must be checked.
|
||||
* @param callback the asynchronous callback
|
||||
*/
|
||||
private suspend fun getDevicesInRoom(userIds: List<String>): Try<MXUsersDevicesMap<MXDeviceInfo>> {
|
||||
private suspend fun getDevicesInRoom(userIds: List<String>): MXUsersDevicesMap<MXDeviceInfo> {
|
||||
// We are happy to use a cached version here: we assume that if we already
|
||||
// have a list of the user's devices, then we already share an e2e room
|
||||
// with them, which means that they will have announced any new devices via
|
||||
// an m.new_device.
|
||||
return deviceListManager
|
||||
.downloadKeys(userIds, false)
|
||||
.flatMap {
|
||||
val encryptToVerifiedDevicesOnly = cryptoStore.getGlobalBlacklistUnverifiedDevices()
|
||||
|| cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(roomId)
|
||||
val keys = deviceListManager.downloadKeys(userIds, false)
|
||||
val encryptToVerifiedDevicesOnly = cryptoStore.getGlobalBlacklistUnverifiedDevices()
|
||||
|| cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(roomId)
|
||||
|
||||
val devicesInRoom = MXUsersDevicesMap<MXDeviceInfo>()
|
||||
val unknownDevices = MXUsersDevicesMap<MXDeviceInfo>()
|
||||
val devicesInRoom = MXUsersDevicesMap<MXDeviceInfo>()
|
||||
val unknownDevices = MXUsersDevicesMap<MXDeviceInfo>()
|
||||
|
||||
for (userId in it.userIds) {
|
||||
val deviceIds = it.getUserDeviceIds(userId) ?: continue
|
||||
for (deviceId in deviceIds) {
|
||||
val deviceInfo = it.getObject(userId, deviceId) ?: continue
|
||||
if (warnOnUnknownDevicesRepository.warnOnUnknownDevices() && deviceInfo.isUnknown) {
|
||||
// The device is not yet known by the user
|
||||
unknownDevices.setObject(userId, deviceId, deviceInfo)
|
||||
continue
|
||||
}
|
||||
if (deviceInfo.isBlocked) {
|
||||
// Remove any blocked devices
|
||||
continue
|
||||
}
|
||||
|
||||
if (!deviceInfo.isVerified && encryptToVerifiedDevicesOnly) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (TextUtils.equals(deviceInfo.identityKey(), olmDevice.deviceCurve25519Key)) {
|
||||
// Don't bother sending to ourself
|
||||
continue
|
||||
}
|
||||
devicesInRoom.setObject(userId, deviceId, deviceInfo)
|
||||
}
|
||||
}
|
||||
if (unknownDevices.isEmpty) {
|
||||
Try.just(devicesInRoom)
|
||||
} else {
|
||||
Try.Failure(MXCryptoError.UnknownDevice(unknownDevices))
|
||||
}
|
||||
for (userId in keys.userIds) {
|
||||
val deviceIds = keys.getUserDeviceIds(userId) ?: continue
|
||||
for (deviceId in deviceIds) {
|
||||
val deviceInfo = keys.getObject(userId, deviceId) ?: continue
|
||||
if (warnOnUnknownDevicesRepository.warnOnUnknownDevices() && deviceInfo.isUnknown) {
|
||||
// The device is not yet known by the user
|
||||
unknownDevices.setObject(userId, deviceId, deviceInfo)
|
||||
continue
|
||||
}
|
||||
if (deviceInfo.isBlocked) {
|
||||
// Remove any blocked devices
|
||||
continue
|
||||
}
|
||||
|
||||
if (!deviceInfo.isVerified && encryptToVerifiedDevicesOnly) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (TextUtils.equals(deviceInfo.identityKey(), olmDevice.deviceCurve25519Key)) {
|
||||
// Don't bother sending to ourself
|
||||
continue
|
||||
}
|
||||
devicesInRoom.setObject(userId, deviceId, deviceInfo)
|
||||
}
|
||||
}
|
||||
if (unknownDevices.isEmpty) {
|
||||
return devicesInRoom
|
||||
} else {
|
||||
throw MXCryptoError.UnknownDevice(unknownDevices)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -17,8 +17,6 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.algorithms.olm
|
||||
|
||||
import arrow.core.Try
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
@@ -32,38 +30,36 @@ import im.vector.matrix.android.internal.crypto.model.event.OlmPayloadContent
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import im.vector.matrix.android.internal.util.convertFromUTF8
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
internal class MXOlmDecryption(
|
||||
// The olm device interface
|
||||
private val olmDevice: MXOlmDevice,
|
||||
// the matrix credentials
|
||||
private val credentials: Credentials)
|
||||
// the matrix userId
|
||||
private val userId: String)
|
||||
: IMXDecrypting {
|
||||
|
||||
override suspend fun decryptEvent(event: Event, timeline: String): Try<MXEventDecryptionResult> {
|
||||
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||
val olmEventContent = event.content.toModel<OlmEventContent>() ?: run {
|
||||
Timber.e("## decryptEvent() : bad event format")
|
||||
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_EVENT_FORMAT,
|
||||
MXCryptoError.BAD_EVENT_FORMAT_TEXT_REASON))
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_EVENT_FORMAT,
|
||||
MXCryptoError.BAD_EVENT_FORMAT_TEXT_REASON)
|
||||
}
|
||||
|
||||
val cipherText = olmEventContent.ciphertext ?: run {
|
||||
Timber.e("## decryptEvent() : missing cipher text")
|
||||
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_CIPHER_TEXT,
|
||||
MXCryptoError.MISSING_CIPHER_TEXT_REASON))
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_CIPHER_TEXT,
|
||||
MXCryptoError.MISSING_CIPHER_TEXT_REASON)
|
||||
}
|
||||
|
||||
val senderKey = olmEventContent.senderKey ?: run {
|
||||
Timber.e("## decryptEvent() : missing sender key")
|
||||
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY,
|
||||
MXCryptoError.MISSING_SENDER_KEY_TEXT_REASON))
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY,
|
||||
MXCryptoError.MISSING_SENDER_KEY_TEXT_REASON)
|
||||
}
|
||||
|
||||
val messageAny = cipherText[olmDevice.deviceCurve25519Key] ?: run {
|
||||
Timber.e("## decryptEvent() : our device ${olmDevice.deviceCurve25519Key} is not included in recipients")
|
||||
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.NOT_INCLUDE_IN_RECIPIENTS,
|
||||
MXCryptoError.NOT_INCLUDED_IN_RECIPIENT_REASON))
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.NOT_INCLUDE_IN_RECIPIENTS, MXCryptoError.NOT_INCLUDED_IN_RECIPIENT_REASON)
|
||||
}
|
||||
|
||||
// The message for myUser
|
||||
@@ -73,14 +69,12 @@ internal class MXOlmDecryption(
|
||||
|
||||
if (decryptedPayload == null) {
|
||||
Timber.e("## decryptEvent() Failed to decrypt Olm event (id= ${event.eventId} from $senderKey")
|
||||
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE,
|
||||
MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON))
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
|
||||
}
|
||||
val payloadString = convertFromUTF8(decryptedPayload)
|
||||
if (payloadString == null) {
|
||||
Timber.e("## decryptEvent() Failed to decrypt Olm event (id= ${event.eventId} from $senderKey")
|
||||
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE,
|
||||
MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON))
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
|
||||
}
|
||||
|
||||
val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
|
||||
@@ -88,73 +82,70 @@ internal class MXOlmDecryption(
|
||||
|
||||
if (payload == null) {
|
||||
Timber.e("## decryptEvent failed : null payload")
|
||||
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT,
|
||||
MXCryptoError.MISSING_CIPHER_TEXT_REASON))
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON)
|
||||
}
|
||||
|
||||
val olmPayloadContent = OlmPayloadContent.fromJsonString(payloadString) ?: run {
|
||||
Timber.e("## decryptEvent() : bad olmPayloadContent format")
|
||||
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT,
|
||||
MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON))
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)
|
||||
}
|
||||
|
||||
if (olmPayloadContent.recipient.isNullOrBlank()) {
|
||||
val reason = String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient")
|
||||
Timber.e("## decryptEvent() : $reason")
|
||||
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY,
|
||||
reason))
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, reason)
|
||||
}
|
||||
|
||||
if (olmPayloadContent.recipient != credentials.userId) {
|
||||
if (olmPayloadContent.recipient != userId) {
|
||||
Timber.e("## decryptEvent() : Event ${event.eventId}:" +
|
||||
" Intended recipient ${olmPayloadContent.recipient} does not match our id ${credentials.userId}")
|
||||
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_RECIPIENT,
|
||||
String.format(MXCryptoError.BAD_RECIPIENT_REASON, olmPayloadContent.recipient)))
|
||||
" Intended recipient ${olmPayloadContent.recipient} does not match our id $userId")
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_RECIPIENT,
|
||||
String.format(MXCryptoError.BAD_RECIPIENT_REASON, olmPayloadContent.recipient))
|
||||
}
|
||||
|
||||
val recipientKeys = olmPayloadContent.recipient_keys ?: run {
|
||||
Timber.e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'recipient_keys' property; cannot prevent unknown-key attack")
|
||||
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY,
|
||||
String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient_keys")))
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY,
|
||||
String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient_keys"))
|
||||
}
|
||||
|
||||
val ed25519 = recipientKeys["ed25519"]
|
||||
|
||||
if (ed25519 != olmDevice.deviceEd25519Key) {
|
||||
Timber.e("## decryptEvent() : Event ${event.eventId}: Intended recipient ed25519 key $ed25519 did not match ours")
|
||||
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_RECIPIENT_KEY,
|
||||
MXCryptoError.BAD_RECIPIENT_KEY_REASON))
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_RECIPIENT_KEY,
|
||||
MXCryptoError.BAD_RECIPIENT_KEY_REASON)
|
||||
}
|
||||
|
||||
if (olmPayloadContent.sender.isNullOrBlank()) {
|
||||
Timber.e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'sender' property; cannot prevent unknown-key attack")
|
||||
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY,
|
||||
String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "sender")))
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY,
|
||||
String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "sender"))
|
||||
}
|
||||
|
||||
if (olmPayloadContent.sender != event.senderId) {
|
||||
Timber.e("Event ${event.eventId}: original sender ${olmPayloadContent.sender} does not match reported sender ${event.senderId}")
|
||||
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.FORWARDED_MESSAGE,
|
||||
String.format(MXCryptoError.FORWARDED_MESSAGE_REASON, olmPayloadContent.sender)))
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.FORWARDED_MESSAGE,
|
||||
String.format(MXCryptoError.FORWARDED_MESSAGE_REASON, olmPayloadContent.sender))
|
||||
}
|
||||
|
||||
if (olmPayloadContent.room_id != event.roomId) {
|
||||
Timber.e("## decryptEvent() : Event ${event.eventId}: original room ${olmPayloadContent.room_id} does not match reported room ${event.roomId}")
|
||||
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ROOM,
|
||||
String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.room_id)))
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ROOM,
|
||||
String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.room_id))
|
||||
}
|
||||
|
||||
val keys = olmPayloadContent.keys ?: run {
|
||||
Timber.e("## decryptEvent failed : null keys")
|
||||
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT,
|
||||
MXCryptoError.MISSING_CIPHER_TEXT_REASON))
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT,
|
||||
MXCryptoError.MISSING_CIPHER_TEXT_REASON)
|
||||
}
|
||||
|
||||
return Try.just(MXEventDecryptionResult(
|
||||
return MXEventDecryptionResult(
|
||||
clearEvent = payload,
|
||||
senderCurve25519Key = senderKey,
|
||||
claimedEd25519Key = keys["ed25519"]
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,33 +156,14 @@ internal class MXOlmDecryption(
|
||||
* @return payload, if decrypted successfully.
|
||||
*/
|
||||
private fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? {
|
||||
val sessionIdsSet = olmDevice.getSessionIds(theirDeviceIdentityKey)
|
||||
val sessionIds = olmDevice.getSessionIds(theirDeviceIdentityKey) ?: emptySet()
|
||||
|
||||
val sessionIds: List<String>
|
||||
|
||||
if (null == sessionIdsSet) {
|
||||
sessionIds = ArrayList()
|
||||
} else {
|
||||
sessionIds = ArrayList(sessionIdsSet)
|
||||
}
|
||||
|
||||
val messageBody = message["body"] as? String
|
||||
var messageType: Int? = null
|
||||
|
||||
val typeAsVoid = message["type"]
|
||||
|
||||
if (null != typeAsVoid) {
|
||||
if (typeAsVoid is Double) {
|
||||
messageType = typeAsVoid.toInt()
|
||||
} else if (typeAsVoid is Int) {
|
||||
messageType = typeAsVoid
|
||||
} else if (typeAsVoid is Long) {
|
||||
messageType = typeAsVoid.toInt()
|
||||
}
|
||||
}
|
||||
|
||||
if (null == messageBody || null == messageType) {
|
||||
return null
|
||||
val messageBody = message["body"] as? String ?: return null
|
||||
val messageType = when (val typeAsVoid = message["type"]) {
|
||||
is Double -> typeAsVoid.toInt()
|
||||
is Int -> typeAsVoid
|
||||
is Long -> typeAsVoid.toInt()
|
||||
else -> return null
|
||||
}
|
||||
|
||||
// Try each session in turn
|
||||
|
@@ -16,17 +16,16 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.algorithms.olm
|
||||
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class MXOlmDecryptionFactory @Inject constructor(private val olmDevice: MXOlmDevice,
|
||||
private val credentials: Credentials) {
|
||||
@UserId private val userId: String) {
|
||||
|
||||
fun create(): MXOlmDecryption {
|
||||
return MXOlmDecryption(
|
||||
olmDevice,
|
||||
credentials)
|
||||
userId)
|
||||
}
|
||||
}
|
@@ -19,7 +19,6 @@
|
||||
package im.vector.matrix.android.internal.crypto.algorithms.olm
|
||||
|
||||
import android.text.TextUtils
|
||||
import arrow.core.Try
|
||||
import im.vector.matrix.android.api.session.events.model.Content
|
||||
import im.vector.matrix.android.api.session.events.model.toContent
|
||||
import im.vector.matrix.android.internal.crypto.DeviceListManager
|
||||
@@ -40,37 +39,35 @@ internal class MXOlmEncryption(
|
||||
private val ensureOlmSessionsForUsersAction: EnsureOlmSessionsForUsersAction)
|
||||
: IMXEncrypting {
|
||||
|
||||
override suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List<String>): Try<Content> {
|
||||
override suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List<String>): Content {
|
||||
// pick the list of recipients based on the membership list.
|
||||
//
|
||||
// TODO: there is a race condition here! What if a new user turns up
|
||||
return ensureSession(userIds)
|
||||
.map {
|
||||
val deviceInfos = ArrayList<MXDeviceInfo>()
|
||||
for (userId in userIds) {
|
||||
val devices = cryptoStore.getUserDevices(userId)?.values ?: emptyList()
|
||||
for (device in devices) {
|
||||
val key = device.identityKey()
|
||||
if (TextUtils.equals(key, olmDevice.deviceCurve25519Key)) {
|
||||
// Don't bother setting up session to ourself
|
||||
continue
|
||||
}
|
||||
if (device.isBlocked) {
|
||||
// Don't bother setting up sessions with blocked users
|
||||
continue
|
||||
}
|
||||
deviceInfos.add(device)
|
||||
}
|
||||
}
|
||||
|
||||
val messageMap = HashMap<String, Any>()
|
||||
messageMap["room_id"] = roomId
|
||||
messageMap["type"] = eventType
|
||||
messageMap["content"] = eventContent
|
||||
|
||||
messageEncrypter.encryptMessage(messageMap, deviceInfos)
|
||||
messageMap.toContent()!!
|
||||
ensureSession(userIds)
|
||||
val deviceInfos = ArrayList<MXDeviceInfo>()
|
||||
for (userId in userIds) {
|
||||
val devices = cryptoStore.getUserDevices(userId)?.values ?: emptyList()
|
||||
for (device in devices) {
|
||||
val key = device.identityKey()
|
||||
if (TextUtils.equals(key, olmDevice.deviceCurve25519Key)) {
|
||||
// Don't bother setting up session to ourself
|
||||
continue
|
||||
}
|
||||
if (device.isBlocked) {
|
||||
// Don't bother setting up sessions with blocked users
|
||||
continue
|
||||
}
|
||||
deviceInfos.add(device)
|
||||
}
|
||||
}
|
||||
|
||||
val messageMap = HashMap<String, Any>()
|
||||
messageMap["room_id"] = roomId
|
||||
messageMap["type"] = eventType
|
||||
messageMap["content"] = eventContent
|
||||
|
||||
messageEncrypter.encryptMessage(messageMap, deviceInfos)
|
||||
return messageMap.toContent()!!
|
||||
}
|
||||
|
||||
|
||||
@@ -78,13 +75,9 @@ internal class MXOlmEncryption(
|
||||
* Ensure that the session
|
||||
*
|
||||
* @param users the user ids list
|
||||
* @param callback the asynchronous callback
|
||||
*/
|
||||
private suspend fun ensureSession(users: List<String>): Try<Unit> {
|
||||
return deviceListManager
|
||||
.downloadKeys(users, false)
|
||||
.flatMap { ensureOlmSessionsForUsersAction.handle(users) }
|
||||
.map { Unit }
|
||||
|
||||
private suspend fun ensureSession(users: List<String>) {
|
||||
deviceListManager.downloadKeys(users, false)
|
||||
ensureOlmSessionsForUsersAction.handle(users)
|
||||
}
|
||||
}
|
||||
|
@@ -21,7 +21,6 @@ import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
||||
import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForUsersAction
|
||||
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@@ -18,8 +18,6 @@ package im.vector.matrix.android.internal.crypto.api
|
||||
|
||||
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.*
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadBody
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.SendToDeviceBody
|
||||
import im.vector.matrix.android.internal.network.NetworkConstants
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.*
|
||||
|
@@ -26,7 +26,6 @@ import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import java.security.MessageDigest
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
@@ -59,8 +58,7 @@ object MXEncryptedAttachments {
|
||||
// Half of the IV is random, the lower order bits are zeroed
|
||||
// such that the counter never wraps.
|
||||
// See https://github.com/matrix-org/matrix-ios-kit/blob/3dc0d8e46b4deb6669ed44f72ad79be56471354c/MatrixKit/Models/Room/MXEncryptedAttachments.m#L75
|
||||
val initVectorBytes = ByteArray(16)
|
||||
Arrays.fill(initVectorBytes, 0.toByte())
|
||||
val initVectorBytes = ByteArray(16) { 0.toByte() }
|
||||
|
||||
val ivRandomPart = ByteArray(8)
|
||||
secureRandom.nextBytes(ivRandomPart)
|
||||
@@ -115,7 +113,7 @@ object MXEncryptedAttachments {
|
||||
encryptedByteArray = outStream.toByteArray()
|
||||
)
|
||||
|
||||
Timber.v("Encrypt in " + (System.currentTimeMillis() - t0) + " ms")
|
||||
Timber.v("Encrypt in ${System.currentTimeMillis() - t0} ms")
|
||||
return Try.just(result)
|
||||
} catch (oom: OutOfMemoryError) {
|
||||
Timber.e(oom, "## encryptAttachment failed")
|
||||
@@ -206,13 +204,13 @@ object MXEncryptedAttachments {
|
||||
val decryptedStream = ByteArrayInputStream(outStream.toByteArray())
|
||||
outStream.close()
|
||||
|
||||
Timber.v("Decrypt in " + (System.currentTimeMillis() - t0) + " ms")
|
||||
Timber.v("Decrypt in ${System.currentTimeMillis() - t0} ms")
|
||||
|
||||
return decryptedStream
|
||||
} catch (oom: OutOfMemoryError) {
|
||||
Timber.e(oom, "## decryptAttachment() : failed " + oom.message)
|
||||
Timber.e(oom, "## decryptAttachment() : failed ${oom.message}")
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## decryptAttachment() : failed " + e.message)
|
||||
Timber.e(e, "## decryptAttachment() : failed ${e.message}")
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -228,34 +226,20 @@ object MXEncryptedAttachments {
|
||||
* Base64 URL conversion methods
|
||||
*/
|
||||
|
||||
private fun base64UrlToBase64(base64Url: String?): String? {
|
||||
var result = base64Url
|
||||
if (null != result) {
|
||||
result = result.replace("-".toRegex(), "+")
|
||||
result = result.replace("_".toRegex(), "/")
|
||||
}
|
||||
|
||||
return result
|
||||
private fun base64UrlToBase64(base64Url: String): String {
|
||||
return base64Url.replace('-', '+')
|
||||
.replace('_', '/')
|
||||
}
|
||||
|
||||
private fun base64ToBase64Url(base64: String?): String? {
|
||||
var result = base64
|
||||
if (null != result) {
|
||||
result = result.replace("\n".toRegex(), "")
|
||||
result = result.replace("\\+".toRegex(), "-")
|
||||
result = result.replace("/".toRegex(), "_")
|
||||
result = result.replace("=".toRegex(), "")
|
||||
}
|
||||
return result
|
||||
private fun base64ToBase64Url(base64: String): String {
|
||||
return base64.replace("\n".toRegex(), "")
|
||||
.replace("\\+".toRegex(), "-")
|
||||
.replace('/', '_')
|
||||
.replace("=", "")
|
||||
}
|
||||
|
||||
private fun base64ToUnpaddedBase64(base64: String?): String? {
|
||||
var result = base64
|
||||
if (null != result) {
|
||||
result = result.replace("\n".toRegex(), "")
|
||||
result = result.replace("=".toRegex(), "")
|
||||
}
|
||||
|
||||
return result
|
||||
private fun base64ToUnpaddedBase64(base64: String): String {
|
||||
return base64.replace("\n".toRegex(), "")
|
||||
.replace("=", "")
|
||||
}
|
||||
}
|
||||
|
@@ -51,7 +51,6 @@ import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEnt
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import im.vector.matrix.android.internal.extensions.foldToCallback
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.task.*
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.TaskThread
|
||||
@@ -67,9 +66,8 @@ import org.matrix.olm.OlmPkEncryption
|
||||
import org.matrix.olm.OlmPkMessage
|
||||
import timber.log.Timber
|
||||
import java.security.InvalidParameterException
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.HashMap
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* A KeysBackup class instance manage incremental backup of e2e keys (megolm keys)
|
||||
@@ -115,8 +113,6 @@ internal class KeysBackup @Inject constructor(
|
||||
// The backup key being used.
|
||||
private var backupOlmPkEncryption: OlmPkEncryption? = null
|
||||
|
||||
private val random = Random()
|
||||
|
||||
private var backupAllGroupSessionsCallback: MatrixCallback<Unit>? = null
|
||||
|
||||
private var keysBackupStateListener: KeysBackupStateListener? = null
|
||||
@@ -204,31 +200,32 @@ internal class KeysBackup @Inject constructor(
|
||||
keysBackupStateManager.state = KeysBackupState.Enabling
|
||||
|
||||
createKeysBackupVersionTask
|
||||
.configureWith(createKeysBackupVersionBody)
|
||||
.dispatchTo(object : MatrixCallback<KeysVersion> {
|
||||
override fun onSuccess(info: KeysVersion) {
|
||||
// Reset backup markers.
|
||||
cryptoStore.resetBackupMarkers()
|
||||
.configureWith(createKeysBackupVersionBody) {
|
||||
this.callback = object : MatrixCallback<KeysVersion> {
|
||||
override fun onSuccess(info: KeysVersion) {
|
||||
// Reset backup markers.
|
||||
cryptoStore.resetBackupMarkers()
|
||||
|
||||
val keyBackupVersion = KeysVersionResult()
|
||||
keyBackupVersion.algorithm = createKeysBackupVersionBody.algorithm
|
||||
keyBackupVersion.authData = createKeysBackupVersionBody.authData
|
||||
keyBackupVersion.version = info.version
|
||||
val keyBackupVersion = KeysVersionResult()
|
||||
keyBackupVersion.algorithm = createKeysBackupVersionBody.algorithm
|
||||
keyBackupVersion.authData = createKeysBackupVersionBody.authData
|
||||
keyBackupVersion.version = info.version
|
||||
|
||||
// We can consider that the server does not have keys yet
|
||||
keyBackupVersion.count = 0
|
||||
keyBackupVersion.hash = null
|
||||
// We can consider that the server does not have keys yet
|
||||
keyBackupVersion.count = 0
|
||||
keyBackupVersion.hash = null
|
||||
|
||||
enableKeysBackup(keyBackupVersion)
|
||||
enableKeysBackup(keyBackupVersion)
|
||||
|
||||
callback.onSuccess(info)
|
||||
callback.onSuccess(info)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
keysBackupStateManager.state = KeysBackupState.Disabled
|
||||
callback.onFailure(failure)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
keysBackupStateManager.state = KeysBackupState.Disabled
|
||||
callback.onFailure(failure)
|
||||
}
|
||||
})
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
@@ -243,27 +240,29 @@ internal class KeysBackup @Inject constructor(
|
||||
keysBackupStateManager.state = KeysBackupState.Unknown
|
||||
}
|
||||
|
||||
deleteBackupTask.configureWith(DeleteBackupTask.Params(version))
|
||||
.dispatchTo(object : MatrixCallback<Unit> {
|
||||
private fun eventuallyRestartBackup() {
|
||||
// Do not stay in KeysBackupState.Unknown but check what is available on the homeserver
|
||||
if (state == KeysBackupState.Unknown) {
|
||||
checkAndStartKeysBackup()
|
||||
deleteBackupTask
|
||||
.configureWith(DeleteBackupTask.Params(version)) {
|
||||
this.callback = object : MatrixCallback<Unit> {
|
||||
private fun eventuallyRestartBackup() {
|
||||
// Do not stay in KeysBackupState.Unknown but check what is available on the homeserver
|
||||
if (state == KeysBackupState.Unknown) {
|
||||
checkAndStartKeysBackup()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSuccess(data: Unit) {
|
||||
eventuallyRestartBackup()
|
||||
|
||||
uiHandler.post { callback?.onSuccess(Unit) }
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
eventuallyRestartBackup()
|
||||
|
||||
uiHandler.post { callback?.onFailure(failure) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSuccess(data: Unit) {
|
||||
eventuallyRestartBackup()
|
||||
|
||||
uiHandler.post { callback?.onSuccess(Unit) }
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
eventuallyRestartBackup()
|
||||
|
||||
uiHandler.post { callback?.onFailure(failure) }
|
||||
}
|
||||
})
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
}
|
||||
@@ -355,15 +354,14 @@ internal class KeysBackup @Inject constructor(
|
||||
callback: MatrixCallback<KeysBackupVersionTrust>) {
|
||||
// TODO Validate with François that this is correct
|
||||
object : Task<KeysVersionResult, KeysBackupVersionTrust> {
|
||||
override suspend fun execute(params: KeysVersionResult): Try<KeysBackupVersionTrust> {
|
||||
return Try {
|
||||
getKeysBackupTrustBg(params)
|
||||
}
|
||||
override suspend fun execute(params: KeysVersionResult): KeysBackupVersionTrust {
|
||||
return getKeysBackupTrustBg(params)
|
||||
}
|
||||
}
|
||||
.configureWith(keysBackupVersion)
|
||||
.dispatchTo(callback)
|
||||
.executeOn(TaskThread.COMPUTATION)
|
||||
.configureWith(keysBackupVersion) {
|
||||
this.callback = callback
|
||||
this.executionThread = TaskThread.COMPUTATION
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
@@ -454,7 +452,8 @@ internal class KeysBackup @Inject constructor(
|
||||
val myUserId = credentials.userId
|
||||
|
||||
// Get current signatures, or create an empty set
|
||||
val myUserSignatures = authData.signatures?.get(myUserId)?.toMutableMap() ?: HashMap()
|
||||
val myUserSignatures = authData.signatures?.get(myUserId)?.toMutableMap()
|
||||
?: HashMap()
|
||||
|
||||
if (trust) {
|
||||
// Add current device signature
|
||||
@@ -492,27 +491,28 @@ internal class KeysBackup @Inject constructor(
|
||||
|
||||
// And send it to the homeserver
|
||||
updateKeysBackupVersionTask
|
||||
.configureWith(UpdateKeysBackupVersionTask.Params(keysBackupVersion.version!!, updateKeysBackupVersionBody))
|
||||
.dispatchTo(object : MatrixCallback<Unit> {
|
||||
override fun onSuccess(data: Unit) {
|
||||
// Relaunch the state machine on this updated backup version
|
||||
val newKeysBackupVersion = KeysVersionResult()
|
||||
.configureWith(UpdateKeysBackupVersionTask.Params(keysBackupVersion.version!!, updateKeysBackupVersionBody)) {
|
||||
this.callback = object : MatrixCallback<Unit> {
|
||||
override fun onSuccess(data: Unit) {
|
||||
// Relaunch the state machine on this updated backup version
|
||||
val newKeysBackupVersion = KeysVersionResult()
|
||||
|
||||
newKeysBackupVersion.version = keysBackupVersion.version
|
||||
newKeysBackupVersion.algorithm = keysBackupVersion.algorithm
|
||||
newKeysBackupVersion.count = keysBackupVersion.count
|
||||
newKeysBackupVersion.hash = keysBackupVersion.hash
|
||||
newKeysBackupVersion.authData = updateKeysBackupVersionBody.authData
|
||||
newKeysBackupVersion.version = keysBackupVersion.version
|
||||
newKeysBackupVersion.algorithm = keysBackupVersion.algorithm
|
||||
newKeysBackupVersion.count = keysBackupVersion.count
|
||||
newKeysBackupVersion.hash = keysBackupVersion.hash
|
||||
newKeysBackupVersion.authData = updateKeysBackupVersionBody.authData
|
||||
|
||||
checkAndStartWithKeysBackupVersion(newKeysBackupVersion)
|
||||
checkAndStartWithKeysBackupVersion(newKeysBackupVersion)
|
||||
|
||||
callback.onSuccess(data)
|
||||
callback.onSuccess(data)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
callback.onFailure(failure)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
callback.onFailure(failure)
|
||||
}
|
||||
})
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
}
|
||||
@@ -758,49 +758,52 @@ internal class KeysBackup @Inject constructor(
|
||||
if (roomId != null && sessionId != null) {
|
||||
// Get key for the room and for the session
|
||||
getRoomSessionDataTask
|
||||
.configureWith(GetRoomSessionDataTask.Params(roomId, sessionId, version))
|
||||
.dispatchTo(object : MatrixCallback<KeyBackupData> {
|
||||
override fun onSuccess(data: KeyBackupData) {
|
||||
// Convert to KeysBackupData
|
||||
val keysBackupData = KeysBackupData()
|
||||
keysBackupData.roomIdToRoomKeysBackupData = HashMap()
|
||||
val roomKeysBackupData = RoomKeysBackupData()
|
||||
roomKeysBackupData.sessionIdToKeyBackupData = HashMap()
|
||||
roomKeysBackupData.sessionIdToKeyBackupData[sessionId] = data
|
||||
keysBackupData.roomIdToRoomKeysBackupData[roomId] = roomKeysBackupData
|
||||
.configureWith(GetRoomSessionDataTask.Params(roomId, sessionId, version)) {
|
||||
this.callback = object : MatrixCallback<KeyBackupData> {
|
||||
override fun onSuccess(data: KeyBackupData) {
|
||||
// Convert to KeysBackupData
|
||||
val keysBackupData = KeysBackupData()
|
||||
keysBackupData.roomIdToRoomKeysBackupData = HashMap()
|
||||
val roomKeysBackupData = RoomKeysBackupData()
|
||||
roomKeysBackupData.sessionIdToKeyBackupData = HashMap()
|
||||
roomKeysBackupData.sessionIdToKeyBackupData[sessionId] = data
|
||||
keysBackupData.roomIdToRoomKeysBackupData[roomId] = roomKeysBackupData
|
||||
|
||||
callback.onSuccess(keysBackupData)
|
||||
}
|
||||
callback.onSuccess(keysBackupData)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
callback.onFailure(failure)
|
||||
override fun onFailure(failure: Throwable) {
|
||||
callback.onFailure(failure)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
} else if (roomId != null) {
|
||||
// Get all keys for the room
|
||||
getRoomSessionsDataTask
|
||||
.configureWith(GetRoomSessionsDataTask.Params(roomId, version))
|
||||
.dispatchTo(object : MatrixCallback<RoomKeysBackupData> {
|
||||
override fun onSuccess(data: RoomKeysBackupData) {
|
||||
// Convert to KeysBackupData
|
||||
val keysBackupData = KeysBackupData()
|
||||
keysBackupData.roomIdToRoomKeysBackupData = HashMap()
|
||||
keysBackupData.roomIdToRoomKeysBackupData[roomId] = data
|
||||
.configureWith(GetRoomSessionsDataTask.Params(roomId, version)) {
|
||||
this.callback = object : MatrixCallback<RoomKeysBackupData> {
|
||||
override fun onSuccess(data: RoomKeysBackupData) {
|
||||
// Convert to KeysBackupData
|
||||
val keysBackupData = KeysBackupData()
|
||||
keysBackupData.roomIdToRoomKeysBackupData = HashMap()
|
||||
keysBackupData.roomIdToRoomKeysBackupData[roomId] = data
|
||||
|
||||
callback.onSuccess(keysBackupData)
|
||||
}
|
||||
callback.onSuccess(keysBackupData)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
callback.onFailure(failure)
|
||||
override fun onFailure(failure: Throwable) {
|
||||
callback.onFailure(failure)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
} else {
|
||||
// Get all keys
|
||||
getSessionsDataTask
|
||||
.configureWith(GetSessionsDataTask.Params(version))
|
||||
.dispatchTo(callback)
|
||||
.configureWith(GetSessionsDataTask.Params(version)) {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
}
|
||||
@@ -842,7 +845,7 @@ internal class KeysBackup @Inject constructor(
|
||||
// Wait between 0 and 10 seconds, to avoid backup requests from
|
||||
// different clients hitting the server all at the same time when a
|
||||
// new key is sent
|
||||
val delayInMs = random.nextInt(KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS).toLong()
|
||||
val delayInMs = Random.nextLong(KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS)
|
||||
|
||||
uiHandler.postDelayed({ backupKeys() }, delayInMs)
|
||||
}
|
||||
@@ -855,45 +858,47 @@ internal class KeysBackup @Inject constructor(
|
||||
override fun getVersion(version: String,
|
||||
callback: MatrixCallback<KeysVersionResult?>) {
|
||||
getKeysBackupVersionTask
|
||||
.configureWith(version)
|
||||
.dispatchTo(object : MatrixCallback<KeysVersionResult> {
|
||||
override fun onSuccess(data: KeysVersionResult) {
|
||||
callback.onSuccess(data)
|
||||
}
|
||||
.configureWith(version) {
|
||||
this.callback = object : MatrixCallback<KeysVersionResult> {
|
||||
override fun onSuccess(data: KeysVersionResult) {
|
||||
callback.onSuccess(data)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
if (failure is Failure.ServerError
|
||||
&& failure.error.code == MatrixError.NOT_FOUND) {
|
||||
// Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup
|
||||
callback.onSuccess(null)
|
||||
} else {
|
||||
// Transmit the error
|
||||
callback.onFailure(failure)
|
||||
override fun onFailure(failure: Throwable) {
|
||||
if (failure is Failure.ServerError
|
||||
&& failure.error.code == MatrixError.NOT_FOUND) {
|
||||
// Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup
|
||||
callback.onSuccess(null)
|
||||
} else {
|
||||
// Transmit the error
|
||||
callback.onFailure(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun getCurrentVersion(callback: MatrixCallback<KeysVersionResult?>) {
|
||||
getKeysBackupLastVersionTask
|
||||
.toConfigurableTask()
|
||||
.dispatchTo(object : MatrixCallback<KeysVersionResult> {
|
||||
override fun onSuccess(data: KeysVersionResult) {
|
||||
callback.onSuccess(data)
|
||||
}
|
||||
.configureWith {
|
||||
this.callback = object : MatrixCallback<KeysVersionResult> {
|
||||
override fun onSuccess(data: KeysVersionResult) {
|
||||
callback.onSuccess(data)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
if (failure is Failure.ServerError
|
||||
&& failure.error.code == MatrixError.NOT_FOUND) {
|
||||
// Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup
|
||||
callback.onSuccess(null)
|
||||
} else {
|
||||
// Transmit the error
|
||||
callback.onFailure(failure)
|
||||
override fun onFailure(failure: Throwable) {
|
||||
if (failure is Failure.ServerError
|
||||
&& failure.error.code == MatrixError.NOT_FOUND) {
|
||||
// Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup
|
||||
callback.onSuccess(null)
|
||||
} else {
|
||||
// Transmit the error
|
||||
callback.onFailure(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
@@ -1236,69 +1241,72 @@ internal class KeysBackup @Inject constructor(
|
||||
|
||||
Timber.v("backupKeys: 4 - Sending request")
|
||||
|
||||
// Make the request
|
||||
storeSessionDataTask
|
||||
.configureWith(StoreSessionsDataTask.Params(keysBackupVersion!!.version!!, keysBackupData))
|
||||
.dispatchTo(object : MatrixCallback<BackupKeysResult> {
|
||||
override fun onSuccess(data: BackupKeysResult) {
|
||||
uiHandler.post {
|
||||
Timber.v("backupKeys: 5a - Request complete")
|
||||
val sendingRequestCallback = object : MatrixCallback<BackupKeysResult> {
|
||||
override fun onSuccess(data: BackupKeysResult) {
|
||||
uiHandler.post {
|
||||
Timber.v("backupKeys: 5a - Request complete")
|
||||
|
||||
// Mark keys as backed up
|
||||
cryptoStore.markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers)
|
||||
// Mark keys as backed up
|
||||
cryptoStore.markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers)
|
||||
|
||||
if (olmInboundGroupSessionWrappers.size < KEY_BACKUP_SEND_KEYS_MAX_COUNT) {
|
||||
Timber.v("backupKeys: All keys have been backed up")
|
||||
onServerDataRetrieved(data.count, data.hash)
|
||||
if (olmInboundGroupSessionWrappers.size < KEY_BACKUP_SEND_KEYS_MAX_COUNT) {
|
||||
Timber.v("backupKeys: All keys have been backed up")
|
||||
onServerDataRetrieved(data.count, data.hash)
|
||||
|
||||
// Note: Changing state will trigger the call to backupAllGroupSessionsCallback.onSuccess()
|
||||
keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
|
||||
} else {
|
||||
Timber.v("backupKeys: Continue to back up keys")
|
||||
keysBackupStateManager.state = KeysBackupState.WillBackUp
|
||||
// Note: Changing state will trigger the call to backupAllGroupSessionsCallback.onSuccess()
|
||||
keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
|
||||
} else {
|
||||
Timber.v("backupKeys: Continue to back up keys")
|
||||
keysBackupStateManager.state = KeysBackupState.WillBackUp
|
||||
|
||||
backupKeys()
|
||||
}
|
||||
}
|
||||
backupKeys()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
if (failure is Failure.ServerError) {
|
||||
uiHandler.post {
|
||||
Timber.e(failure, "backupKeys: backupKeys failed.")
|
||||
override fun onFailure(failure: Throwable) {
|
||||
if (failure is Failure.ServerError) {
|
||||
uiHandler.post {
|
||||
Timber.e(failure, "backupKeys: backupKeys failed.")
|
||||
|
||||
when (failure.error.code) {
|
||||
MatrixError.NOT_FOUND,
|
||||
MatrixError.WRONG_ROOM_KEYS_VERSION -> {
|
||||
// Backup has been deleted on the server, or we are not using the last backup version
|
||||
keysBackupStateManager.state = KeysBackupState.WrongBackUpVersion
|
||||
backupAllGroupSessionsCallback?.onFailure(failure)
|
||||
resetBackupAllGroupSessionsListeners()
|
||||
resetKeysBackupData()
|
||||
keysBackupVersion = null
|
||||
|
||||
// Do not stay in KeysBackupState.WrongBackUpVersion but check what is available on the homeserver
|
||||
checkAndStartKeysBackup()
|
||||
}
|
||||
else ->
|
||||
// Come back to the ready state so that we will retry on the next received key
|
||||
keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
|
||||
}
|
||||
}
|
||||
} else {
|
||||
uiHandler.post {
|
||||
when (failure.error.code) {
|
||||
MatrixError.NOT_FOUND,
|
||||
MatrixError.WRONG_ROOM_KEYS_VERSION -> {
|
||||
// Backup has been deleted on the server, or we are not using the last backup version
|
||||
keysBackupStateManager.state = KeysBackupState.WrongBackUpVersion
|
||||
backupAllGroupSessionsCallback?.onFailure(failure)
|
||||
resetBackupAllGroupSessionsListeners()
|
||||
resetKeysBackupData()
|
||||
keysBackupVersion = null
|
||||
|
||||
Timber.e("backupKeys: backupKeys failed.")
|
||||
|
||||
// Retry a bit later
|
||||
keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
|
||||
maybeBackupKeys()
|
||||
// Do not stay in KeysBackupState.WrongBackUpVersion but check what is available on the homeserver
|
||||
checkAndStartKeysBackup()
|
||||
}
|
||||
else ->
|
||||
// Come back to the ready state so that we will retry on the next received key
|
||||
keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
uiHandler.post {
|
||||
backupAllGroupSessionsCallback?.onFailure(failure)
|
||||
resetBackupAllGroupSessionsListeners()
|
||||
|
||||
Timber.e("backupKeys: backupKeys failed.")
|
||||
|
||||
// Retry a bit later
|
||||
keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
|
||||
maybeBackupKeys()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make the request
|
||||
storeSessionDataTask
|
||||
.configureWith(StoreSessionsDataTask.Params(keysBackupVersion!!.version!!, keysBackupData)) {
|
||||
this.callback = sendingRequestCallback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
}
|
||||
@@ -1394,7 +1402,7 @@ internal class KeysBackup @Inject constructor(
|
||||
|
||||
companion object {
|
||||
// Maximum delay in ms in {@link maybeBackupKeys}
|
||||
private const val KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS = 10000
|
||||
private const val KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS = 10_000L
|
||||
|
||||
// Maximum number of keys to send at a time to the homeserver.
|
||||
private const val KEY_BACKUP_SEND_KEYS_MAX_COUNT = 100
|
||||
|
@@ -22,7 +22,7 @@ package im.vector.matrix.android.internal.crypto.keysbackup
|
||||
import androidx.annotation.WorkerThread
|
||||
import im.vector.matrix.android.api.listeners.ProgressListener
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import kotlin.experimental.xor
|
||||
@@ -142,12 +142,11 @@ private fun deriveKey(password: String,
|
||||
* Generate a 32 chars salt
|
||||
*/
|
||||
private fun generateSalt(): String {
|
||||
var salt = ""
|
||||
|
||||
do {
|
||||
salt += UUID.randomUUID().toString()
|
||||
} while (salt.length < SALT_LENGTH)
|
||||
|
||||
val salt = buildString {
|
||||
do {
|
||||
append(UUID.randomUUID().toString())
|
||||
} while (length < SALT_LENGTH)
|
||||
}
|
||||
|
||||
return salt.substring(0, SALT_LENGTH)
|
||||
}
|
@@ -20,7 +20,6 @@ import android.os.Handler
|
||||
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
|
||||
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
internal class KeysBackupStateManager(private val uiHandler: Handler) {
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user