Merge branch 'release/0.21.0'

This commit is contained in:
Benoit Marty 2020-05-28 10:43:55 +02:00
commit d1c4d4b099
411 changed files with 13503 additions and 1602 deletions

View file

@ -12,12 +12,15 @@
<w>fdroid</w>
<w>gplay</w>
<w>hmac</w>
<w>homeserver</w>
<w>ktlint</w>
<w>linkified</w>
<w>linkify</w>
<w>megolm</w>
<w>msisdn</w>
<w>msisdns</w>
<w>pbkdf</w>
<w>pids</w>
<w>pkcs</w>
<w>riotx</w>
<w>signin</w>

View file

@ -1,3 +1,30 @@
Changes in RiotX 0.21.0 (2020-05-28)
===================================================
Features ✨:
- Identity server support (#607)
- Switch language support (#41)
- Display list of attachments of a room (#860)
Improvements 🙌:
- Better connectivity lost indicator when airplane mode is on
- Add a setting to hide redacted events (#951)
- Render formatted_body for m.notice and m.emote (#1196)
- Change icon to magnifying-glass to filter room (#1384)
Bugfix 🐛:
- After jump to unread, newer messages are never loaded (#1008)
- Fix issues with FontScale switch (#69, #645)
- "Seen by" uses 12h time (#1378)
- Enable markdown (if active) when sending emote (#734)
- Screenshots for Rageshake now includes Dialogs such as BottomSheet (#1349)
SDK API changes ⚠️:
- initialize with proxy configuration
Other changes:
- support new key agreement method for SAS (#1374)
Changes in RiotX 0.20.0 (2020-05-15)
===================================================

92
docs/identity_server.md Normal file
View file

@ -0,0 +1,92 @@
# Identity server
Issue: #607
PR: #1354
## Introduction
Identity Servers support contact discovery on Matrix by letting people look up Third Party Identifiers to see if the owner has publicly linked them with their Matrix ID.
## Implementation
The current implementation was Inspired by the code from Riot-Android.
Difference though (list not exhaustive):
- Only API v2 is supported (see https://matrix.org/docs/spec/identity_service/latest)
- Homeserver has to be up to date to support binding (Versions.isLoginAndRegistrationSupportedBySdk() has to return true)
- The SDK managed the session and client secret when binding ThreePid. Those data are not exposed to the client.
- The SDK supports incremental sendAttempt (this is not used by RiotX)
- The "Continue" button is now under the information, and not as the same place that the checkbox
- The app can cancel a binding. Current data are erased from DB.
- The API (IdentityService) is improved.
- A new DB to store data related to the identity server management.
Missing features (list not exhaustive):
- Invite by 3Pid (will be in a dedicated PR)
- Add email or phone to account (not P1, can be done on Riot-Web)
- List email and phone of the account (could be done in a dedicated PR)
- Search contact (not P1)
- Logout from identity server when user sign out or deactivate his account.
## Related MSCs
The list can be found here: https://matrix.org/blog/2019/09/27/privacy-improvements-in-synapse-1-4-and-riot-1-4
## Steps and requirements
- Only one identity server by account can be set. The user's choice is stored in account data with key `m.identity_server`. But every clients will managed its own token to log in to the identity server
```json
{
"type": "m.identity_server",
"content": {
"base_url": "https://matrix.org"
}
}
```
- The accepted terms are stored in the account data:
```json
{
"type": "m.accepted_terms",
"content": {
"accepted": [
"https://vector.im/identity-server-privacy-notice-1"
]
}
}
```
- Default identity server URL, from Wellknown data is proposed to the user.
- Identity server can be set
- Identity server can be changed on another user's device, so when the change is detected (thanks to account data sync) RiotX should properly disconnect from a previous identity server (I think it was not the case in Riot-Android, where we keep the token forever)
- Registration to the identity server is managed with an openId token
- Terms of service can be accepted when configuring the identity server.
- Terms of service can be accepted after, if they change.
- Identity server can be modified
- Identity server can be disconnected with a warning dialog, with special content if there are current bound 3pid on this identity server.
- Email can be bound
- Email can be unbound
- Phone can be bound
- Phone can be unbound
- Look up can be performed, to get matrixIds from local contact book (phone and email): Android permission correctly handled (not done yet)
- Look up pepper can be updated if it is rotated on the identity server
- Invitation using 3PID can be done (See #548) (not done yet)
- Homeserver access-token will never be sent to an identity server
- When user sign-out: logout from the identity server if any.
- When user deactivate account: logout from the identity server if any.
## Screens
### Settings
Identity server settings can be accessed from the internal setting of the application, both from "Discovery" section and from identity detail section.
### Discovery screen
This screen displays the identity server configuration and the binding of the user's ThreePid (email and msisdn). This is the main screen of the feature.
### Set identity server screen
This screen is a form to set a new identity server URL
## Ref:
- https://matrix.org/blog/2019/09/27/privacy-improvements-in-synapse-1-4-and-riot-1-4 is a good summary of the role of an Identity server and the proper way to configure and use it in respect to the privacy and the consent of the user.
- API documentation: https://matrix.org/docs/spec/identity_service/latest
- vector.im TOS: https://vector.im/identity-server-privacy-notice

View file

@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
import im.vector.matrix.android.api.session.group.GroupSummaryQueryParams
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.identity.ThreePid
import im.vector.matrix.android.api.session.pushers.Pusher
import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams
import im.vector.matrix.android.api.session.room.model.RoomSummary
@ -94,6 +95,11 @@ class RxSession(private val session: Session) {
return session.getPagedUsersLive(filter, excludedUserIds).asObservable()
}
fun liveThreePIds(refreshData: Boolean): Observable<List<ThreePid>> {
return session.getThreePidsLive(refreshData).asObservable()
.startWithCallable { session.getThreePids() }
}
fun createRoom(roomParams: CreateRoomParams): Single<String> = singleBuilder {
session.createRoom(roomParams, it)
}

View file

@ -158,6 +158,9 @@ dependencies {
// Bus
implementation 'org.greenrobot:eventbus:3.1.1'
// Phone number https://github.com/google/libphonenumber
implementation 'com.googlecode.libphonenumber:libphonenumber:8.10.23'
debugImplementation 'com.airbnb.okreplay:okreplay:1.5.0'
releaseImplementation 'com.airbnb.okreplay:noop:1.5.0'
androidTestImplementation 'com.airbnb.okreplay:espresso:1.5.0'

View file

@ -28,10 +28,10 @@ import im.vector.matrix.android.api.auth.data.LoginFlowResult
import im.vector.matrix.android.api.auth.registration.RegistrationResult
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.LocalEcho
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
@ -88,7 +88,8 @@ class CommonTestHelper(context: Context) {
fun syncSession(session: Session) {
val lock = CountDownLatch(1)
session.open()
GlobalScope.launch(Dispatchers.Main) { session.open() }
session.startSync(true)
val syncLiveData = runBlocking(Dispatchers.Main) {
@ -116,7 +117,7 @@ class CommonTestHelper(context: Context) {
*/
fun sendTextMessage(room: Room, message: String, nbOfMessages: Int): List<TimelineEvent> {
val sentEvents = ArrayList<TimelineEvent>(nbOfMessages)
val latch = CountDownLatch(nbOfMessages)
val latch = CountDownLatch(1)
val timelineListener = object : Timeline.Listener {
override fun onTimelineFailure(throwable: Throwable) {
}
@ -127,7 +128,7 @@ class CommonTestHelper(context: Context) {
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
val newMessages = snapshot
.filter { LocalEcho.isLocalEchoId(it.eventId).not() }
.filter { it.root.sendState == SendState.SYNCED }
.filter { it.root.getClearType() == EventType.MESSAGE }
.filter { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(message) == true }
@ -143,7 +144,8 @@ class CommonTestHelper(context: Context) {
for (i in 0 until nbOfMessages) {
room.sendTextMessage(message + " #" + (i + 1))
}
await(latch)
// Wait 3 second more per message
await(latch, timeout = TestConstants.timeOutMillis + 3_000L * nbOfMessages)
timeline.removeListener(timelineListener)
timeline.dispose()
@ -291,6 +293,24 @@ class CommonTestHelper(context: Context) {
return requestFailure!!
}
fun createEventListener(latch: CountDownLatch, predicate: (List<TimelineEvent>) -> Boolean): Timeline.Listener {
return object : Timeline.Listener {
override fun onTimelineFailure(throwable: Throwable) {
// noop
}
override fun onNewTimelineEvents(eventIds: List<String>) {
// noop
}
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
if (predicate(snapshot)) {
latch.countDown()
}
}
}
}
/**
* Await for a latch and ensure the result is true
*
@ -349,3 +369,13 @@ class CommonTestHelper(context: Context) {
session.close()
}
}
fun List<TimelineEvent>.checkSendOrder(baseTextMessage: String, numberOfMessages: Int, startIndex: Int): Boolean {
return drop(startIndex)
.take(numberOfMessages)
.foldRightIndexed(true) { index, timelineEvent, acc ->
val body = timelineEvent.root.content.toModel<MessageContent>()?.body
val currentMessageSuffix = numberOfMessages - index
acc && (body == null || body.startsWith(baseTextMessage) && body.endsWith("#$currentMessageSuffix"))
}
}

View file

@ -53,17 +53,19 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
/**
* @return alice session
*/
fun doE2ETestWithAliceInARoom(): CryptoTestData {
fun doE2ETestWithAliceInARoom(encryptedRoom: Boolean = true): CryptoTestData {
val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
val roomId = mTestHelper.doSync<String> {
aliceSession.createRoom(CreateRoomParams(name = "MyRoom"), it)
}
val room = aliceSession.getRoom(roomId)!!
if (encryptedRoom) {
val room = aliceSession.getRoom(roomId)!!
mTestHelper.doSync<Unit> {
room.enableEncryption(callback = it)
mTestHelper.doSync<Unit> {
room.enableEncryption(callback = it)
}
}
return CryptoTestData(aliceSession, roomId)
@ -72,8 +74,8 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
/**
* @return alice and bob sessions
*/
fun doE2ETestWithAliceAndBobInARoom(): CryptoTestData {
val cryptoTestData = doE2ETestWithAliceInARoom()
fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true): CryptoTestData {
val cryptoTestData = doE2ETestWithAliceInARoom(encryptedRoom)
val aliceSession = cryptoTestData.firstSession
val aliceRoomId = cryptoTestData.roomId
@ -246,7 +248,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
assertNotNull(eventWireContent.get("session_id"))
assertNotNull(eventWireContent.get("sender_key"))
assertEquals(senderSession.sessionParams.credentials.deviceId, eventWireContent.get("device_id"))
assertEquals(senderSession.sessionParams.deviceId, eventWireContent.get("device_id"))
assertNotNull(event.eventId)
assertEquals(roomId, event.roomId)

View file

@ -122,7 +122,7 @@ class XSigningTest : InstrumentedTest {
// We will want to test that in alice POV, this new device would be trusted by cross signing
val bobSession2 = mTestHelper.logIntoAccount(bobUserId, SessionTestParams(true))
val bobSecondDeviceId = bobSession2.sessionParams.credentials.deviceId!!
val bobSecondDeviceId = bobSession2.sessionParams.deviceId!!
// Check that bob first session sees the new login
val data = mTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> {

View file

@ -148,7 +148,7 @@ class KeyShareTests : InstrumentedTest {
// Mark the device as trusted
aliceSession.cryptoService().setDeviceVerification(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession.myUserId,
aliceSession2.sessionParams.credentials.deviceId ?: "")
aliceSession2.sessionParams.deviceId ?: "")
// Re request
aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root)
@ -253,12 +253,12 @@ class KeyShareTests : InstrumentedTest {
})
val txId: String = "m.testVerif12"
aliceVerificationService2.beginKeyVerification(VerificationMethod.SAS, aliceSession1.myUserId, aliceSession1.sessionParams.credentials.deviceId
aliceVerificationService2.beginKeyVerification(VerificationMethod.SAS, aliceSession1.myUserId, aliceSession1.sessionParams.deviceId
?: "", txId)
mTestHelper.waitWithLatch { latch ->
mTestHelper.retryPeriodicallyWithLatch(latch) {
aliceSession1.cryptoService().getDeviceInfo(aliceSession1.myUserId, aliceSession2.sessionParams.credentials.deviceId ?: "")?.isVerified == true
aliceSession1.cryptoService().getDeviceInfo(aliceSession1.myUserId, aliceSession2.sessionParams.deviceId ?: "")?.isVerified == true
}
}

View file

@ -835,7 +835,7 @@ class KeysBackupTest : InstrumentedTest {
assertTrue(signature.valid)
assertNotNull(signature.device)
assertEquals(cryptoTestData.firstSession.cryptoService().getMyDevice().deviceId, signature.deviceId)
assertEquals(signature.device!!.deviceId, cryptoTestData.firstSession.sessionParams.credentials.deviceId)
assertEquals(signature.device!!.deviceId, cryptoTestData.firstSession.sessionParams.deviceId)
stateObserver.stopAndCheckStates(null)
cryptoTestData.cleanUp(mTestHelper)
@ -997,7 +997,7 @@ class KeysBackupTest : InstrumentedTest {
keysBackup.backupAllGroupSessions(null, it)
}
val oldDeviceId = cryptoTestData.firstSession.sessionParams.credentials.deviceId!!
val oldDeviceId = cryptoTestData.firstSession.sessionParams.deviceId!!
val oldKeyBackupVersion = keysBackup.currentBackupVersion
val aliceUserId = cryptoTestData.firstSession.myUserId

View file

@ -468,14 +468,19 @@ class SASTest : InstrumentedTest {
val aliceSASLatch = CountDownLatch(1)
val aliceListener = object : VerificationService.Listener {
var matchOnce = true
override fun transactionUpdated(tx: VerificationTransaction) {
val uxState = (tx as OutgoingSasVerificationTransaction).uxState
Log.v("TEST", "== aliceState ${uxState.name}")
when (uxState) {
OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> {
tx.userHasVerifiedShortCode()
}
OutgoingSasVerificationTransaction.UxState.VERIFIED -> {
aliceSASLatch.countDown()
if (matchOnce) {
matchOnce = false
aliceSASLatch.countDown()
}
}
else -> Unit
}
@ -485,14 +490,23 @@ class SASTest : InstrumentedTest {
val bobSASLatch = CountDownLatch(1)
val bobListener = object : VerificationService.Listener {
var acceptOnce = true
var matchOnce = true
override fun transactionUpdated(tx: VerificationTransaction) {
val uxState = (tx as IncomingSasVerificationTransaction).uxState
Log.v("TEST", "== bobState ${uxState.name}")
when (uxState) {
IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> {
tx.performAccept()
if (acceptOnce) {
acceptOnce = false
tx.performAccept()
}
}
IncomingSasVerificationTransaction.UxState.SHOW_SAS -> {
tx.userHasVerifiedShortCode()
if (matchOnce) {
matchOnce = false
tx.userHasVerifiedShortCode()
}
}
IncomingSasVerificationTransaction.UxState.VERIFIED -> {
bobSASLatch.countDown()
@ -579,7 +593,7 @@ class SASTest : InstrumentedTest {
requestID!!,
cryptoTestData.roomId,
bobSession.myUserId,
bobSession.sessionParams.credentials.deviceId!!,
bobSession.sessionParams.deviceId!!,
null)
bobVerificationService.beginKeyVerificationInDMs(
@ -587,7 +601,7 @@ class SASTest : InstrumentedTest {
requestID!!,
cryptoTestData.roomId,
aliceSession.myUserId,
aliceSession.sessionParams.credentials.deviceId!!,
aliceSession.sessionParams.deviceId!!,
null)
// we should reach SHOW SAS on both

View file

@ -0,0 +1,183 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.session.room.timeline
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.api.extensions.orFalse
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
import im.vector.matrix.android.common.CommonTestHelper
import im.vector.matrix.android.common.CryptoTestHelper
import im.vector.matrix.android.common.checkSendOrder
import org.amshove.kluent.shouldBeFalse
import org.amshove.kluent.shouldBeTrue
import org.junit.Assert.assertTrue
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.junit.runners.MethodSorters
import timber.log.Timber
import java.util.concurrent.CountDownLatch
@RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
class TimelineBackToPreviousLastForwardTest : InstrumentedTest {
private val commonTestHelper = CommonTestHelper(context())
private val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
/**
* This test ensure that if we have a chunk in the timeline which is due to a sync, and we click to permalink of an
* even contained in a previous lastForward chunk, we will be able to go back to the live
*/
@Test
fun backToPreviousLastForwardTest() {
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
val aliceSession = cryptoTestData.firstSession
val bobSession = cryptoTestData.secondSession!!
val aliceRoomId = cryptoTestData.roomId
aliceSession.cryptoService().setWarnOnUnknownDevices(false)
bobSession.cryptoService().setWarnOnUnknownDevices(false)
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!!
val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(30))
bobTimeline.start()
var roomCreationEventId: String? = null
run {
val lock = CountDownLatch(1)
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
snapshot.forEach {
Timber.w(" event ${it.root}")
}
roomCreationEventId = snapshot.lastOrNull()?.root?.eventId
// Ok, we have the 8 first messages of the initial sync (room creation and bob join event)
snapshot.size == 8
}
bobTimeline.addListener(eventsListener)
commonTestHelper.await(lock)
bobTimeline.removeAllListeners()
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
}
// Bob stop to sync
bobSession.stopSync()
val messageRoot = "First messages from Alice"
// Alice sends 30 messages
commonTestHelper.sendTextMessage(
roomFromAlicePOV,
messageRoot,
30)
// Bob start to sync
bobSession.startSync(true)
run {
val lock = CountDownLatch(1)
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
snapshot.forEach {
Timber.w(" event ${it.root}")
}
// Ok, we have the 10 last messages from Alice.
snapshot.size == 10
&& snapshot.all { it.root.content.toModel<MessageContent>()?.body?.startsWith(messageRoot).orFalse() }
}
bobTimeline.addListener(eventsListener)
commonTestHelper.await(lock)
bobTimeline.removeAllListeners()
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue()
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
}
// Bob navigate to the first event (room creation event), so inside the previous last forward chunk
run {
val lock = CountDownLatch(1)
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
snapshot.forEach {
Timber.w(" event ${it.root}")
}
// The event is in db, so it is fetch and auto pagination occurs, half of the number of events we have for this chunk (?)
snapshot.size == 4
}
bobTimeline.addListener(eventsListener)
// Restart the timeline to the first sent event, which is already in the database, so pagination should start automatically
assertTrue(roomFromBobPOV.getTimeLineEvent(roomCreationEventId!!) != null)
bobTimeline.restartWithEventId(roomCreationEventId)
commonTestHelper.await(lock)
bobTimeline.removeAllListeners()
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue()
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
}
// Bob scroll to the future
run {
val lock = CountDownLatch(1)
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
snapshot.forEach {
Timber.w(" event ${it.root}")
}
// Bob can see the first event of the room (so Back pagination has worked)
snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE
// 8 for room creation item, and 30 for the forward pagination
&& snapshot.size == 38
&& snapshot.checkSendOrder(messageRoot, 30, 0)
}
bobTimeline.addListener(eventsListener)
bobTimeline.paginate(Timeline.Direction.FORWARDS, 50)
commonTestHelper.await(lock)
bobTimeline.removeAllListeners()
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
}
bobTimeline.dispose()
cryptoTestData.cleanUp(commonTestHelper)
}
}

View file

@ -0,0 +1,190 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.session.room.timeline
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.api.extensions.orFalse
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
import im.vector.matrix.android.common.CommonTestHelper
import im.vector.matrix.android.common.CryptoTestHelper
import im.vector.matrix.android.common.checkSendOrder
import org.amshove.kluent.shouldBeFalse
import org.amshove.kluent.shouldBeTrue
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.junit.runners.MethodSorters
import timber.log.Timber
import java.util.concurrent.CountDownLatch
@RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
class TimelineForwardPaginationTest : InstrumentedTest {
private val commonTestHelper = CommonTestHelper(context())
private val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
/**
* This test ensure that if we click to permalink, we will be able to go back to the live
*/
@Test
fun forwardPaginationTest() {
val numberOfMessagesToSend = 90
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
val aliceSession = cryptoTestData.firstSession
val aliceRoomId = cryptoTestData.roomId
aliceSession.cryptoService().setWarnOnUnknownDevices(false)
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
// Alice sends X messages
val message = "Message from Alice"
val sentMessages = commonTestHelper.sendTextMessage(
roomFromAlicePOV,
message,
numberOfMessagesToSend)
// Alice clear the cache
commonTestHelper.doSync<Unit> {
aliceSession.clearCache(it)
}
// And restarts the sync
aliceSession.startSync(true)
val aliceTimeline = roomFromAlicePOV.createTimeline(null, TimelineSettings(30))
aliceTimeline.start()
// Alice sees the 10 last message of the room, and can only navigate BACKWARD
run {
val lock = CountDownLatch(1)
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
Timber.e("Alice timeline updated: with ${snapshot.size} events:")
snapshot.forEach {
Timber.w(" event ${it.root.content}")
}
// Ok, we have the 10 last messages of the initial sync
snapshot.size == 10
&& snapshot.all { it.root.content.toModel<MessageContent>()?.body?.startsWith(message).orFalse() }
}
// Open the timeline at last sent message
aliceTimeline.addListener(eventsListener)
commonTestHelper.await(lock)
aliceTimeline.removeAllListeners()
aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue()
aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
}
// Alice navigates to the first message of the room, which is not in its database. A GET /context is performed
// Then she can paginate BACKWARD and FORWARD
run {
val lock = CountDownLatch(1)
val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
Timber.e("Alice timeline updated: with ${snapshot.size} events:")
snapshot.forEach {
Timber.w(" event ${it.root.content}")
}
// The event is not in db, so it is fetch alone
snapshot.size == 1
&& snapshot.all { it.root.content.toModel<MessageContent>()?.body?.startsWith("Message from Alice").orFalse() }
}
aliceTimeline.addListener(aliceEventsListener)
// Restart the timeline to the first sent event
aliceTimeline.restartWithEventId(sentMessages.last().eventId)
commonTestHelper.await(lock)
aliceTimeline.removeAllListeners()
aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue()
aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue()
}
// Alice paginates BACKWARD and FORWARD of 50 events each
// Then she can only navigate FORWARD
run {
val lock = CountDownLatch(1)
val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
Timber.e("Alice timeline updated: with ${snapshot.size} events:")
snapshot.forEach {
Timber.w(" event ${it.root.content}")
}
// Alice can see the first event of the room (so Back pagination has worked)
snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE
// 6 for room creation item (backward pagination), 1 for the context, and 50 for the forward pagination
&& snapshot.size == 6 + 1 + 50
}
aliceTimeline.addListener(aliceEventsListener)
// Restart the timeline to the first sent event
// We ask to load event backward and forward
aliceTimeline.paginate(Timeline.Direction.BACKWARDS, 50)
aliceTimeline.paginate(Timeline.Direction.FORWARDS, 50)
commonTestHelper.await(lock)
aliceTimeline.removeAllListeners()
aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue()
aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
}
// Alice paginates once again FORWARD for 50 events
// All the timeline is retrieved, she cannot paginate anymore in both direction
run {
val lock = CountDownLatch(1)
val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
Timber.e("Alice timeline updated: with ${snapshot.size} events:")
snapshot.forEach {
Timber.w(" event ${it.root.content}")
}
// 6 for room creation item (backward pagination),and numberOfMessagesToSend (all the message of the room)
snapshot.size == 6 + numberOfMessagesToSend
&& snapshot.checkSendOrder(message, numberOfMessagesToSend, 0)
}
aliceTimeline.addListener(aliceEventsListener)
// Ask for a forward pagination
aliceTimeline.paginate(Timeline.Direction.FORWARDS, 50)
commonTestHelper.await(lock)
aliceTimeline.removeAllListeners()
// The timeline is fully loaded
aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
}
aliceTimeline.dispose()
cryptoTestData.cleanUp(commonTestHelper)
}
}

View file

@ -0,0 +1,241 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.session.room.timeline
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.api.extensions.orFalse
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
import im.vector.matrix.android.common.CommonTestHelper
import im.vector.matrix.android.common.CryptoTestHelper
import im.vector.matrix.android.common.checkSendOrder
import org.amshove.kluent.shouldBeFalse
import org.amshove.kluent.shouldBeTrue
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.junit.runners.MethodSorters
import timber.log.Timber
import java.util.concurrent.CountDownLatch
@RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
class TimelinePreviousLastForwardTest : InstrumentedTest {
private val commonTestHelper = CommonTestHelper(context())
private val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
/**
* This test ensure that if we have a chunk in the timeline which is due to a sync, and we click to permalink, we will be able to go back to the live
*/
@Test
fun previousLastForwardTest() {
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
val aliceSession = cryptoTestData.firstSession
val bobSession = cryptoTestData.secondSession!!
val aliceRoomId = cryptoTestData.roomId
aliceSession.cryptoService().setWarnOnUnknownDevices(false)
bobSession.cryptoService().setWarnOnUnknownDevices(false)
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!!
val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(30))
bobTimeline.start()
run {
val lock = CountDownLatch(1)
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
snapshot.forEach {
Timber.w(" event ${it.root}")
}
// Ok, we have the 8 first messages of the initial sync (room creation and bob invite and join events)
snapshot.size == 8
}
bobTimeline.addListener(eventsListener)
commonTestHelper.await(lock)
bobTimeline.removeAllListeners()
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
}
// Bob stop to sync
bobSession.stopSync()
val firstMessage = "First messages from Alice"
// Alice sends 30 messages
val firstMessageFromAliceId = commonTestHelper.sendTextMessage(
roomFromAlicePOV,
firstMessage,
30)
.last()
.eventId
// Bob start to sync
bobSession.startSync(true)
run {
val lock = CountDownLatch(1)
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
snapshot.forEach {
Timber.w(" event ${it.root}")
}
// Ok, we have the 10 last messages from Alice. This will be our future previous lastForward chunk
snapshot.size == 10
&& snapshot.all { it.root.content.toModel<MessageContent>()?.body?.startsWith(firstMessage).orFalse() }
}
bobTimeline.addListener(eventsListener)
commonTestHelper.await(lock)
bobTimeline.removeAllListeners()
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue()
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
}
// Bob stop to sync
bobSession.stopSync()
val secondMessage = "Second messages from Alice"
// Alice sends again 30 messages
commonTestHelper.sendTextMessage(
roomFromAlicePOV,
secondMessage,
30)
// Bob start to sync
bobSession.startSync(true)
run {
val lock = CountDownLatch(1)
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
snapshot.forEach {
Timber.w(" event ${it.root}")
}
// Ok, we have the 10 last messages from Alice. This will be our future previous lastForward chunk
snapshot.size == 10
&& snapshot.all { it.root.content.toModel<MessageContent>()?.body?.startsWith(secondMessage).orFalse() }
}
bobTimeline.addListener(eventsListener)
commonTestHelper.await(lock)
bobTimeline.removeAllListeners()
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue()
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
}
// Bob navigate to the first message sent from Alice
run {
val lock = CountDownLatch(1)
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
snapshot.forEach {
Timber.w(" event ${it.root}")
}
// The event is not in db, so it is fetch
snapshot.size == 1
}
bobTimeline.addListener(eventsListener)
// Restart the timeline to the first sent event, and paginate in both direction
bobTimeline.restartWithEventId(firstMessageFromAliceId)
bobTimeline.paginate(Timeline.Direction.BACKWARDS, 50)
bobTimeline.paginate(Timeline.Direction.FORWARDS, 50)
commonTestHelper.await(lock)
bobTimeline.removeAllListeners()
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue()
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue()
}
// Paginate in both direction
run {
val lock = CountDownLatch(1)
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
snapshot.forEach {
Timber.w(" event ${it.root}")
}
snapshot.size == 8 + 1 + 35
}
bobTimeline.addListener(eventsListener)
// Paginate in both direction
bobTimeline.paginate(Timeline.Direction.BACKWARDS, 50)
// Ensure the chunk in the middle is included in the next pagination
bobTimeline.paginate(Timeline.Direction.FORWARDS, 35)
commonTestHelper.await(lock)
bobTimeline.removeAllListeners()
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue()
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
}
// Bob scroll to the future, till the live
run {
val lock = CountDownLatch(1)
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
snapshot.forEach {
Timber.w(" event ${it.root}")
}
// Bob can see the first event of the room (so Back pagination has worked)
snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE
// 8 for room creation item 60 message from Alice
&& snapshot.size == 8 + 60
&& snapshot.checkSendOrder(secondMessage, 30, 0)
&& snapshot.checkSendOrder(firstMessage, 30, 30)
}
bobTimeline.addListener(eventsListener)
bobTimeline.paginate(Timeline.Direction.FORWARDS, 50)
commonTestHelper.await(lock)
bobTimeline.removeAllListeners()
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
}
bobTimeline.dispose()
cryptoTestData.cleanUp(commonTestHelper)
}
}

View file

@ -23,7 +23,6 @@ import androidx.work.WorkManager
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.api.crypto.MXCryptoConfig
import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
@ -32,20 +31,10 @@ import im.vector.matrix.android.internal.network.UserAgentHolder
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
import org.matrix.olm.OlmManager
import java.io.InputStream
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
data class MatrixConfiguration(
val applicationFlavor: String = "Default-application-flavor",
val cryptoConfig: MXCryptoConfig = MXCryptoConfig()
) {
interface Provider {
fun providesMatrixConfiguration(): MatrixConfiguration
}
}
/**
* This is the main entry point to the matrix sdk.
* To get the singleton instance, use getInstance static method.

View file

@ -0,0 +1,38 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api
import im.vector.matrix.android.api.crypto.MXCryptoConfig
import java.net.Proxy
data class MatrixConfiguration(
val applicationFlavor: String = "Default-application-flavor",
val cryptoConfig: MXCryptoConfig = MXCryptoConfig(),
/**
* Optional proxy to connect to the matrix servers
* You can create one using for instance Proxy(proxyType, InetSocketAddress(hostname, port)
*/
val proxy: Proxy? = null
) {
/**
* Can be implemented by your Application class
*/
interface Provider {
fun providesMatrixConfiguration(): MatrixConfiguration
}
}

View file

@ -20,7 +20,6 @@ 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.LoginFlowResult
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.auth.login.LoginWizard
import im.vector.matrix.android.api.auth.registration.RegistrationWizard
import im.vector.matrix.android.api.auth.wellknown.WellknownResult
@ -37,6 +36,11 @@ interface AuthenticationService {
*/
fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResult>): Cancelable
/**
* Request the supported login flows for the corresponding sessionId.
*/
fun getLoginFlowOfSession(sessionId: String, callback: MatrixCallback<LoginFlowResult>): Cancelable
/**
* Return a LoginWizard, to login to the homeserver. The login flow has to be retrieved first.
*/
@ -74,15 +78,6 @@ interface AuthenticationService {
*/
fun getLastAuthenticatedSession(): Session?
/**
* Get an authenticated session. You should at least call authenticate one time before.
* If you logout, this session will no longer be valid.
*
* @param sessionParams the sessionParams to open with.
* @return the associated session if any, or null
*/
fun getSession(sessionParams: SessionParams): Session?
/**
* Create a session after a SSO successful login
*/

View file

@ -21,7 +21,48 @@ package im.vector.matrix.android.api.auth.data
* You don't have to manually instantiate it.
*/
data class SessionParams(
/**
* Please consider using shortcuts instead
*/
val credentials: Credentials,
/**
* Please consider using shortcuts instead
*/
val homeServerConnectionConfig: HomeServerConnectionConfig,
/**
* Set to false if the current token is not valid anymore. Application should not have to use this info.
*/
val isTokenValid: Boolean
)
) {
/*
* Shortcuts. Usually the application should only need to use these shortcuts
*/
/**
* The userId of the session (Ex: "@user:domain.org")
*/
val userId = credentials.userId
/**
* The deviceId of the session (Ex: "ABCDEFGH")
*/
val deviceId = credentials.deviceId
/**
* The current homeserver Url. It can be different that the homeserver url entered
* during login phase, because a redirection may have occurred
*/
val homeServerUrl = homeServerConnectionConfig.homeServerUri.toString()
/**
* The current homeserver host
*/
val homeServerHost = homeServerConnectionConfig.homeServerUri.host
/**
* The default identity server url if any, returned by the homeserver during login phase
*/
val defaultIdentityServerUrl = homeServerConnectionConfig.identityServerUri?.toString()
}

View file

@ -19,11 +19,11 @@ package im.vector.matrix.android.api.crypto
/**
* Class to define the parameters used to customize or configure the end-to-end crypto.
*/
data class MXCryptoConfig(
data class MXCryptoConfig constructor(
// Tell whether the encryption of the event content is enabled for the invited members.
// SDK clients can disable this by settings it to false.
// Note that the encryption for the invited members will be blocked if the history visibility is "joined".
var enableEncryptionForInvitedMembers: Boolean = true,
val enableEncryptionForInvitedMembers: Boolean = true,
/**
* If set to true, the SDK will automatically ignore room key request (gossiping)
@ -31,6 +31,5 @@ data class MXCryptoConfig(
* If set to false, the request will be forwarded to the application layer; in this
* case the application can decide to prompt the user.
*/
var discardRoomKeyRequestsFromUntrustedDevices : Boolean = true
val discardRoomKeyRequestsFromUntrustedDevices: Boolean = true
)

View file

@ -39,7 +39,10 @@ data class MatrixError(
// For M_LIMIT_EXCEEDED
@Json(name = "retry_after_ms") val retryAfterMillis: Long? = null,
// For M_UNKNOWN_TOKEN
@Json(name = "soft_logout") val isSoftLogout: Boolean = false
@Json(name = "soft_logout") val isSoftLogout: Boolean = false,
// For M_INVALID_PEPPER
// {"error": "pepper does not match 'erZvr'", "lookup_pepper": "pQgMS", "algorithm": "sha256", "errcode": "M_INVALID_PEPPER"}
@Json(name = "lookup_pepper") val newLookupPepper: String? = null
) {
companion object {
@ -129,6 +132,11 @@ data class MatrixError(
/** (Not documented yet) */
const val M_WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"
const val M_TERMS_NOT_SIGNED = "M_TERMS_NOT_SIGNED"
// For identity service
const val M_INVALID_PEPPER = "M_INVALID_PEPPER"
// Possible value for "limit_type"
const val LIMIT_TYPE_MAU = "monthly_active_user"
}

View file

@ -30,6 +30,7 @@ import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.session.group.GroupService
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService
import im.vector.matrix.android.api.session.identity.IdentityService
import im.vector.matrix.android.api.session.profile.ProfileService
import im.vector.matrix.android.api.session.pushers.PushersService
import im.vector.matrix.android.api.session.room.RoomDirectoryService
@ -39,6 +40,7 @@ import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageSer
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
import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.matrix.android.api.session.user.UserService
/**
@ -54,6 +56,7 @@ interface Session :
SignOutService,
FilterService,
FileService,
TermsService,
ProfileService,
PushRuleService,
PushersService,
@ -77,7 +80,7 @@ interface Session :
* Useful shortcut to get access to the userId
*/
val myUserId: String
get() = sessionParams.credentials.userId
get() = sessionParams.userId
/**
* The sessionId
@ -145,6 +148,11 @@ interface Session :
*/
fun cryptoService(): CryptoService
/**
* Returns the identity service associated with the session
*/
fun identityService(): IdentityService
/**
* Add a listener to the session.
* @param listener the listener to add.

View file

@ -220,3 +220,11 @@ fun Event.isImageMessage(): Boolean {
else -> false
}
}
fun Event.isVideoMessage(): Boolean {
return getClearType() == EventType.MESSAGE
&& when (getClearContent()?.toModel<MessageContent>()?.msgType) {
MessageType.MSGTYPE_VIDEO -> true
else -> false
}
}

View file

@ -24,7 +24,15 @@ data class HomeServerCapabilities(
/**
* Max size of file which can be uploaded to the homeserver in bytes. [MAX_UPLOAD_FILE_SIZE_UNKNOWN] if unknown or not retrieved yet
*/
val maxUploadFileSize: Long = MAX_UPLOAD_FILE_SIZE_UNKNOWN
val maxUploadFileSize: Long = MAX_UPLOAD_FILE_SIZE_UNKNOWN,
/**
* Last version identity server and binding supported
*/
val lastVersionIdentityServerSupported: Boolean = false,
/**
* Default identity server url, provided in Wellknown
*/
val defaultIdentityServerUrl: String? = null
) {
companion object {
const val MAX_UPLOAD_FILE_SIZE_UNKNOWN = -1L

View file

@ -0,0 +1,22 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.identity
data class FoundThreePid(
val threePid: ThreePid,
val matrixId: String
)

View file

@ -0,0 +1,109 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.identity
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
/**
* Provides access to the identity server configuration and services identity server can provide
*/
interface IdentityService {
/**
* Return the default identity server of the user, which may have been provided at login time by the homeserver,
* or by the Well-known setup of the homeserver
* It may be different from the current configured identity server
*/
fun getDefaultIdentityServer(): String?
/**
* Return the current identity server URL used by this account. Returns null if no identity server is configured.
*/
fun getCurrentIdentityServerUrl(): String?
/**
* Check if the identity server is valid
* See https://matrix.org/docs/spec/identity_service/latest#status-check
* RiotX SDK only supports identity server API v2
*/
fun isValidIdentityServer(url: String, callback: MatrixCallback<Unit>): Cancelable
/**
* Update the identity server url.
* If successful, any previous identity server will be disconnected.
* In case of error, any previous identity server will remain configured.
* @param url the new url.
* @param callback will notify the user if change is successful. The String will be the final url of the identity server.
* The SDK can prepend "https://" for instance.
*/
fun setNewIdentityServer(url: String, callback: MatrixCallback<String>): Cancelable
/**
* Disconnect (logout) from the current identity server
*/
fun disconnect(callback: MatrixCallback<Unit>): Cancelable
/**
* This will ask the identity server to send an email or an SMS to let the user confirm he owns the ThreePid
*/
fun startBindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable
/**
* This will cancel a pending binding of threePid.
*/
fun cancelBindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable
/**
* This will ask the identity server to send an new email or a new SMS to let the user confirm he owns the ThreePid
*/
fun sendAgainValidationCode(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable
/**
* Submit the code that the identity server has sent to the user (in email or SMS)
* Once successful, you will have to call [finalizeBindThreePid]
* @param code the code sent to the user
*/
fun submitValidationToken(threePid: ThreePid, code: String, callback: MatrixCallback<Unit>): Cancelable
/**
* This will perform the actual association of ThreePid and Matrix account
*/
fun finalizeBindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable
/**
* Unbind a threePid
* The request will actually be done on the homeserver
*/
fun unbindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable
/**
* Search MatrixId of users providing email and phone numbers
*/
fun lookUp(threePids: List<ThreePid>, callback: MatrixCallback<List<FoundThreePid>>): Cancelable
/**
* Get the status of the current user's threePid
* A lookup will be performed, but also pending binding state will be restored
*
* @param threePids the list of threePid the user owns (retrieved form the homeserver)
* @param callback onSuccess will be called with a map of ThreePid -> SharedState
*/
fun getShareStatus(threePids: List<ThreePid>, callback: MatrixCallback<Map<ThreePid, SharedState>>): Cancelable
fun addListener(listener: IdentityServiceListener)
fun removeListener(listener: IdentityServiceListener)
}

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.identity
sealed class IdentityServiceError : Throwable() {
object OutdatedIdentityServer : IdentityServiceError()
object OutdatedHomeServer : IdentityServiceError()
object NoIdentityServerConfigured : IdentityServiceError()
object TermsNotSignedException : IdentityServiceError()
object BulkLookupSha256NotSupported : IdentityServiceError()
object BindingError : IdentityServiceError()
object NoCurrentBindingError : IdentityServiceError()
}

View file

@ -0,0 +1,21 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.identity
interface IdentityServiceListener {
fun onIdentityServerChange()
}

View file

@ -0,0 +1,23 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.identity
enum class SharedState {
SHARED,
NOT_SHARED,
BINDING_IN_PROGRESS
}

View file

@ -0,0 +1,40 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.identity
import com.google.i18n.phonenumbers.NumberParseException
import com.google.i18n.phonenumbers.PhoneNumberUtil
import im.vector.matrix.android.internal.session.profile.ThirdPartyIdentifier
sealed class ThreePid(open val value: String) {
data class Email(val email: String) : ThreePid(email)
data class Msisdn(val msisdn: String) : ThreePid(msisdn)
}
internal fun ThreePid.toMedium(): String {
return when (this) {
is ThreePid.Email -> ThirdPartyIdentifier.MEDIUM_EMAIL
is ThreePid.Msisdn -> ThirdPartyIdentifier.MEDIUM_MSISDN
}
}
@Throws(NumberParseException::class)
internal fun ThreePid.Msisdn.getCountryCode(): String {
return with(PhoneNumberUtil.getInstance()) {
getRegionCodeForCountryCode(parse("+$msisdn", null).countryCode)
}
}

View file

@ -17,7 +17,9 @@
package im.vector.matrix.android.api.session.profile
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.identity.ThreePid
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.api.util.Optional
@ -53,4 +55,15 @@ interface ProfileService {
*
*/
fun getProfile(userId: String, matrixCallback: MatrixCallback<JsonDict>): Cancelable
/**
* Get the current user 3Pids
*/
fun getThreePids(): List<ThreePid>
/**
* Get the current user 3Pids Live
* @param refreshData set to true to fetch data from the homeserver
*/
fun getThreePidsLive(refreshData: Boolean): LiveData<List<ThreePid>>
}

View file

@ -29,6 +29,7 @@ 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
import im.vector.matrix.android.api.session.room.typing.TypingService
import im.vector.matrix.android.api.session.room.uploads.UploadsService
import im.vector.matrix.android.api.util.Optional
/**
@ -42,6 +43,7 @@ interface Room :
TypingService,
MembershipService,
StateService,
UploadsService,
ReportingService,
RelationService,
RoomCryptoService,

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.room.model.message
interface MessageContentWithFormattedBody : MessageContent {
/**
* The format used in the formatted_body. Currently only "org.matrix.custom.html" is supported.
*/
val format: String?
/**
* The formatted version of the body. This is required if format is specified.
*/
val formattedBody: String?
/**
* Get the formattedBody, only if not blank and if the format is equal to "org.matrix.custom.html"
*/
val matrixFormattedBody: String?
get() = formattedBody?.takeIf { it.isNotBlank() && format == MessageFormat.FORMAT_MATRIX_HTML }
}

View file

@ -34,15 +34,15 @@ data class MessageEmoteContent(
@Json(name = "body") override val body: String,
/**
* The format used in the formatted_body. Currently only org.matrix.custom.html is supported.
* The format used in the formatted_body. Currently only "org.matrix.custom.html" is supported.
*/
@Json(name = "format") val format: String? = null,
@Json(name = "format") override val format: String? = null,
/**
* The formatted version of the body. This is required if format is specified.
*/
@Json(name = "formatted_body") val formattedBody: String? = null,
@Json(name = "formatted_body") override val formattedBody: String? = null,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null
) : MessageContent
) : MessageContentWithFormattedBody

View file

@ -34,15 +34,15 @@ data class MessageNoticeContent(
@Json(name = "body") override val body: String,
/**
* The format used in the formatted_body. Currently only org.matrix.custom.html is supported.
* The format used in the formatted_body. Currently only "org.matrix.custom.html" is supported.
*/
@Json(name = "format") val format: String? = null,
@Json(name = "format") override val format: String? = null,
/**
* The formatted version of the body. This is required if format is specified.
*/
@Json(name = "formatted_body") val formattedBody: String? = null,
@Json(name = "formatted_body") override val formattedBody: String? = null,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null
) : MessageContent
) : MessageContentWithFormattedBody

View file

@ -34,15 +34,15 @@ data class MessageTextContent(
@Json(name = "body") override val body: String,
/**
* The format used in the formatted_body. Currently only org.matrix.custom.html is supported.
* The format used in the formatted_body. Currently only "org.matrix.custom.html" is supported.
*/
@Json(name = "format") val format: String? = null,
@Json(name = "format") override val format: String? = null,
/**
* The formatted version of the body. This is required if format is specified.
*/
@Json(name = "formatted_body") val formattedBody: String? = null,
@Json(name = "formatted_body") override val formattedBody: String? = null,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null
) : MessageContent
) : MessageContentWithFormattedBody

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.room.sender
data class SenderInfo(
val userId: String,
/**
* Consider using [disambiguatedDisplayName]
*/
val displayName: String?,
val isUniqueDisplayName: Boolean,
val avatarUrl: String?
) {
val disambiguatedDisplayName: String
get() = when {
displayName.isNullOrBlank() -> userId
isUniqueDisplayName -> displayName
else -> "$displayName ($userId)"
}
}

View file

@ -58,7 +58,7 @@ interface Timeline {
/**
* Check if the timeline can be enriched by paginating.
* @param the direction to check in
* @param direction the direction to check in
* @return true if timeline can be enriched
*/
fun hasMoreToLoad(direction: Direction): Boolean

View file

@ -16,6 +16,7 @@
package im.vector.matrix.android.api.session.room.timeline
import im.vector.matrix.android.BuildConfig
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
@ -25,6 +26,7 @@ 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.MessageStickerContent
import im.vector.matrix.android.api.session.room.model.message.isReply
import im.vector.matrix.android.api.session.room.sender.SenderInfo
import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
@ -38,13 +40,17 @@ data class TimelineEvent(
val localId: Long,
val eventId: String,
val displayIndex: Int,
val senderName: String?,
val isUniqueDisplayName: Boolean,
val senderAvatar: String?,
val senderInfo: SenderInfo,
val annotations: EventAnnotationsSummary? = null,
val readReceipts: List<ReadReceipt> = emptyList()
) {
init {
if (BuildConfig.DEBUG) {
assert(eventId == root.eventId)
}
}
val metadata = HashMap<String, Any>()
/**
@ -62,14 +68,6 @@ data class TimelineEvent(
}
}
fun getDisambiguatedDisplayName(): String {
return when {
senderName.isNullOrBlank() -> root.senderId ?: ""
isUniqueDisplayName -> senderName
else -> "$senderName (${root.senderId})"
}
}
/**
* Get the metadata associated with a key.
* @param key the key to get the metadata

View file

@ -28,6 +28,10 @@ data class TimelineSettings(
* A flag to filter edit events
*/
val filterEdits: Boolean = false,
/**
* A flag to filter redacted events
*/
val filterRedacted: Boolean = false,
/**
* A flag to filter by types. It should be used with [allowedTypes] field
*/

View file

@ -0,0 +1,26 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.room.uploads
data class GetUploadsResult(
// List of fetched Events, most recent first
val uploadEvents: List<UploadEvent>,
// token to get more events
val nextToken: String,
// True if there are more event to load
val hasMore: Boolean
)

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.room.uploads
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent
import im.vector.matrix.android.api.session.room.sender.SenderInfo
/**
* Wrapper around on Event.
* Similar to [im.vector.matrix.android.api.session.room.timeline.TimelineEvent], contains an Event with extra useful data
*/
data class UploadEvent(
val root: Event,
val eventId: String,
val contentWithAttachmentContent: MessageWithAttachmentContent,
val senderInfo: SenderInfo
)

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.room.uploads
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
/**
* This interface defines methods to get event with uploads (= attachments) sent to a room. It's implemented at the room level.
*/
interface UploadsService {
/**
* Get a list of events containing URL sent to a room, from most recent to oldest one
* @param numberOfEvents the expected number of events to retrieve. The result can contain less events.
* @param since token to get next page, or null to get the first page
*/
fun getUploads(numberOfEvents: Int,
since: String?,
callback: MatrixCallback<GetUploadsResult>): Cancelable
}

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.terms
import im.vector.matrix.android.internal.session.terms.TermsResponse
data class GetTermsResponse(
val serverResponse: TermsResponse,
val alreadyAcceptedTermUrls: Set<String>
)

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.terms
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
interface TermsService {
enum class ServiceType {
IntegrationManager,
IdentityService
}
fun getTerms(serviceType: ServiceType,
baseUrl: String,
callback: MatrixCallback<GetTermsResponse>): Cancelable
fun agreeToTerms(serviceType: ServiceType,
baseUrl: String,
agreedUrls: List<String>,
token: String?,
callback: MatrixCallback<Unit>): Cancelable
}

View file

@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.matrix.android.api.session.room.sender.SenderInfo
import im.vector.matrix.android.api.session.user.model.User
import java.util.Locale
@ -154,3 +155,5 @@ fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlia
fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: getPrimaryAlias() ?: "", avatarUrl)
fun RoomMemberSummary.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)
fun SenderInfo.toMatrixItem() = MatrixItem.UserItem(userId, disambiguatedDisplayName, avatarUrl)

View file

@ -25,16 +25,15 @@ import im.vector.matrix.android.internal.auth.db.AuthRealmMigration
import im.vector.matrix.android.internal.auth.db.AuthRealmModule
import im.vector.matrix.android.internal.auth.db.RealmPendingSessionStore
import im.vector.matrix.android.internal.auth.db.RealmSessionParamsStore
import im.vector.matrix.android.internal.auth.wellknown.DefaultDirectLoginTask
import im.vector.matrix.android.internal.auth.wellknown.DefaultGetWellknownTask
import im.vector.matrix.android.internal.auth.wellknown.DirectLoginTask
import im.vector.matrix.android.internal.auth.wellknown.GetWellknownTask
import im.vector.matrix.android.internal.auth.login.DefaultDirectLoginTask
import im.vector.matrix.android.internal.auth.login.DirectLoginTask
import im.vector.matrix.android.internal.database.RealmKeysUtils
import im.vector.matrix.android.internal.di.AuthDatabase
import im.vector.matrix.android.internal.wellknown.WellknownModule
import io.realm.RealmConfiguration
import java.io.File
@Module
@Module(includes = [WellknownModule::class])
internal abstract class AuthModule {
@Module
@ -74,9 +73,6 @@ internal abstract class AuthModule {
@Binds
abstract fun bindSessionCreator(creator: DefaultSessionCreator): SessionCreator
@Binds
abstract fun bindGetWellknownTask(task: DefaultGetWellknownTask): GetWellknownTask
@Binds
abstract fun bindDirectLoginTask(task: DefaultDirectLoginTask): DirectLoginTask
}

View file

@ -23,7 +23,6 @@ import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.auth.data.LoginFlowResult
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.auth.data.Versions
import im.vector.matrix.android.api.auth.data.isLoginAndRegistrationSupportedBySdk
import im.vector.matrix.android.api.auth.data.isSupportedBySdk
@ -33,14 +32,14 @@ import im.vector.matrix.android.api.auth.wellknown.WellknownResult
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.NoOpCancellable
import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
import im.vector.matrix.android.internal.auth.data.RiotConfig
import im.vector.matrix.android.internal.auth.db.PendingSessionData
import im.vector.matrix.android.internal.auth.login.DefaultLoginWizard
import im.vector.matrix.android.internal.auth.login.DirectLoginTask
import im.vector.matrix.android.internal.auth.registration.DefaultRegistrationWizard
import im.vector.matrix.android.internal.auth.wellknown.DirectLoginTask
import im.vector.matrix.android.internal.auth.wellknown.GetWellknownTask
import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.network.executeRequest
@ -50,7 +49,7 @@ import im.vector.matrix.android.internal.task.launchToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.exhaustive
import im.vector.matrix.android.internal.util.toCancelable
import kotlinx.coroutines.GlobalScope
import im.vector.matrix.android.internal.wellknown.GetWellknownTask
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
@ -87,14 +86,21 @@ internal class DefaultAuthenticationService @Inject constructor(
}
}
override fun getSession(sessionParams: SessionParams): Session? {
return sessionManager.getOrCreateSession(sessionParams)
override fun getLoginFlowOfSession(sessionId: String, callback: MatrixCallback<LoginFlowResult>): Cancelable {
val homeServerConnectionConfig = sessionParamsStore.get(sessionId)?.homeServerConnectionConfig
return if (homeServerConnectionConfig == null) {
callback.onFailure(IllegalStateException("Session not found"))
NoOpCancellable
} else {
getLoginFlow(homeServerConnectionConfig, callback)
}
}
override fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResult>): Cancelable {
pendingSessionData = null
return GlobalScope.launch(coroutineDispatchers.main) {
return taskExecutor.executorScope.launch(coroutineDispatchers.main) {
pendingSessionStore.delete()
val result = runCatching {
@ -246,7 +252,8 @@ internal class DefaultAuthenticationService @Inject constructor(
retrofitFactory,
coroutineDispatchers,
sessionCreator,
pendingSessionStore
pendingSessionStore,
taskExecutor.executorScope
).also {
currentRegistrationWizard = it
}
@ -266,7 +273,8 @@ internal class DefaultAuthenticationService @Inject constructor(
retrofitFactory,
coroutineDispatchers,
sessionCreator,
pendingSessionStore
pendingSessionStore,
taskExecutor.executorScope
).also {
currentLoginWizard = it
}
@ -283,7 +291,7 @@ internal class DefaultAuthenticationService @Inject constructor(
pendingSessionData = pendingSessionData?.homeServerConnectionConfig
?.let { PendingSessionData(it) }
.also {
GlobalScope.launch(coroutineDispatchers.main) {
taskExecutor.executorScope.launch(coroutineDispatchers.main) {
if (it == null) {
// Should not happen
pendingSessionStore.delete()
@ -300,7 +308,7 @@ internal class DefaultAuthenticationService @Inject constructor(
pendingSessionData = null
GlobalScope.launch(coroutineDispatchers.main) {
taskExecutor.executorScope.launch(coroutineDispatchers.main) {
pendingSessionStore.delete()
}
}
@ -308,7 +316,7 @@ internal class DefaultAuthenticationService @Inject constructor(
override fun createSessionFromSso(homeServerConnectionConfig: HomeServerConnectionConfig,
credentials: Credentials,
callback: MatrixCallback<Session>): Cancelable {
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) {
createSessionFromSso(credentials, homeServerConnectionConfig)
}
}

View file

@ -51,7 +51,7 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
}
return SessionParamsEntity(
sessionParams.credentials.sessionId(),
sessionParams.credentials.userId,
sessionParams.userId,
credentialsJson,
homeServerConnectionConfigJson,
sessionParams.isTokenValid)

View file

@ -38,7 +38,7 @@ import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.launchToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
@ -47,7 +47,8 @@ internal class DefaultLoginWizard(
retrofitFactory: RetrofitFactory,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val sessionCreator: SessionCreator,
private val pendingSessionStore: PendingSessionStore
private val pendingSessionStore: PendingSessionStore,
private val coroutineScope: CoroutineScope
) : LoginWizard {
private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here")
@ -59,7 +60,7 @@ internal class DefaultLoginWizard(
password: String,
deviceName: String,
callback: MatrixCallback<Session>): Cancelable {
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) {
loginInternal(login, password, deviceName)
}
}
@ -80,7 +81,7 @@ internal class DefaultLoginWizard(
}
override fun resetPassword(email: String, newPassword: String, callback: MatrixCallback<Unit>): Cancelable {
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) {
resetPasswordInternal(email, newPassword)
}
}
@ -108,7 +109,7 @@ internal class DefaultLoginWizard(
callback.onFailure(IllegalStateException("developer error, no reset password in progress"))
return NoOpCancellable
}
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) {
resetPasswordMailConfirmedInternal(safeResetPasswordData)
}
}

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
package im.vector.matrix.android.internal.auth.wellknown
package im.vector.matrix.android.internal.auth.login
import dagger.Lazy
import im.vector.matrix.android.api.auth.data.Credentials

View file

@ -33,7 +33,7 @@ import im.vector.matrix.android.internal.auth.db.PendingSessionData
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.task.launchToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import okhttp3.OkHttpClient
@ -45,7 +45,8 @@ internal class DefaultRegistrationWizard(
private val retrofitFactory: RetrofitFactory,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val sessionCreator: SessionCreator,
private val pendingSessionStore: PendingSessionStore
private val pendingSessionStore: PendingSessionStore,
private val coroutineScope: CoroutineScope
) : RegistrationWizard {
private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here")
@ -72,7 +73,7 @@ internal class DefaultRegistrationWizard(
override fun getRegistrationFlow(callback: MatrixCallback<RegistrationResult>): Cancelable {
val params = RegistrationParams()
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) {
performRegistrationRequest(params)
}
}
@ -86,7 +87,7 @@ internal class DefaultRegistrationWizard(
password = password,
initialDeviceDisplayName = initialDeviceDisplayName
)
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) {
performRegistrationRequest(params)
.also {
pendingSessionData = pendingSessionData.copy(isRegistrationStarted = true)
@ -101,7 +102,7 @@ internal class DefaultRegistrationWizard(
return NoOpCancellable
}
val params = RegistrationParams(auth = AuthParams.createForCaptcha(safeSession, response))
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) {
performRegistrationRequest(params)
}
}
@ -112,13 +113,13 @@ internal class DefaultRegistrationWizard(
return NoOpCancellable
}
val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.TERMS, session = safeSession))
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) {
performRegistrationRequest(params)
}
}
override fun addThreePid(threePid: RegisterThreePid, callback: MatrixCallback<RegistrationResult>): Cancelable {
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) {
pendingSessionData = pendingSessionData.copy(currentThreePidData = null)
.also { pendingSessionStore.savePendingSessionData(it) }
@ -131,7 +132,7 @@ internal class DefaultRegistrationWizard(
callback.onFailure(IllegalStateException("developer error, call createAccount() method first"))
return NoOpCancellable
}
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) {
sendThreePid(safeCurrentThreePid)
}
}
@ -177,13 +178,13 @@ internal class DefaultRegistrationWizard(
callback.onFailure(IllegalStateException("developer error, no pending three pid"))
return NoOpCancellable
}
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) {
performRegistrationRequest(safeParam, delayMillis)
}
}
override fun handleValidateThreePid(code: String, callback: MatrixCallback<RegistrationResult>): Cancelable {
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) {
validateThreePid(code)
}
}
@ -199,7 +200,7 @@ internal class DefaultRegistrationWizard(
code = code
)
val validationResponse = validateCodeTask.execute(ValidateCodeTask.Params(url, validationBody))
if (validationResponse.success == true) {
if (validationResponse.isSuccess()) {
// The entered code is correct
// Same than validate email
return performRegistrationRequest(registrationParams, 3_000)
@ -214,7 +215,7 @@ internal class DefaultRegistrationWizard(
callback.onFailure(IllegalStateException("developer error, call createAccount() method first"))
return NoOpCancellable
}
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) {
val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.DUMMY, session = safeSession))
performRegistrationRequest(params)
}

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.matrix.androidsdk.rest.model.login
package im.vector.matrix.android.internal.auth.registration
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize

View file

@ -76,7 +76,7 @@ fun RegistrationFlowResponse.toFlowResult(): FlowResult {
this.flows?.forEach { it.stages?.mapTo(allFlowTypes) { type -> type } }
allFlowTypes.forEach { type ->
val isMandatory = flows?.all { type in it.stages ?: emptyList() } == true
val isMandatory = flows?.all { type in it.stages.orEmpty() } == true
val stage = when (type) {
LoginFlowTypes.RECAPTCHA -> Stage.ReCaptcha(isMandatory, ((params?.get(type) as? Map<*, *>)?.get("public_key") as? String)
@ -88,7 +88,7 @@ fun RegistrationFlowResponse.toFlowResult(): FlowResult {
else -> Stage.Other(isMandatory, type, (params?.get(type) as? Map<*, *>))
}
if (type in completedStages ?: emptyList()) {
if (type in completedStages.orEmpty()) {
completedStage.add(stage)
} else {
missingStage.add(stage)

View file

@ -18,9 +18,12 @@ package im.vector.matrix.android.internal.auth.registration
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.extensions.orFalse
@JsonClass(generateAdapter = true)
data class SuccessResult(
@Json(name = "success")
val success: Boolean?
)
) {
fun isSuccess() = success.orFalse()
}

View file

@ -262,7 +262,7 @@ internal class DefaultCryptoService @Inject constructor(
override fun onSuccess(data: DevicesListResponse) {
// Save in local DB
cryptoStore.saveMyDevicesInfo(data.devices ?: emptyList())
cryptoStore.saveMyDevicesInfo(data.devices.orEmpty())
callback.onSuccess(data)
}
}
@ -446,7 +446,7 @@ internal class DefaultCryptoService @Inject constructor(
}
override fun getCryptoDeviceInfo(userId: String): List<CryptoDeviceInfo> {
return cryptoStore.getUserDeviceList(userId) ?: emptyList()
return cryptoStore.getUserDeviceList(userId).orEmpty()
}
override fun getLiveCryptoDeviceInfo(): LiveData<List<CryptoDeviceInfo>> {

View file

@ -137,7 +137,7 @@ internal class OneTimeKeysUploader @Inject constructor(
private suspend fun uploadOneTimeKeys(oneTimeKeys: Map<String, Map<String, String>>?): KeysUploadResponse {
val oneTimeJson = mutableMapOf<String, Any>()
val curve25519Map = oneTimeKeys?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY) ?: emptyMap()
val curve25519Map = oneTimeKeys?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY).orEmpty()
curve25519Map.forEach { (key_id, value) ->
val k = mutableMapOf<String, Any>()

View file

@ -34,7 +34,7 @@ internal class EnsureOlmSessionsForUsersAction @Inject constructor(private val o
suspend fun handle(users: List<String>): MXUsersDevicesMap<MXOlmSessionResult> {
Timber.v("## ensureOlmSessionsForUsers() : ensureOlmSessionsForUsers $users")
val devicesByUser = users.associateWith { userId ->
val devices = cryptoStore.getUserDevices(userId)?.values ?: emptyList()
val devices = cryptoStore.getUserDevices(userId)?.values.orEmpty()
devices.filter {
// Don't bother setting up session to ourself

View file

@ -103,7 +103,7 @@ internal class MXMegolmDecryption(private val userId: String,
senderCurve25519Key = olmDecryptionResult.senderKey,
claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"),
forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain
?: emptyList()
.orEmpty()
)
} else {
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)

View file

@ -44,7 +44,7 @@ internal class MXOlmEncryption(
ensureSession(userIds)
val deviceInfos = ArrayList<CryptoDeviceInfo>()
for (userId in userIds) {
val devices = cryptoStore.getUserDevices(userId)?.values ?: emptyList()
val devices = cryptoStore.getUserDevices(userId)?.values.orEmpty()
for (device in devices) {
val key = device.identityKey()
if (key == olmDevice.deviceCurve25519Key) {

View file

@ -199,7 +199,7 @@ internal object MXEncryptedAttachments {
.replace('_', '/')
}
private fun base64ToBase64Url(base64: String): String {
internal fun base64ToBase64Url(base64: String): String {
return base64.replace("\n".toRegex(), "")
.replace("\\+".toRegex(), "-")
.replace('/', '_')

View file

@ -47,7 +47,7 @@ internal object CryptoInfoMapper {
return CryptoCrossSigningKey(
userId = keyInfo.userId,
usages = keyInfo.usages,
keys = keyInfo.keys ?: emptyMap(),
keys = keyInfo.keys.orEmpty(),
signatures = keyInfo.signatures,
trustLevel = null
)

View file

@ -450,7 +450,7 @@ internal class RealmCryptoStore @Inject constructor(
}
)
return Transformations.map(liveData) {
it.firstOrNull() ?: emptyList()
it.firstOrNull().orEmpty()
}
}
@ -480,7 +480,7 @@ internal class RealmCryptoStore @Inject constructor(
}
)
return Transformations.map(liveData) {
it.firstOrNull() ?: emptyList()
it.firstOrNull().orEmpty()
}
}

View file

@ -200,6 +200,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
}
private fun migrateTo3(realm: DynamicRealm) {
Timber.d("Step 2 -> 3")
Timber.d("Updating CryptoMetadataEntity table")
realm.schema.get("CryptoMetadataEntity")
?.addField(CryptoMetadataEntityFields.KEY_BACKUP_RECOVERY_KEY, String::class.java)
@ -207,6 +208,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
}
private fun migrateTo4(realm: DynamicRealm) {
Timber.d("Step 3 -> 4")
Timber.d("Updating KeyInfoEntity table")
val keyInfoEntities = realm.where("KeyInfoEntity").findAll()
try {
@ -238,6 +240,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
}
private fun migrateTo5(realm: DynamicRealm) {
Timber.d("Step 4 -> 5")
realm.schema.create("MyDeviceLastSeenInfoEntity")
.addField(MyDeviceLastSeenInfoEntityFields.DEVICE_ID, String::class.java)
.addPrimaryKey(MyDeviceLastSeenInfoEntityFields.DEVICE_ID)

View file

@ -78,7 +78,7 @@ internal open class OutgoingGossipingRequestEntity(
GossipRequestType.KEY -> {
OutgoingRoomKeyRequest(
requestBody = getRequestedKeyInfo(),
recipients = getRecipients() ?: emptyMap(),
recipients = getRecipients().orEmpty(),
requestId = requestId ?: "",
state = requestState
)
@ -86,7 +86,7 @@ internal open class OutgoingGossipingRequestEntity(
GossipRequestType.SECRET -> {
OutgoingSecretRequest(
secretName = getRequestedSecretName(),
recipients = getRecipients() ?: emptyMap(),
recipients = getRecipients().orEmpty(),
requestId = requestId ?: "",
state = requestState
)

View file

@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.crypto.tools
import org.matrix.olm.OlmPkDecryption
import org.matrix.olm.OlmPkEncryption
import org.matrix.olm.OlmPkSigning
import org.matrix.olm.OlmUtility
fun <T> withOlmEncryption(block: (OlmPkEncryption) -> T): T {
val olmPkEncryption = OlmPkEncryption()
@ -46,3 +47,12 @@ fun <T> withOlmSigning(block: (OlmPkSigning) -> T): T {
olmPkSigning.releaseSigning()
}
}
fun <T> withOlmUtility(block: (OlmUtility) -> T): T {
val olmUtility = OlmUtility()
try {
return block(olmUtility)
} finally {
olmUtility.releaseUtility()
}
}

View file

@ -198,18 +198,8 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
// using the result as the shared secret.
getSAS().setTheirPublicKey(otherKey)
// (Note: In all of the following HKDF is as defined in RFC 5869, and uses the previously agreed-on hash function as the hash function,
// the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of:
// - the string “MATRIX_KEY_VERIFICATION_SAS”,
// - the Matrix ID of the user who sent the m.key.verification.start message,
// - the device ID of the device that sent the m.key.verification.start message,
// - the Matrix ID of the user who sent the m.key.verification.accept message,
// - he device ID of the device that sent the m.key.verification.accept message
// - the transaction ID.
val sasInfo = "MATRIX_KEY_VERIFICATION_SAS$otherUserId$otherDeviceId$userId$deviceId$transactionId"
// decimal: generate five bytes by using HKDF.
// emoji: generate six bytes by using HKDF.
shortCodeBytes = getSAS().generateShortCode(sasInfo, 6)
shortCodeBytes = calculateSASBytes()
if (BuildConfig.LOG_PRIVATE_DATA) {
Timber.v("************ BOB CODE ${getDecimalCodeRepresentation(shortCodeBytes!!)}")
@ -219,6 +209,35 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
state = VerificationTxState.ShortCodeReady
}
private fun calculateSASBytes(): ByteArray {
when (accepted?.keyAgreementProtocol) {
KEY_AGREEMENT_V1 -> {
// (Note: In all of the following HKDF is as defined in RFC 5869, and uses the previously agreed-on hash function as the hash function,
// the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of:
// - the string “MATRIX_KEY_VERIFICATION_SAS”,
// - the Matrix ID of the user who sent the m.key.verification.start message,
// - the device ID of the device that sent the m.key.verification.start message,
// - the Matrix ID of the user who sent the m.key.verification.accept message,
// - he device ID of the device that sent the m.key.verification.accept message
// - the transaction ID.
val sasInfo = "MATRIX_KEY_VERIFICATION_SAS$otherUserId$otherDeviceId$userId$deviceId$transactionId"
// decimal: generate five bytes by using HKDF.
// emoji: generate six bytes by using HKDF.
return getSAS().generateShortCode(sasInfo, 6)
}
KEY_AGREEMENT_V2 -> {
// Adds the SAS public key, and separate by |
val sasInfo = "MATRIX_KEY_VERIFICATION_SAS|$otherUserId|$otherDeviceId|$otherKey|$userId|$deviceId|${getSAS().publicKey}|$transactionId"
return getSAS().generateShortCode(sasInfo, 6)
}
else -> {
// Protocol has been checked earlier
throw IllegalArgumentException()
}
}
}
override fun onKeyVerificationMac(vMac: ValidVerificationInfoMac) {
Timber.v("## SAS I: received mac for request id:$transactionId")
// Check for state?

View file

@ -121,7 +121,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
// }
//
// val requestMessage = KeyVerificationRequest(
// fromDevice = session.sessionParams.credentials.deviceId ?: "",
// fromDevice = session.sessionParams.deviceId ?: "",
// methods = listOf(KeyVerificationStart.VERIF_METHOD_SAS),
// timestamp = System.currentTimeMillis().toInt(),
// transactionId = transactionId
@ -193,18 +193,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
if (accepted!!.commitment.equals(otherCommitment)) {
getSAS().setTheirPublicKey(otherKey)
// (Note: In all of the following HKDF is as defined in RFC 5869, and uses the previously agreed-on hash function as the hash function,
// the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of:
// - the string “MATRIX_KEY_VERIFICATION_SAS”,
// - the Matrix ID of the user who sent the m.key.verification.start message,
// - the device ID of the device that sent the m.key.verification.start message,
// - the Matrix ID of the user who sent the m.key.verification.accept message,
// - he device ID of the device that sent the m.key.verification.accept message
// - the transaction ID.
val sasInfo = "MATRIX_KEY_VERIFICATION_SAS$userId$deviceId$otherUserId$otherDeviceId$transactionId"
// decimal: generate five bytes by using HKDF.
// emoji: generate six bytes by using HKDF.
shortCodeBytes = getSAS().generateShortCode(sasInfo, 6)
shortCodeBytes = calculateSASBytes()
state = VerificationTxState.ShortCodeReady
} else {
// bad commitment
@ -212,14 +201,45 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
}
}
private fun calculateSASBytes(): ByteArray {
when (accepted?.keyAgreementProtocol) {
KEY_AGREEMENT_V1 -> {
// (Note: In all of the following HKDF is as defined in RFC 5869, and uses the previously agreed-on hash function as the hash function,
// the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of:
// - the string “MATRIX_KEY_VERIFICATION_SAS”,
// - the Matrix ID of the user who sent the m.key.verification.start message,
// - the device ID of the device that sent the m.key.verification.start message,
// - the Matrix ID of the user who sent the m.key.verification.accept message,
// - he device ID of the device that sent the m.key.verification.accept message
// - the transaction ID.
val sasInfo = "MATRIX_KEY_VERIFICATION_SAS$userId$deviceId$otherUserId$otherDeviceId$transactionId"
// decimal: generate five bytes by using HKDF.
// emoji: generate six bytes by using HKDF.
return getSAS().generateShortCode(sasInfo, 6)
}
KEY_AGREEMENT_V2 -> {
// Adds the SAS public key, and separate by |
val sasInfo = "MATRIX_KEY_VERIFICATION_SAS|$userId|$deviceId|${getSAS().publicKey}|$otherUserId|$otherDeviceId|$otherKey|$transactionId"
return getSAS().generateShortCode(sasInfo, 6)
}
else -> {
// Protocol has been checked earlier
throw IllegalArgumentException()
}
}
}
override fun onKeyVerificationMac(vMac: ValidVerificationInfoMac) {
Timber.v("## SAS O: onKeyVerificationMac id:$transactionId")
// There is starting to be a huge amount of state / race here :/
if (state != VerificationTxState.OnKeyReceived
&& state != VerificationTxState.ShortCodeReady
&& state != VerificationTxState.ShortCodeAccepted
&& state != VerificationTxState.KeySent
&& state != VerificationTxState.SendingMac
&& state != VerificationTxState.MacSent) {
Timber.e("## SAS O: received key from invalid state $state")
Timber.e("## SAS O: received mac from invalid state $state")
cancel(CancelCode.UnexpectedMessage)
return
}

View file

@ -81,9 +81,9 @@ import im.vector.matrix.android.internal.crypto.verification.qrcode.generateShar
import im.vector.matrix.android.internal.di.DeviceId
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.UUID
@ -104,7 +104,8 @@ internal class DefaultVerificationService @Inject constructor(
private val verificationTransportRoomMessageFactory: VerificationTransportRoomMessageFactory,
private val verificationTransportToDeviceFactory: VerificationTransportToDeviceFactory,
private val crossSigningService: CrossSigningService,
private val cryptoCoroutineScope: CoroutineScope
private val cryptoCoroutineScope: CoroutineScope,
private val taskExecutor: TaskExecutor
) : DefaultVerificationTransaction.Listener, VerificationService {
private val uiHandler = Handler(Looper.getMainLooper())
@ -161,7 +162,7 @@ internal class DefaultVerificationService @Inject constructor(
}
fun onRoomEvent(event: Event) {
GlobalScope.launch(coroutineDispatchers.crypto) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
when (event.getClearType()) {
EventType.KEY_VERIFICATION_START -> {
onRoomStartRequestReceived(event)
@ -301,7 +302,7 @@ internal class DefaultVerificationService @Inject constructor(
// We don't want to block here
val otherDeviceId = validRequestInfo.fromDevice
GlobalScope.launch {
cryptoCoroutineScope.launch {
if (checkKeysAreDownloaded(senderId, otherDeviceId) == null) {
Timber.e("## Verification device $otherDeviceId is not known")
}
@ -340,7 +341,7 @@ internal class DefaultVerificationService @Inject constructor(
}
// We don't want to block here
GlobalScope.launch {
taskExecutor.executorScope.launch {
if (checkKeysAreDownloaded(senderId, validRequestInfo.fromDevice) == null) {
Timber.e("## SAS Verification device ${validRequestInfo.fromDevice} is not known")
}
@ -610,7 +611,7 @@ internal class DefaultVerificationService @Inject constructor(
if (validCancelReq == null) {
// ignore
Timber.e("## SAS Received invalid key request")
Timber.e("## SAS Received invalid cancel request")
// TODO should we cancel?
return
}

View file

@ -26,7 +26,6 @@ import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.crypto.IncomingGossipingRequestManager
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
import im.vector.matrix.android.internal.crypto.model.MXKey
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.extensions.toUnsignedInt
import im.vector.matrix.android.internal.util.withoutPrefix
@ -66,8 +65,11 @@ internal abstract class SASDefaultVerificationTransaction(
const val SAS_MAC_SHA256_LONGKDF = "hmac-sha256"
const val SAS_MAC_SHA256 = "hkdf-hmac-sha256"
// Deprecated maybe removed later, use V2
const val KEY_AGREEMENT_V1 = "curve25519"
const val KEY_AGREEMENT_V2 = "curve25519-hkdf-sha256"
// ordered by preferred order
val KNOWN_AGREEMENT_PROTOCOLS = listOf(MXKey.KEY_CURVE_25519_TYPE)
val KNOWN_AGREEMENT_PROTOCOLS = listOf(KEY_AGREEMENT_V2, KEY_AGREEMENT_V1)
// ordered by preferred order
val KNOWN_HASHES = listOf("sha256")
// ordered by preferred order

View file

@ -48,10 +48,11 @@ import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.di.WorkManagerProvider
import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.StringProvider
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.UUID
@ -66,7 +67,8 @@ internal class VerificationTransportRoomMessage(
private val userDeviceId: String?,
private val roomId: String,
private val localEchoEventFactory: LocalEchoEventFactory,
private val tx: DefaultVerificationTransaction?
private val tx: DefaultVerificationTransaction?,
private val coroutineScope: CoroutineScope
) : VerificationTransport {
override fun <T> sendToOther(type: String,
@ -131,7 +133,7 @@ internal class VerificationTransportRoomMessage(
}
// TODO listen to DB to get synced info
GlobalScope.launch(Dispatchers.Main) {
coroutineScope.launch(Dispatchers.Main) {
workLiveData.observeForever(observer)
}
}
@ -212,7 +214,7 @@ internal class VerificationTransportRoomMessage(
}
// TODO listen to DB to get synced info
GlobalScope.launch(Dispatchers.Main) {
coroutineScope.launch(Dispatchers.Main) {
workLiveData.observeForever(observer)
}
}
@ -265,7 +267,7 @@ internal class VerificationTransportRoomMessage(
}
// TODO listen to DB to get synced info
GlobalScope.launch(Dispatchers.Main) {
coroutineScope.launch(Dispatchers.Main) {
workLiveData.observeForever(observer)
}
}
@ -384,9 +386,19 @@ internal class VerificationTransportRoomMessageFactory @Inject constructor(
private val userId: String,
@DeviceId
private val deviceId: String?,
private val localEchoEventFactory: LocalEchoEventFactory) {
private val localEchoEventFactory: LocalEchoEventFactory,
private val taskExecutor: TaskExecutor
) {
fun createTransport(roomId: String, tx: DefaultVerificationTransaction?): VerificationTransportRoomMessage {
return VerificationTransportRoomMessage(workManagerProvider, stringProvider, sessionId, userId, deviceId, roomId, localEchoEventFactory, tx)
return VerificationTransportRoomMessage(workManagerProvider,
stringProvider,
sessionId,
userId,
deviceId,
roomId,
localEchoEventFactory,
tx,
taskExecutor.executorScope)
}
}

View file

@ -60,10 +60,9 @@ internal fun ChunkEntity.merge(roomId: String, chunkToMerge: ChunkEntity, direct
chunkToMerge.stateEvents.forEach { stateEvent ->
addStateEvent(roomId, stateEvent, direction)
}
return eventsToMerge
.forEach {
addTimelineEventFromMerge(localRealm, it, direction)
}
eventsToMerge.forEach {
addTimelineEventFromMerge(localRealm, it, direction)
}
}
internal fun ChunkEntity.addStateEvent(roomId: String, stateEvent: EventEntity, direction: PaginationDirection) {

View file

@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.database.helper
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
import im.vector.matrix.android.internal.extensions.assertIsManaged
import io.realm.Realm
internal fun TimelineEventEntity.Companion.nextId(realm: Realm): Long {
@ -28,3 +29,11 @@ internal fun TimelineEventEntity.Companion.nextId(realm: Realm): Long {
currentIdNum.toLong() + 1
}
}
internal fun TimelineEventEntity.deleteOnCascade() {
assertIsManaged()
root?.deleteFromRealm()
annotations?.deleteFromRealm()
readReceipts?.deleteFromRealm()
deleteFromRealm()
}

View file

@ -20,21 +20,16 @@ import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities
import im.vector.matrix.android.internal.database.model.HomeServerCapabilitiesEntity
/**
* HomeServerCapabilitiesEntity <-> HomeSeverCapabilities
* HomeServerCapabilitiesEntity -> HomeSeverCapabilities
*/
internal object HomeServerCapabilitiesMapper {
fun map(entity: HomeServerCapabilitiesEntity): HomeServerCapabilities {
return HomeServerCapabilities(
canChangePassword = entity.canChangePassword,
maxUploadFileSize = entity.maxUploadFileSize
)
}
fun map(domain: HomeServerCapabilities): HomeServerCapabilitiesEntity {
return HomeServerCapabilitiesEntity(
canChangePassword = domain.canChangePassword,
maxUploadFileSize = domain.maxUploadFileSize
maxUploadFileSize = entity.maxUploadFileSize,
lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported,
defaultIdentityServerUrl = entity.defaultIdentityServerUrl
)
}
}

View file

@ -45,7 +45,7 @@ internal object PushRulesMapper {
private fun fromActionStr(actionsStr: String?): List<Any> {
try {
return actionsStr?.let { moshiActionsAdapter.fromJson(it) } ?: emptyList()
return actionsStr?.let { moshiActionsAdapter.fromJson(it) }.orEmpty()
} catch (e: Throwable) {
Timber.e(e, "## failed to map push rule actions <$actionsStr>")
return emptyList()

View file

@ -49,7 +49,7 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
membership = roomSummaryEntity.membership,
versioningState = roomSummaryEntity.versioningState,
readMarkerId = roomSummaryEntity.readMarkerId,
userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList(),
userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) }.orEmpty(),
canonicalAlias = roomSummaryEntity.canonicalAlias,
aliases = roomSummaryEntity.aliases.toList(),
isEncrypted = roomSummaryEntity.isEncrypted,

View file

@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.database.mapper
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.ReadReceipt
import im.vector.matrix.android.api.session.room.sender.SenderInfo
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import javax.inject.Inject
@ -41,15 +41,18 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
annotations = timelineEventEntity.annotations?.asDomain(),
localId = timelineEventEntity.localId,
displayIndex = timelineEventEntity.displayIndex,
senderName = timelineEventEntity.senderName,
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
senderAvatar = timelineEventEntity.senderAvatar,
senderInfo = SenderInfo(
userId = timelineEventEntity.root?.sender ?: "",
displayName = timelineEventEntity.senderName,
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
avatarUrl = timelineEventEntity.senderAvatar
),
readReceipts = readReceipts
?.distinctBy {
it.user
}?.sortedByDescending {
it.originServerTs
} ?: emptyList()
}.orEmpty()
)
}
}

View file

@ -23,15 +23,20 @@ import io.realm.annotations.Index
import io.realm.annotations.LinkingObjects
internal open class ChunkEntity(@Index var prevToken: String? = null,
// Because of gaps we can have several chunks with nextToken == null
@Index var nextToken: String? = null,
var stateEvents: RealmList<EventEntity> = RealmList(),
var timelineEvents: RealmList<TimelineEventEntity> = RealmList(),
// Only one chunk will have isLastForward == true
@Index var isLastForward: Boolean = false,
@Index var isLastBackward: Boolean = false
) : RealmObject() {
fun identifier() = "${prevToken}_$nextToken"
// If true, then this chunk was previously a last forward chunk
fun hasBeenALastForwardChunk() = nextToken == null && !isLastForward
@LinkingObjects("chunks")
val room: RealmResults<RoomEntity>? = null

View file

@ -22,6 +22,8 @@ import io.realm.RealmObject
internal open class HomeServerCapabilitiesEntity(
var canChangePassword: Boolean = true,
var maxUploadFileSize: Long = HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN,
var lastVersionIdentityServerSupported: Boolean = false,
var defaultIdentityServerUrl: String? = null,
var lastUpdatedTimestamp: Long = 0L
) : RealmObject() {

View file

@ -37,6 +37,7 @@ import io.realm.annotations.RealmModule
UserEntity::class,
IgnoredUserEntity::class,
BreadcrumbsEntity::class,
UserThreePidEntity::class,
EventAnnotationsSummaryEntity::class,
ReactionAggregatedSummaryEntity::class,
EditAggregatedSummaryEntity::class,

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.database.model
import io.realm.RealmObject
internal open class UserThreePidEntity(
var medium: String = "",
var address: String = "",
var validatedAt: Long = 0,
var addedAt: Long = 0
) : RealmObject()

View file

@ -41,7 +41,7 @@ internal fun ChunkEntity.Companion.find(realm: Realm, roomId: String, prevToken:
return query.findFirst()
}
internal fun ChunkEntity.Companion.findLastLiveChunkFromRoom(realm: Realm, roomId: String): ChunkEntity? {
internal fun ChunkEntity.Companion.findLastForwardChunkOfRoom(realm: Realm, roomId: String): ChunkEntity? {
return where(realm, roomId)
.equalTo(ChunkEntityFields.IS_LAST_FORWARD, true)
.findFirst()

View file

@ -35,7 +35,7 @@ internal fun FilterEntity.Companion.get(realm: Realm): FilterEntity? {
internal fun FilterEntity.Companion.getOrCreate(realm: Realm): FilterEntity {
return get(realm) ?: realm.createObject<FilterEntity>()
.apply {
filterBodyJson = FilterFactory.createDefaultFilterBody().toJSONString()
filterBodyJson = FilterFactory.createDefaultFilter().toJSONString()
roomEventFilterJson = FilterFactory.createDefaultRoomFilter().toJSONString()
filterId = ""
}

View file

@ -36,7 +36,7 @@ internal fun isEventRead(monarchy: Monarchy,
var isEventRead = false
monarchy.doWithRealm { realm ->
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) ?: return@doWithRealm
val liveChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) ?: return@doWithRealm
val eventToCheck = liveChunk.timelineEvents.find(eventId)
isEventRead = if (eventToCheck == null || eventToCheck.root?.sender == userId) {
true

View file

@ -59,11 +59,11 @@ internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm,
filterTypes: List<String> = emptyList()): TimelineEventEntity? {
val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: return null
val sendingTimelineEvents = roomEntity.sendingTimelineEvents.where().filterTypes(filterTypes)
val liveEvents = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)?.timelineEvents?.where()?.filterTypes(filterTypes)
val liveEvents = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)?.timelineEvents?.where()?.filterTypes(filterTypes)
if (filterContentRelation) {
liveEvents
?.not()?.like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.EDIT_TYPE)
?.not()?.like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.RESPONSE_TYPE)
?.not()?.like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT)
?.not()?.like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE)
}
val query = if (includesSending && sendingTimelineEvents.findAll().isNotEmpty()) {
sendingTimelineEvents

View file

@ -0,0 +1,44 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.database.query
/**
* Query strings used to filter the timeline events regarding the Json raw string of the Event
*/
internal object TimelineEventFilter {
/**
* To apply to Event.content
*/
internal object Content {
internal const val EDIT = """{*"m.relates_to"*"rel_type":*"m.replace"*}"""
internal const val RESPONSE = """{*"m.relates_to"*"rel_type":*"m.response"*}"""
}
/**
* To apply to Event.decryptionResultJson
*/
internal object DecryptedContent {
internal const val URL = """{*"file":*"url":*}"""
}
/**
* To apply to Event.unsigned
*/
internal object Unsigned {
internal const val REDACTED = """{*"redacted_because":*}"""
}
}

View file

@ -20,8 +20,12 @@ import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class Authenticated
internal annotation class Authenticated
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class Unauthenticated
internal annotation class AuthenticatedIdentity
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
internal annotation class Unauthenticated

View file

@ -20,12 +20,16 @@ import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class AuthDatabase
internal annotation class AuthDatabase
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class SessionDatabase
internal annotation class SessionDatabase
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class CryptoDatabase
internal annotation class CryptoDatabase
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
internal annotation class IdentityDatabase

View file

@ -20,16 +20,16 @@ import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class SessionFilesDirectory
internal annotation class SessionFilesDirectory
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class SessionCacheDirectory
internal annotation class SessionCacheDirectory
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class CacheDirectory
internal annotation class CacheDirectory
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class ExternalFilesDirectory
internal annotation class ExternalFilesDirectory

View file

@ -21,6 +21,7 @@ import com.squareup.moshi.Moshi
import dagger.Module
import dagger.Provides
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.MatrixConfiguration
import im.vector.matrix.android.internal.network.TimeOutInterceptor
import im.vector.matrix.android.internal.network.UserAgentInterceptor
import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor
@ -64,7 +65,8 @@ internal object NetworkModule {
@Provides
@JvmStatic
@Unauthenticated
fun providesOkHttpClient(stethoInterceptor: StethoInterceptor,
fun providesOkHttpClient(matrixConfiguration: MatrixConfiguration,
stethoInterceptor: StethoInterceptor,
timeoutInterceptor: TimeOutInterceptor,
userAgentInterceptor: UserAgentInterceptor,
httpLoggingInterceptor: HttpLoggingInterceptor,
@ -82,6 +84,9 @@ internal object NetworkModule {
if (BuildConfig.LOG_PRIVATE_DATA) {
addInterceptor(curlLoggingInterceptor)
}
matrixConfiguration.proxy?.let {
proxy(it)
}
}
.addInterceptor(okReplayInterceptor)
.build()

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.extensions
/**
* Ex: "abcdef".subStringBetween("a", "f") -> "bcde"
* Ex: "abcdefff".subStringBetween("a", "f") -> "bcdeff"
* Ex: "aaabcdef".subStringBetween("a", "f") -> "aabcde"
*/
internal fun String.subStringBetween(prefix: String, suffix: String) = substringAfter(prefix).substringBeforeLast(suffix)

View file

@ -16,20 +16,16 @@
package im.vector.matrix.android.internal.network
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.network.token.AccessTokenProvider
import okhttp3.Interceptor
import okhttp3.Response
import javax.inject.Inject
internal class AccessTokenInterceptor @Inject constructor(
@SessionId private val sessionId: String,
private val sessionParamsStore: SessionParamsStore) : Interceptor {
internal class AccessTokenInterceptor(private val accessTokenProvider: AccessTokenProvider) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
accessToken?.let {
accessTokenProvider.getToken()?.let {
val newRequestBuilder = request.newBuilder()
// Add the access token to all requests if it is set
newRequestBuilder.addHeader(HttpHeaders.Authorization, "Bearer $it")
@ -38,7 +34,4 @@ internal class AccessTokenInterceptor @Inject constructor(
return chain.proceed(request)
}
private val accessToken
get() = sessionParamsStore.get(sessionId)?.credentials?.accessToken
}

View file

@ -28,8 +28,9 @@ internal object NetworkConstants {
const val URI_API_MEDIA_PREFIX_PATH_R0 = "$URI_API_MEDIA_PREFIX_PATH/r0/"
// Identity server
const val URI_IDENTITY_PATH = "_matrix/identity/api/v1/"
const val URI_IDENTITY_PATH_V2 = "_matrix/identity/v2/"
const val URI_IDENTITY_PREFIX_PATH = "_matrix/identity/v2"
const val URI_IDENTITY_PATH_V2 = "$URI_IDENTITY_PREFIX_PATH/"
const val URI_API_PREFIX_IDENTITY = "_matrix/identity/api/v1"
// TODO Ganfra, use correct value
const val URI_INTEGRATION_MANAGER_PATH = "TODO/"
}

View file

@ -0,0 +1,39 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.network.httpclient
import im.vector.matrix.android.internal.network.AccessTokenInterceptor
import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor
import im.vector.matrix.android.internal.network.token.AccessTokenProvider
import okhttp3.OkHttpClient
internal fun OkHttpClient.addAccessTokenInterceptor(accessTokenProvider: AccessTokenProvider): OkHttpClient {
return newBuilder()
.apply {
// Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor
val existingCurlInterceptors = interceptors().filterIsInstance<CurlLoggingInterceptor>()
interceptors().removeAll(existingCurlInterceptors)
addInterceptor(AccessTokenInterceptor(accessTokenProvider))
// Re add eventually the curl logging interceptors
existingCurlInterceptors.forEach {
addInterceptor(it)
}
}
.build()
}

View file

@ -0,0 +1,21 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.network.token
internal interface AccessTokenProvider {
fun getToken(): String?
}

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.network.token
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.di.SessionId
import javax.inject.Inject
internal class HomeserverAccessTokenProvider @Inject constructor(
@SessionId private val sessionId: String,
private val sessionParamsStore: SessionParamsStore
) : AccessTokenProvider {
override fun getToken() = sessionParamsStore.get(sessionId)?.credentials?.accessToken
}

View file

@ -28,10 +28,10 @@ import im.vector.matrix.android.internal.di.ExternalFilesDirectory
import im.vector.matrix.android.internal.di.SessionCacheDirectory
import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.toCancelable
import im.vector.matrix.android.internal.util.writeToFile
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
@ -51,7 +51,9 @@ internal class DefaultFileService @Inject constructor(
private val contentUrlResolver: ContentUrlResolver,
@Unauthenticated
private val okHttpClient: OkHttpClient,
private val coroutineDispatchers: MatrixCoroutineDispatchers) : FileService {
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val taskExecutor: TaskExecutor
) : FileService {
/**
* Download file in the cache folder, and eventually decrypt it
@ -63,7 +65,7 @@ internal class DefaultFileService @Inject constructor(
url: String?,
elementToDecrypt: ElementToDecrypt?,
callback: MatrixCallback<File>): Cancelable {
return GlobalScope.launch(coroutineDispatchers.main) {
return taskExecutor.executorScope.launch(coroutineDispatchers.main) {
withContext(coroutineDispatchers.io) {
Try {
val folder = File(sessionCacheDirectory, "MF")

View file

@ -43,6 +43,7 @@ import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageSer
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
import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.crypto.DefaultCryptoService
@ -50,12 +51,14 @@ import im.vector.matrix.android.internal.crypto.crosssigning.ShieldTrustUpdater
import im.vector.matrix.android.internal.database.LiveEntityObserver
import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.di.WorkManagerProvider
import im.vector.matrix.android.internal.session.identity.DefaultIdentityService
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventDecryptor
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
import im.vector.matrix.android.internal.session.sync.job.SyncThread
import im.vector.matrix.android.internal.session.sync.job.SyncWorker
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
@ -82,6 +85,7 @@ internal class DefaultSession @Inject constructor(
private val signOutService: Lazy<SignOutService>,
private val pushRuleService: Lazy<PushRuleService>,
private val pushersService: Lazy<PushersService>,
private val termsService: Lazy<TermsService>,
private val cryptoService: Lazy<DefaultCryptoService>,
private val fileService: Lazy<FileService>,
private val secureStorageService: Lazy<SecureStorageService>,
@ -97,8 +101,11 @@ internal class DefaultSession @Inject constructor(
private val _sharedSecretStorageService: Lazy<SharedSecretStorageService>,
private val accountService: Lazy<AccountService>,
private val timelineEventDecryptor: TimelineEventDecryptor,
private val shieldTrustUpdater: ShieldTrustUpdater)
: Session,
private val shieldTrustUpdater: ShieldTrustUpdater,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val defaultIdentityService: DefaultIdentityService,
private val taskExecutor: TaskExecutor
) : Session,
RoomService by roomService.get(),
RoomDirectoryService by roomDirectoryService.get(),
GroupService by groupService.get(),
@ -108,6 +115,7 @@ internal class DefaultSession @Inject constructor(
PushRuleService by pushRuleService.get(),
PushersService by pushersService.get(),
FileService by fileService.get(),
TermsService by termsService.get(),
InitialSyncProgressService by initialSyncProgressService.get(),
SecureStorageService by secureStorageService.get(),
HomeServerCapabilitiesService by homeServerCapabilitiesService.get(),
@ -133,6 +141,7 @@ internal class DefaultSession @Inject constructor(
eventBus.register(this)
timelineEventDecryptor.start()
shieldTrustUpdater.start()
defaultIdentityService.start()
}
override fun requireBackgroundSync() {
@ -175,6 +184,10 @@ internal class DefaultSession @Inject constructor(
isOpen = false
eventBus.unregister(this)
shieldTrustUpdater.stop()
taskExecutor.executorScope.launch(coroutineDispatchers.main) {
// This has to be done on main thread
defaultIdentityService.stop()
}
}
override fun getSyncStateLive(): LiveData<SyncState> {
@ -204,7 +217,7 @@ internal class DefaultSession @Inject constructor(
if (globalError is GlobalError.InvalidToken
&& globalError.softLogout) {
// Mark the token has invalid
GlobalScope.launch(Dispatchers.IO) {
taskExecutor.executorScope.launch(Dispatchers.IO) {
sessionParamsStore.setTokenInvalid(sessionId)
}
}
@ -218,6 +231,8 @@ internal class DefaultSession @Inject constructor(
override fun cryptoService(): CryptoService = cryptoService.get()
override fun identityService() = defaultIdentityService
override fun addListener(listener: Session.Listener) {
sessionListeners.addListener(listener)
}
@ -228,6 +243,6 @@ internal class DefaultSession @Inject constructor(
// For easy debugging
override fun toString(): String {
return "$myUserId - ${sessionParams.credentials.deviceId}"
return "$myUserId - ${sessionParams.deviceId}"
}
}

View file

@ -36,6 +36,7 @@ import im.vector.matrix.android.internal.session.filter.FilterModule
import im.vector.matrix.android.internal.session.group.GetGroupDataWorker
import im.vector.matrix.android.internal.session.group.GroupModule
import im.vector.matrix.android.internal.session.homeserver.HomeServerCapabilitiesModule
import im.vector.matrix.android.internal.session.identity.IdentityModule
import im.vector.matrix.android.internal.session.openid.OpenIdModule
import im.vector.matrix.android.internal.session.profile.ProfileModule
import im.vector.matrix.android.internal.session.pushers.AddHttpPusherWorker
@ -51,6 +52,7 @@ import im.vector.matrix.android.internal.session.sync.SyncModule
import im.vector.matrix.android.internal.session.sync.SyncTask
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
import im.vector.matrix.android.internal.session.sync.job.SyncWorker
import im.vector.matrix.android.internal.session.terms.TermsModule
import im.vector.matrix.android.internal.session.user.UserModule
import im.vector.matrix.android.internal.session.user.accountdata.AccountDataModule
import im.vector.matrix.android.internal.task.TaskExecutor
@ -72,6 +74,8 @@ import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
CryptoModule::class,
PushersModule::class,
OpenIdModule::class,
IdentityModule::class,
TermsModule::class,
AccountDataModule::class,
ProfileModule::class,
SessionAssistedInjectModule::class,

View file

@ -50,14 +50,15 @@ import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.di.UserMd5
import im.vector.matrix.android.internal.eventbus.EventBusTimberLogger
import im.vector.matrix.android.internal.network.AccessTokenInterceptor
import im.vector.matrix.android.internal.network.DefaultNetworkConnectivityChecker
import im.vector.matrix.android.internal.network.FallbackNetworkCallbackStrategy
import im.vector.matrix.android.internal.network.NetworkCallbackStrategy
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
import im.vector.matrix.android.internal.network.PreferredNetworkCallbackStrategy
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor
import im.vector.matrix.android.internal.network.httpclient.addAccessTokenInterceptor
import im.vector.matrix.android.internal.network.token.AccessTokenProvider
import im.vector.matrix.android.internal.network.token.HomeserverAccessTokenProvider
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
import im.vector.matrix.android.internal.session.homeserver.DefaultHomeServerCapabilitiesService
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
@ -175,21 +176,8 @@ internal abstract class SessionModule {
@SessionScope
@Authenticated
fun providesOkHttpClient(@Unauthenticated okHttpClient: OkHttpClient,
accessTokenInterceptor: AccessTokenInterceptor): OkHttpClient {
return okHttpClient.newBuilder()
.apply {
// Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor
val existingCurlInterceptors = interceptors().filterIsInstance<CurlLoggingInterceptor>()
interceptors().removeAll(existingCurlInterceptors)
addInterceptor(accessTokenInterceptor)
// Re add eventually the curl logging interceptors
existingCurlInterceptors.forEach {
addInterceptor(it)
}
}
.build()
@Authenticated accessTokenProvider: AccessTokenProvider): OkHttpClient {
return okHttpClient.addAccessTokenInterceptor(accessTokenProvider)
}
@JvmStatic
@ -233,44 +221,48 @@ internal abstract class SessionModule {
}
}
@Binds
@Authenticated
abstract fun bindAccessTokenProvider(provider: HomeserverAccessTokenProvider): AccessTokenProvider
@Binds
abstract fun bindSession(session: DefaultSession): Session
@Binds
abstract fun bindNetworkConnectivityChecker(networkConnectivityChecker: DefaultNetworkConnectivityChecker): NetworkConnectivityChecker
abstract fun bindNetworkConnectivityChecker(checker: DefaultNetworkConnectivityChecker): NetworkConnectivityChecker
@Binds
@IntoSet
abstract fun bindGroupSummaryUpdater(groupSummaryUpdater: GroupSummaryUpdater): LiveEntityObserver
abstract fun bindGroupSummaryUpdater(updater: GroupSummaryUpdater): LiveEntityObserver
@Binds
@IntoSet
abstract fun bindEventsPruner(eventsPruner: EventsPruner): LiveEntityObserver
abstract fun bindEventsPruner(pruner: EventsPruner): LiveEntityObserver
@Binds
@IntoSet
abstract fun bindEventRelationsAggregationUpdater(eventRelationsAggregationUpdater: EventRelationsAggregationUpdater): LiveEntityObserver
abstract fun bindEventRelationsAggregationUpdater(updater: EventRelationsAggregationUpdater): LiveEntityObserver
@Binds
@IntoSet
abstract fun bindRoomTombstoneEventLiveObserver(roomTombstoneEventLiveObserver: RoomTombstoneEventLiveObserver): LiveEntityObserver
abstract fun bindRoomTombstoneEventLiveObserver(observer: RoomTombstoneEventLiveObserver): LiveEntityObserver
@Binds
@IntoSet
abstract fun bindRoomCreateEventLiveObserver(roomCreateEventLiveObserver: RoomCreateEventLiveObserver): LiveEntityObserver
abstract fun bindRoomCreateEventLiveObserver(observer: RoomCreateEventLiveObserver): LiveEntityObserver
@Binds
@IntoSet
abstract fun bindVerificationMessageLiveObserver(verificationMessageLiveObserver: VerificationMessageLiveObserver): LiveEntityObserver
abstract fun bindVerificationMessageLiveObserver(observer: VerificationMessageLiveObserver): LiveEntityObserver
@Binds
abstract fun bindInitialSyncProgressService(initialSyncProgressService: DefaultInitialSyncProgressService): InitialSyncProgressService
abstract fun bindInitialSyncProgressService(service: DefaultInitialSyncProgressService): InitialSyncProgressService
@Binds
abstract fun bindSecureStorageService(secureStorageService: DefaultSecureStorageService): SecureStorageService
abstract fun bindSecureStorageService(service: DefaultSecureStorageService): SecureStorageService
@Binds
abstract fun bindHomeServerCapabilitiesService(homeServerCapabilitiesService: DefaultHomeServerCapabilitiesService): HomeServerCapabilitiesService
abstract fun bindHomeServerCapabilitiesService(service: DefaultHomeServerCapabilitiesService): HomeServerCapabilitiesService
@Binds
abstract fun bindAccountDataService(service: DefaultAccountDataService): AccountDataService

View file

@ -19,8 +19,10 @@ package im.vector.matrix.android.internal.session.account
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.cleanup.CleanupSession
import im.vector.matrix.android.internal.session.identity.IdentityDisconnectTask
import im.vector.matrix.android.internal.task.Task
import org.greenrobot.eventbus.EventBus
import timber.log.Timber
import javax.inject.Inject
internal interface DeactivateAccountTask : Task<DeactivateAccountTask.Params, Unit> {
@ -34,6 +36,7 @@ internal class DefaultDeactivateAccountTask @Inject constructor(
private val accountAPI: AccountAPI,
private val eventBus: EventBus,
@UserId private val userId: String,
private val identityDisconnectTask: IdentityDisconnectTask,
private val cleanupSession: CleanupSession
) : DeactivateAccountTask {
@ -44,6 +47,10 @@ internal class DefaultDeactivateAccountTask @Inject constructor(
apiCall = accountAPI.deactivate(deactivateAccountParams)
}
// Logout from identity server if any, ignoring errors
runCatching { identityDisconnectTask.execute(Unit) }
.onFailure { Timber.w(it, "Unable to disconnect identity server") }
cleanupSession.handle()
}
}

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