Merge develop into db_clean_up
This commit is contained in:
commit
3db26bcae1
@ -9,6 +9,10 @@ Improvements 🙌:
|
|||||||
- "Add Matrix app" menu is now always visible (#1495)
|
- "Add Matrix app" menu is now always visible (#1495)
|
||||||
- Handle `/op`, `/deop`, and `/nick` commands (#12)
|
- Handle `/op`, `/deop`, and `/nick` commands (#12)
|
||||||
- Prioritising Recovery key over Recovery passphrase (#1463)
|
- Prioritising Recovery key over Recovery passphrase (#1463)
|
||||||
|
- Room Settings: Name, Topic, Photo, Aliases, History Visibility (#1455)
|
||||||
|
- Update user avatar (#1054)
|
||||||
|
- Allow self-signed certificate (#1564)
|
||||||
|
- Improve file download and open in timeline
|
||||||
|
|
||||||
Bugfix 🐛:
|
Bugfix 🐛:
|
||||||
- Fix dark theme issue on login screen (#1097)
|
- Fix dark theme issue on login screen (#1097)
|
||||||
@ -16,6 +20,7 @@ Bugfix 🐛:
|
|||||||
- User could not redact message that they have sent (#1543)
|
- User could not redact message that they have sent (#1543)
|
||||||
- Use vendor prefix for non merged MSC (#1537)
|
- Use vendor prefix for non merged MSC (#1537)
|
||||||
- Compress images before sending (#1333)
|
- Compress images before sending (#1333)
|
||||||
|
- Searching by displayname is case sensitive (#1468)
|
||||||
|
|
||||||
Translations 🗣:
|
Translations 🗣:
|
||||||
-
|
-
|
||||||
|
@ -16,12 +16,14 @@
|
|||||||
|
|
||||||
package im.vector.matrix.rx
|
package im.vector.matrix.rx
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import im.vector.matrix.android.api.query.QueryStringValue
|
import im.vector.matrix.android.api.query.QueryStringValue
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
import im.vector.matrix.android.api.session.room.Room
|
import im.vector.matrix.android.api.session.room.Room
|
||||||
import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams
|
import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams
|
||||||
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
||||||
import im.vector.matrix.android.api.session.room.model.ReadReceipt
|
import im.vector.matrix.android.api.session.room.model.ReadReceipt
|
||||||
|
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
|
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.RoomSummary
|
||||||
import im.vector.matrix.android.api.session.room.notification.RoomNotificationState
|
import im.vector.matrix.android.api.session.room.notification.RoomNotificationState
|
||||||
@ -101,6 +103,30 @@ class RxRoom(private val room: Room) {
|
|||||||
fun invite(userId: String, reason: String? = null): Completable = completableBuilder<Unit> {
|
fun invite(userId: String, reason: String? = null): Completable = completableBuilder<Unit> {
|
||||||
room.invite(userId, reason, it)
|
room.invite(userId, reason, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateTopic(topic: String): Completable = completableBuilder<Unit> {
|
||||||
|
room.updateTopic(topic, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateName(name: String): Completable = completableBuilder<Unit> {
|
||||||
|
room.updateName(name, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addRoomAlias(alias: String): Completable = completableBuilder<Unit> {
|
||||||
|
room.addRoomAlias(alias, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateCanonicalAlias(alias: String): Completable = completableBuilder<Unit> {
|
||||||
|
room.updateCanonicalAlias(alias, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateHistoryReadability(readability: RoomHistoryVisibility): Completable = completableBuilder<Unit> {
|
||||||
|
room.updateHistoryReadability(readability, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateAvatar(avatarUri: Uri, fileName: String): Completable = completableBuilder<Unit> {
|
||||||
|
room.updateAvatar(avatarUri, fileName, it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Room.rx(): RxRoom {
|
fun Room.rx(): RxRoom {
|
||||||
|
@ -31,6 +31,11 @@ android {
|
|||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
|
// The following argument makes the Android Test Orchestrator run its
|
||||||
|
// "pm clear" command after each test invocation. This command ensures
|
||||||
|
// that the app's state is completely cleared between tests.
|
||||||
|
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||||
|
|
||||||
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
|
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
|
||||||
resValue "string", "git_sdk_revision", "\"${gitRevision()}\""
|
resValue "string", "git_sdk_revision", "\"${gitRevision()}\""
|
||||||
resValue "string", "git_sdk_revision_unix_date", "\"${gitRevisionUnixDate()}\""
|
resValue "string", "git_sdk_revision_unix_date", "\"${gitRevisionUnixDate()}\""
|
||||||
@ -41,6 +46,10 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|
||||||
debug {
|
debug {
|
||||||
@ -181,6 +190,7 @@ dependencies {
|
|||||||
// Plant Timber tree for test
|
// Plant Timber tree for test
|
||||||
testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
|
testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
|
||||||
|
|
||||||
|
kaptAndroidTest "com.google.dagger:dagger-compiler:$daggerVersion"
|
||||||
androidTestImplementation 'androidx.test:core:1.2.0'
|
androidTestImplementation 'androidx.test:core:1.2.0'
|
||||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||||
androidTestImplementation 'androidx.test:rules:1.2.0'
|
androidTestImplementation 'androidx.test:rules:1.2.0'
|
||||||
@ -193,4 +203,7 @@ dependencies {
|
|||||||
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||||
// Plant Timber tree for test
|
// Plant Timber tree for test
|
||||||
androidTestImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
|
androidTestImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
|
||||||
|
|
||||||
|
androidTestUtil 'androidx.test:orchestrator:1.2.0'
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* 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 android.content.Context
|
||||||
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
|
import androidx.work.Configuration
|
||||||
|
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.common.DaggerTestMatrixComponent
|
||||||
|
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
|
||||||
|
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.Executors
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the main entry point to the matrix sdk.
|
||||||
|
* To get the singleton instance, use getInstance static method.
|
||||||
|
*/
|
||||||
|
class Matrix private constructor(context: Context, matrixConfiguration: MatrixConfiguration) {
|
||||||
|
|
||||||
|
@Inject internal lateinit var authenticationService: AuthenticationService
|
||||||
|
@Inject internal lateinit var userAgentHolder: UserAgentHolder
|
||||||
|
@Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver
|
||||||
|
@Inject internal lateinit var olmManager: OlmManager
|
||||||
|
@Inject internal lateinit var sessionManager: SessionManager
|
||||||
|
|
||||||
|
init {
|
||||||
|
Monarchy.init(context)
|
||||||
|
DaggerTestMatrixComponent.factory().create(context, matrixConfiguration).inject(this)
|
||||||
|
if (context.applicationContext !is Configuration.Provider) {
|
||||||
|
WorkManager.initialize(context, Configuration.Builder().setExecutor(Executors.newCachedThreadPool()).build())
|
||||||
|
}
|
||||||
|
ProcessLifecycleOwner.get().lifecycle.addObserver(backgroundDetectionObserver)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getUserAgent() = userAgentHolder.userAgent
|
||||||
|
|
||||||
|
fun authenticationService(): AuthenticationService {
|
||||||
|
return authenticationService
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private lateinit var instance: Matrix
|
||||||
|
private val isInit = AtomicBoolean(false)
|
||||||
|
|
||||||
|
fun initialize(context: Context, matrixConfiguration: MatrixConfiguration) {
|
||||||
|
if (isInit.compareAndSet(false, true)) {
|
||||||
|
instance = Matrix(context.applicationContext, matrixConfiguration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getInstance(context: Context): Matrix {
|
||||||
|
if (isInit.compareAndSet(false, true)) {
|
||||||
|
val appContext = context.applicationContext
|
||||||
|
if (appContext is MatrixConfiguration.Provider) {
|
||||||
|
val matrixConfiguration = (appContext as MatrixConfiguration.Provider).providesMatrixConfiguration()
|
||||||
|
instance = Matrix(appContext, matrixConfiguration)
|
||||||
|
} else {
|
||||||
|
throw IllegalStateException("Matrix is not initialized properly." +
|
||||||
|
" You should call Matrix.initialize or let your application implements MatrixConfiguration.Provider.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSdkVersion(): String {
|
||||||
|
return BuildConfig.VERSION_NAME + " (" + BuildConfig.GIT_SDK_REVISION + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decryptStream(inputStream: InputStream?, elementToDecrypt: ElementToDecrypt): InputStream? {
|
||||||
|
return MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -57,9 +57,10 @@ class CommonTestHelper(context: Context) {
|
|||||||
|
|
||||||
val matrix: Matrix
|
val matrix: Matrix
|
||||||
|
|
||||||
|
fun getTestInterceptor(session: Session): MockOkHttpInterceptor? = TestNetworkModule.interceptorForSession(session.sessionId) as? MockOkHttpInterceptor
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Matrix.initialize(context, MatrixConfiguration("TestFlavor"))
|
Matrix.initialize(context, MatrixConfiguration("TestFlavor"))
|
||||||
|
|
||||||
matrix = Matrix.getInstance(context)
|
matrix = Matrix.getInstance(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,6 +117,7 @@ class CommonTestHelper(context: Context) {
|
|||||||
* @param nbOfMessages the number of time the message will be sent
|
* @param nbOfMessages the number of time the message will be sent
|
||||||
*/
|
*/
|
||||||
fun sendTextMessage(room: Room, message: String, nbOfMessages: Int): List<TimelineEvent> {
|
fun sendTextMessage(room: Room, message: String, nbOfMessages: Int): List<TimelineEvent> {
|
||||||
|
val timeline = room.createTimeline(null, TimelineSettings(10))
|
||||||
val sentEvents = ArrayList<TimelineEvent>(nbOfMessages)
|
val sentEvents = ArrayList<TimelineEvent>(nbOfMessages)
|
||||||
val latch = CountDownLatch(1)
|
val latch = CountDownLatch(1)
|
||||||
val timelineListener = object : Timeline.Listener {
|
val timelineListener = object : Timeline.Listener {
|
||||||
@ -134,11 +136,12 @@ class CommonTestHelper(context: Context) {
|
|||||||
|
|
||||||
if (newMessages.size == nbOfMessages) {
|
if (newMessages.size == nbOfMessages) {
|
||||||
sentEvents.addAll(newMessages)
|
sentEvents.addAll(newMessages)
|
||||||
|
// Remove listener now, if not at the next update sendEvents could change
|
||||||
|
timeline.removeListener(this)
|
||||||
latch.countDown()
|
latch.countDown()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val timeline = room.createTimeline(null, TimelineSettings(10))
|
|
||||||
timeline.start()
|
timeline.start()
|
||||||
timeline.addListener(timelineListener)
|
timeline.addListener(timelineListener)
|
||||||
for (i in 0 until nbOfMessages) {
|
for (i in 0 until nbOfMessages) {
|
||||||
@ -146,11 +149,10 @@ class CommonTestHelper(context: Context) {
|
|||||||
}
|
}
|
||||||
// Wait 3 second more per message
|
// Wait 3 second more per message
|
||||||
await(latch, timeout = TestConstants.timeOutMillis + 3_000L * nbOfMessages)
|
await(latch, timeout = TestConstants.timeOutMillis + 3_000L * nbOfMessages)
|
||||||
timeline.removeListener(timelineListener)
|
|
||||||
timeline.dispose()
|
timeline.dispose()
|
||||||
|
|
||||||
// Check that all events has been created
|
// Check that all events has been created
|
||||||
assertEquals(nbOfMessages.toLong(), sentEvents.size.toLong())
|
assertEquals("Message number do not match $sentEvents", nbOfMessages.toLong(), sentEvents.size.toLong())
|
||||||
|
|
||||||
return sentEvents
|
return sentEvents
|
||||||
}
|
}
|
||||||
|
@ -17,8 +17,13 @@
|
|||||||
package im.vector.matrix.android.common
|
package im.vector.matrix.android.common
|
||||||
|
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
|
import android.util.Log
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
|
import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction
|
||||||
|
import im.vector.matrix.android.api.session.crypto.verification.OutgoingSasVerificationTransaction
|
||||||
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod
|
||||||
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
import im.vector.matrix.android.api.session.events.model.toContent
|
import im.vector.matrix.android.api.session.events.model.toContent
|
||||||
@ -34,6 +39,7 @@ import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
|||||||
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
||||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupAuthData
|
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupAuthData
|
||||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
|
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -41,6 +47,8 @@ import kotlinx.coroutines.runBlocking
|
|||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertNotNull
|
import org.junit.Assert.assertNotNull
|
||||||
import org.junit.Assert.assertNull
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import java.util.UUID
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
|
|
||||||
class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
||||||
@ -274,4 +282,141 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
|||||||
authData = createFakeMegolmBackupAuthData()
|
authData = createFakeMegolmBackupAuthData()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createDM(alice: Session, bob: Session): String {
|
||||||
|
val roomId = mTestHelper.doSync<String> {
|
||||||
|
alice.createRoom(
|
||||||
|
CreateRoomParams(invitedUserIds = listOf(bob.myUserId))
|
||||||
|
.setDirectMessage()
|
||||||
|
.enableEncryptionIfInvitedUsersSupportIt(),
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
mTestHelper.waitWithLatch { latch ->
|
||||||
|
val bobRoomSummariesLive = runBlocking(Dispatchers.Main) {
|
||||||
|
bob.getRoomSummariesLive(roomSummaryQueryParams { })
|
||||||
|
}
|
||||||
|
|
||||||
|
val newRoomObserver = object : Observer<List<RoomSummary>> {
|
||||||
|
override fun onChanged(t: List<RoomSummary>?) {
|
||||||
|
val indexOfFirst = t?.indexOfFirst { it.roomId == roomId } ?: -1
|
||||||
|
if (indexOfFirst != -1) {
|
||||||
|
latch.countDown()
|
||||||
|
bobRoomSummariesLive.removeObserver(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalScope.launch(Dispatchers.Main) {
|
||||||
|
bobRoomSummariesLive.observeForever(newRoomObserver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mTestHelper.waitWithLatch { latch ->
|
||||||
|
val bobRoomSummariesLive = runBlocking(Dispatchers.Main) {
|
||||||
|
bob.getRoomSummariesLive(roomSummaryQueryParams { })
|
||||||
|
}
|
||||||
|
|
||||||
|
val newRoomObserver = object : Observer<List<RoomSummary>> {
|
||||||
|
override fun onChanged(t: List<RoomSummary>?) {
|
||||||
|
if (bob.getRoom(roomId)
|
||||||
|
?.getRoomMember(bob.myUserId)
|
||||||
|
?.membership == Membership.JOIN) {
|
||||||
|
latch.countDown()
|
||||||
|
bobRoomSummariesLive.removeObserver(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalScope.launch(Dispatchers.Main) {
|
||||||
|
bobRoomSummariesLive.observeForever(newRoomObserver)
|
||||||
|
}
|
||||||
|
|
||||||
|
mTestHelper.doSync<Unit> { bob.joinRoom(roomId, callback = it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return roomId
|
||||||
|
}
|
||||||
|
|
||||||
|
fun initializeCrossSigning(session: Session) {
|
||||||
|
mTestHelper.doSync<Unit> {
|
||||||
|
session.cryptoService().crossSigningService()
|
||||||
|
.initializeCrossSigning(UserPasswordAuth(
|
||||||
|
user = session.myUserId,
|
||||||
|
password = TestConstants.PASSWORD
|
||||||
|
), it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun verifySASCrossSign(alice: Session, bob: Session, roomId: String) {
|
||||||
|
assertTrue(alice.cryptoService().crossSigningService().canCrossSign())
|
||||||
|
assertTrue(bob.cryptoService().crossSigningService().canCrossSign())
|
||||||
|
|
||||||
|
val requestID = UUID.randomUUID().toString()
|
||||||
|
val aliceVerificationService = alice.cryptoService().verificationService()
|
||||||
|
val bobVerificationService = bob.cryptoService().verificationService()
|
||||||
|
|
||||||
|
aliceVerificationService.beginKeyVerificationInDMs(
|
||||||
|
VerificationMethod.SAS,
|
||||||
|
requestID,
|
||||||
|
roomId,
|
||||||
|
bob.myUserId,
|
||||||
|
bob.sessionParams.credentials.deviceId!!,
|
||||||
|
null)
|
||||||
|
|
||||||
|
// we should reach SHOW SAS on both
|
||||||
|
var alicePovTx: OutgoingSasVerificationTransaction? = null
|
||||||
|
var bobPovTx: IncomingSasVerificationTransaction? = null
|
||||||
|
|
||||||
|
// wait for alice to get the ready
|
||||||
|
mTestHelper.waitWithLatch {
|
||||||
|
mTestHelper.retryPeriodicallyWithLatch(it) {
|
||||||
|
bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? IncomingSasVerificationTransaction
|
||||||
|
Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}")
|
||||||
|
if (bobPovTx?.state == VerificationTxState.OnStarted) {
|
||||||
|
bobPovTx?.performAccept()
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mTestHelper.waitWithLatch {
|
||||||
|
mTestHelper.retryPeriodicallyWithLatch(it) {
|
||||||
|
alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID) as? OutgoingSasVerificationTransaction
|
||||||
|
Log.v("TEST", "== alicePovTx is ${alicePovTx?.uxState}")
|
||||||
|
alicePovTx?.state == VerificationTxState.ShortCodeReady
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// wait for alice to get the ready
|
||||||
|
mTestHelper.waitWithLatch {
|
||||||
|
mTestHelper.retryPeriodicallyWithLatch(it) {
|
||||||
|
bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? IncomingSasVerificationTransaction
|
||||||
|
Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}")
|
||||||
|
if (bobPovTx?.state == VerificationTxState.OnStarted) {
|
||||||
|
bobPovTx?.performAccept()
|
||||||
|
}
|
||||||
|
bobPovTx?.state == VerificationTxState.ShortCodeReady
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("SAS code do not match", alicePovTx!!.getDecimalCodeRepresentation(), bobPovTx!!.getDecimalCodeRepresentation())
|
||||||
|
|
||||||
|
bobPovTx!!.userHasVerifiedShortCode()
|
||||||
|
alicePovTx!!.userHasVerifiedShortCode()
|
||||||
|
|
||||||
|
mTestHelper.waitWithLatch {
|
||||||
|
mTestHelper.retryPeriodicallyWithLatch(it) {
|
||||||
|
alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mTestHelper.waitWithLatch {
|
||||||
|
mTestHelper.retryPeriodicallyWithLatch(it) {
|
||||||
|
alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package im.vector.matrix.android.common
|
package im.vector.matrix.android.common
|
||||||
|
|
||||||
|
import im.vector.matrix.android.internal.session.TestInterceptor
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Protocol
|
import okhttp3.Protocol
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
@ -37,7 +38,7 @@ import javax.net.ssl.HttpsURLConnection
|
|||||||
* AutoDiscovery().findClientConfig("matrix.org", <callback>)
|
* AutoDiscovery().findClientConfig("matrix.org", <callback>)
|
||||||
* </code>
|
* </code>
|
||||||
*/
|
*/
|
||||||
class MockOkHttpInterceptor : Interceptor {
|
class MockOkHttpInterceptor : TestInterceptor {
|
||||||
|
|
||||||
private var rules: ArrayList<Rule> = ArrayList()
|
private var rules: ArrayList<Rule> = ArrayList()
|
||||||
|
|
||||||
@ -45,6 +46,12 @@ class MockOkHttpInterceptor : Interceptor {
|
|||||||
rules.add(rule)
|
rules.add(rule)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearRules() {
|
||||||
|
rules.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
override var sessionId: String? = null
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val originalRequest = chain.request()
|
val originalRequest = chain.request()
|
||||||
|
|
||||||
|
@ -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.common
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import dagger.BindsInstance
|
||||||
|
import dagger.Component
|
||||||
|
import im.vector.matrix.android.api.MatrixConfiguration
|
||||||
|
import im.vector.matrix.android.internal.auth.AuthModule
|
||||||
|
import im.vector.matrix.android.internal.di.MatrixComponent
|
||||||
|
import im.vector.matrix.android.internal.di.MatrixModule
|
||||||
|
import im.vector.matrix.android.internal.di.MatrixScope
|
||||||
|
import im.vector.matrix.android.internal.di.NetworkModule
|
||||||
|
|
||||||
|
@Component(modules = [TestModule::class, MatrixModule::class, NetworkModule::class, AuthModule::class, TestNetworkModule::class])
|
||||||
|
@MatrixScope
|
||||||
|
internal interface TestMatrixComponent : MatrixComponent {
|
||||||
|
|
||||||
|
@Component.Factory
|
||||||
|
interface Factory {
|
||||||
|
fun create(@BindsInstance context: Context,
|
||||||
|
@BindsInstance matrixConfiguration: MatrixConfiguration): TestMatrixComponent
|
||||||
|
}
|
||||||
|
}
|
@ -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.common
|
||||||
|
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import im.vector.matrix.android.internal.di.MatrixComponent
|
||||||
|
|
||||||
|
@Module
|
||||||
|
internal abstract class TestModule {
|
||||||
|
@Binds
|
||||||
|
abstract fun providesMatrixComponent(testMatrixComponent: TestMatrixComponent): MatrixComponent
|
||||||
|
}
|
@ -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.common
|
||||||
|
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import im.vector.matrix.android.internal.session.MockHttpInterceptor
|
||||||
|
import im.vector.matrix.android.internal.session.TestInterceptor
|
||||||
|
|
||||||
|
@Module
|
||||||
|
internal object TestNetworkModule {
|
||||||
|
|
||||||
|
val interceptors = ArrayList<TestInterceptor>()
|
||||||
|
|
||||||
|
fun interceptorForSession(sessionId: String): TestInterceptor? = interceptors.firstOrNull { it.sessionId == sessionId }
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@JvmStatic
|
||||||
|
@MockHttpInterceptor
|
||||||
|
fun providesTestInterceptor(): TestInterceptor? {
|
||||||
|
return MockOkHttpInterceptor().also {
|
||||||
|
interceptors.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -22,10 +22,8 @@ import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
|
|||||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertFalse
|
|
||||||
import org.junit.Assert.assertNotEquals
|
import org.junit.Assert.assertNotEquals
|
||||||
import org.junit.Assert.assertNull
|
import org.junit.Assert.assertNull
|
||||||
import org.junit.Assert.assertTrue
|
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
@ -45,22 +43,22 @@ class CryptoStoreTest : InstrumentedTest {
|
|||||||
Realm.init(context())
|
Realm.init(context())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
// @Test
|
||||||
fun test_metadata_realm_ok() {
|
// fun test_metadata_realm_ok() {
|
||||||
val cryptoStore: IMXCryptoStore = cryptoStoreHelper.createStore()
|
// val cryptoStore: IMXCryptoStore = cryptoStoreHelper.createStore()
|
||||||
|
//
|
||||||
assertFalse(cryptoStore.hasData())
|
// assertFalse(cryptoStore.hasData())
|
||||||
|
//
|
||||||
cryptoStore.open()
|
// cryptoStore.open()
|
||||||
|
//
|
||||||
assertEquals("deviceId_sample", cryptoStore.getDeviceId())
|
// assertEquals("deviceId_sample", cryptoStore.getDeviceId())
|
||||||
|
//
|
||||||
assertTrue(cryptoStore.hasData())
|
// assertTrue(cryptoStore.hasData())
|
||||||
|
//
|
||||||
// Cleanup
|
// // Cleanup
|
||||||
cryptoStore.close()
|
// cryptoStore.close()
|
||||||
cryptoStore.deleteStore()
|
// cryptoStore.deleteStore()
|
||||||
}
|
// }
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun test_lastSessionUsed() {
|
fun test_lastSessionUsed() {
|
||||||
|
@ -29,6 +29,7 @@ import im.vector.matrix.android.api.session.events.model.toModel
|
|||||||
import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
|
import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
|
||||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||||
import im.vector.matrix.android.common.CommonTestHelper
|
import im.vector.matrix.android.common.CommonTestHelper
|
||||||
|
import im.vector.matrix.android.common.CryptoTestHelper
|
||||||
import im.vector.matrix.android.common.SessionTestParams
|
import im.vector.matrix.android.common.SessionTestParams
|
||||||
import im.vector.matrix.android.common.TestConstants
|
import im.vector.matrix.android.common.TestConstants
|
||||||
import im.vector.matrix.android.internal.crypto.GossipingRequestState
|
import im.vector.matrix.android.internal.crypto.GossipingRequestState
|
||||||
@ -56,6 +57,7 @@ import java.util.concurrent.CountDownLatch
|
|||||||
class KeyShareTests : InstrumentedTest {
|
class KeyShareTests : InstrumentedTest {
|
||||||
|
|
||||||
private val mTestHelper = CommonTestHelper(context())
|
private val mTestHelper = CommonTestHelper(context())
|
||||||
|
private val mCryptoTestHelper = CryptoTestHelper(mTestHelper)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun test_DoNotSelfShareIfNotTrusted() {
|
fun test_DoNotSelfShareIfNotTrusted() {
|
||||||
@ -234,6 +236,7 @@ class KeyShareTests : InstrumentedTest {
|
|||||||
}
|
}
|
||||||
if (tx.state == VerificationTxState.ShortCodeReady) {
|
if (tx.state == VerificationTxState.ShortCodeReady) {
|
||||||
session1ShortCode = tx.getDecimalCodeRepresentation()
|
session1ShortCode = tx.getDecimalCodeRepresentation()
|
||||||
|
Thread.sleep(500)
|
||||||
tx.userHasVerifiedShortCode()
|
tx.userHasVerifiedShortCode()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -246,6 +249,7 @@ class KeyShareTests : InstrumentedTest {
|
|||||||
if (tx is SasVerificationTransaction) {
|
if (tx is SasVerificationTransaction) {
|
||||||
if (tx.state == VerificationTxState.ShortCodeReady) {
|
if (tx.state == VerificationTxState.ShortCodeReady) {
|
||||||
session2ShortCode = tx.getDecimalCodeRepresentation()
|
session2ShortCode = tx.getDecimalCodeRepresentation()
|
||||||
|
Thread.sleep(500)
|
||||||
tx.userHasVerifiedShortCode()
|
tx.userHasVerifiedShortCode()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -285,5 +289,8 @@ class KeyShareTests : InstrumentedTest {
|
|||||||
keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey == creationInfo.recoveryKey
|
keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey == creationInfo.recoveryKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mTestHelper.signOutAndClose(aliceSession1)
|
||||||
|
mTestHelper.signOutAndClose(aliceSession2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,245 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.crypto.gossiping
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import im.vector.matrix.android.InstrumentedTest
|
||||||
|
import im.vector.matrix.android.api.NoOpMatrixCallback
|
||||||
|
import im.vector.matrix.android.api.extensions.tryThis
|
||||||
|
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||||
|
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.common.CommonTestHelper
|
||||||
|
import im.vector.matrix.android.common.CryptoTestHelper
|
||||||
|
import im.vector.matrix.android.common.MockOkHttpInterceptor
|
||||||
|
import im.vector.matrix.android.common.SessionTestParams
|
||||||
|
import im.vector.matrix.android.common.TestConstants
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.FixMethodOrder
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.junit.runners.MethodSorters
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@FixMethodOrder(MethodSorters.JVM)
|
||||||
|
class WithHeldTests : InstrumentedTest {
|
||||||
|
|
||||||
|
private val mTestHelper = CommonTestHelper(context())
|
||||||
|
private val mCryptoTestHelper = CryptoTestHelper(mTestHelper)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_WithHeldUnverifiedReason() {
|
||||||
|
// =============================
|
||||||
|
// ARRANGE
|
||||||
|
// =============================
|
||||||
|
|
||||||
|
val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
|
||||||
|
val bobSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
|
||||||
|
|
||||||
|
// Initialize cross signing on both
|
||||||
|
mCryptoTestHelper.initializeCrossSigning(aliceSession)
|
||||||
|
mCryptoTestHelper.initializeCrossSigning(bobSession)
|
||||||
|
|
||||||
|
val roomId = mCryptoTestHelper.createDM(aliceSession, bobSession)
|
||||||
|
mCryptoTestHelper.verifySASCrossSign(aliceSession, bobSession, roomId)
|
||||||
|
|
||||||
|
val roomAlicePOV = aliceSession.getRoom(roomId)!!
|
||||||
|
|
||||||
|
val bobUnverifiedSession = mTestHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true))
|
||||||
|
|
||||||
|
// =============================
|
||||||
|
// ACT
|
||||||
|
// =============================
|
||||||
|
|
||||||
|
// Alice decide to not send to unverified sessions
|
||||||
|
aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(true)
|
||||||
|
|
||||||
|
val timelineEvent = mTestHelper.sendTextMessage(roomAlicePOV, "Hello Bob", 1).first()
|
||||||
|
|
||||||
|
// await for bob unverified session to get the message
|
||||||
|
mTestHelper.waitWithLatch { latch ->
|
||||||
|
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
bobUnverifiedSession.getRoom(roomId)?.getTimeLineEvent(timelineEvent.eventId) != null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val eventBobPOV = bobUnverifiedSession.getRoom(roomId)?.getTimeLineEvent(timelineEvent.eventId)!!
|
||||||
|
|
||||||
|
// =============================
|
||||||
|
// ASSERT
|
||||||
|
// =============================
|
||||||
|
|
||||||
|
// Bob should not be able to decrypt because the keys is withheld
|
||||||
|
try {
|
||||||
|
// .. might need to wait a bit for stability?
|
||||||
|
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
|
||||||
|
Assert.fail("This session should not be able to decrypt")
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
val type = (failure as MXCryptoError.Base).errorType
|
||||||
|
val technicalMessage = failure.technicalMessage
|
||||||
|
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
|
||||||
|
Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// enable back sending to unverified
|
||||||
|
aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(false)
|
||||||
|
|
||||||
|
val secondEvent = mTestHelper.sendTextMessage(roomAlicePOV, "Verify your device!!", 1).first()
|
||||||
|
|
||||||
|
mTestHelper.waitWithLatch { latch ->
|
||||||
|
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
val ev = bobUnverifiedSession.getRoom(roomId)?.getTimeLineEvent(secondEvent.eventId)
|
||||||
|
// wait until it's decrypted
|
||||||
|
ev?.root?.getClearType() == EventType.MESSAGE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Previous message should still be undecryptable (partially withheld session)
|
||||||
|
try {
|
||||||
|
// .. might need to wait a bit for stability?
|
||||||
|
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
|
||||||
|
Assert.fail("This session should not be able to decrypt")
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
val type = (failure as MXCryptoError.Base).errorType
|
||||||
|
val technicalMessage = failure.technicalMessage
|
||||||
|
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
|
||||||
|
Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
mTestHelper.signOutAndClose(aliceSession)
|
||||||
|
mTestHelper.signOutAndClose(bobSession)
|
||||||
|
mTestHelper.signOutAndClose(bobUnverifiedSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_WithHeldNoOlm() {
|
||||||
|
val testData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||||
|
val aliceSession = testData.firstSession
|
||||||
|
val bobSession = testData.secondSession!!
|
||||||
|
val aliceInterceptor = mTestHelper.getTestInterceptor(aliceSession)
|
||||||
|
|
||||||
|
// Simulate no OTK
|
||||||
|
aliceInterceptor!!.addRule(MockOkHttpInterceptor.SimpleRule(
|
||||||
|
"/keys/claim",
|
||||||
|
200,
|
||||||
|
"""
|
||||||
|
{ "one_time_keys" : {} }
|
||||||
|
"""
|
||||||
|
))
|
||||||
|
Log.d("#TEST", "Recovery :${aliceSession.sessionParams.credentials.accessToken}")
|
||||||
|
|
||||||
|
val roomAlicePov = aliceSession.getRoom(testData.roomId)!!
|
||||||
|
|
||||||
|
val eventId = mTestHelper.sendTextMessage(roomAlicePov, "first message", 1).first().eventId
|
||||||
|
|
||||||
|
// await for bob session to get the message
|
||||||
|
mTestHelper.waitWithLatch { latch ->
|
||||||
|
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
bobSession.getRoom(testData.roomId)?.getTimeLineEvent(eventId) != null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Previous message should still be undecryptable (partially withheld session)
|
||||||
|
val eventBobPOV = bobSession.getRoom(testData.roomId)?.getTimeLineEvent(eventId)
|
||||||
|
try {
|
||||||
|
// .. might need to wait a bit for stability?
|
||||||
|
bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "")
|
||||||
|
Assert.fail("This session should not be able to decrypt")
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
val type = (failure as MXCryptoError.Base).errorType
|
||||||
|
val technicalMessage = failure.technicalMessage
|
||||||
|
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
|
||||||
|
Assert.assertEquals("Cause should be unverified", WithHeldCode.NO_OLM.value, technicalMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that alice has marked the session to be shared with bob
|
||||||
|
val sessionId = eventBobPOV!!.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
|
||||||
|
val chainIndex = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject(bobSession.myUserId, bobSession.sessionParams.credentials.deviceId)
|
||||||
|
|
||||||
|
Assert.assertEquals("Alice should have marked bob's device for this session", 0, chainIndex)
|
||||||
|
// Add a new device for bob
|
||||||
|
|
||||||
|
aliceInterceptor.clearRules()
|
||||||
|
val bobSecondSession = mTestHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(withInitialSync = true))
|
||||||
|
// send a second message
|
||||||
|
val secondMessageId = mTestHelper.sendTextMessage(roomAlicePov, "second message", 1).first().eventId
|
||||||
|
|
||||||
|
// Check that the
|
||||||
|
// await for bob SecondSession session to get the message
|
||||||
|
mTestHelper.waitWithLatch { latch ->
|
||||||
|
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
bobSecondSession.getRoom(testData.roomId)?.getTimeLineEvent(secondMessageId) != null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val chainIndex2 = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject(bobSecondSession.myUserId, bobSecondSession.sessionParams.credentials.deviceId)
|
||||||
|
|
||||||
|
Assert.assertEquals("Alice should have marked bob's device for this session", 1, chainIndex2)
|
||||||
|
|
||||||
|
aliceInterceptor.clearRules()
|
||||||
|
testData.cleanUp(mTestHelper)
|
||||||
|
mTestHelper.signOutAndClose(bobSecondSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_WithHeldKeyRequest() {
|
||||||
|
val testData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||||
|
val aliceSession = testData.firstSession
|
||||||
|
val bobSession = testData.secondSession!!
|
||||||
|
|
||||||
|
val roomAlicePov = aliceSession.getRoom(testData.roomId)!!
|
||||||
|
|
||||||
|
val eventId = mTestHelper.sendTextMessage(roomAlicePov, "first message", 1).first().eventId
|
||||||
|
|
||||||
|
mTestHelper.signOutAndClose(bobSession)
|
||||||
|
|
||||||
|
// Create a new session for bob
|
||||||
|
|
||||||
|
val bobSecondSession = mTestHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true))
|
||||||
|
// initialize to force request keys if missing
|
||||||
|
mCryptoTestHelper.initializeCrossSigning(bobSecondSession)
|
||||||
|
|
||||||
|
// Trust bob second device from Alice POV
|
||||||
|
aliceSession.cryptoService().crossSigningService().trustDevice(bobSecondSession.sessionParams.deviceId!!, NoOpMatrixCallback())
|
||||||
|
bobSecondSession.cryptoService().crossSigningService().trustDevice(aliceSession.sessionParams.deviceId!!, NoOpMatrixCallback())
|
||||||
|
|
||||||
|
var sessionId: String? = null
|
||||||
|
// Check that the
|
||||||
|
// await for bob SecondSession session to get the message
|
||||||
|
mTestHelper.waitWithLatch { latch ->
|
||||||
|
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimeLineEvent(eventId)?.also {
|
||||||
|
// try to decrypt and force key request
|
||||||
|
tryThis { bobSecondSession.cryptoService().decryptEvent(it.root, "") }
|
||||||
|
}
|
||||||
|
sessionId = timeLineEvent?.root?.content?.toModel<EncryptedEventContent>()?.sessionId
|
||||||
|
timeLineEvent != null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that bob second session requested the key
|
||||||
|
mTestHelper.waitWithLatch { latch ->
|
||||||
|
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
val wc = bobSecondSession.cryptoService().getWithHeldMegolmSession(roomAlicePov.roomId, sessionId!!)
|
||||||
|
wc?.code == WithHeldCode.UNAUTHORISED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,8 +13,20 @@
|
|||||||
android:authorities="${applicationId}.workmanager-init"
|
android:authorities="${applicationId}.workmanager-init"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
tools:node="remove" />
|
tools:node="remove" />
|
||||||
|
<!--
|
||||||
|
The SDK offers a secured File provider to access downloaded files.
|
||||||
|
Access to these file will be given via the FileService, with a temporary
|
||||||
|
read access permission
|
||||||
|
-->
|
||||||
|
<provider
|
||||||
|
android:name="im.vector.matrix.android.api.session.file.MatrixSDKFileProvider"
|
||||||
|
android:authorities="${applicationId}.mx-sdk.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/sdk_provider_paths" />
|
||||||
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -89,6 +89,7 @@ interface AuthenticationService {
|
|||||||
* Perform a wellknown request, using the domain from the matrixId
|
* Perform a wellknown request, using the domain from the matrixId
|
||||||
*/
|
*/
|
||||||
fun getWellKnownData(matrixId: String,
|
fun getWellKnownData(matrixId: String,
|
||||||
|
homeServerConnectionConfig: HomeServerConnectionConfig?,
|
||||||
callback: MatrixCallback<WellknownResult>): Cancelable
|
callback: MatrixCallback<WellknownResult>): Cancelable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -18,6 +18,7 @@ package im.vector.matrix.android.api.failure
|
|||||||
|
|
||||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||||
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
|
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
|
||||||
|
import im.vector.matrix.android.internal.network.ssl.Fingerprint
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -32,9 +33,11 @@ import java.io.IOException
|
|||||||
sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) {
|
sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) {
|
||||||
data class Unknown(val throwable: Throwable? = null) : Failure(throwable)
|
data class Unknown(val throwable: Throwable? = null) : Failure(throwable)
|
||||||
data class Cancelled(val throwable: Throwable? = null) : Failure(throwable)
|
data class Cancelled(val throwable: Throwable? = null) : Failure(throwable)
|
||||||
|
data class UnrecognizedCertificateFailure(val url: String, val fingerprint: Fingerprint) : Failure()
|
||||||
data class NetworkConnection(val ioException: IOException? = null) : Failure(ioException)
|
data class NetworkConnection(val ioException: IOException? = null) : Failure(ioException)
|
||||||
data class ServerError(val error: MatrixError, val httpCode: Int) : Failure(RuntimeException(error.toString()))
|
data class ServerError(val error: MatrixError, val httpCode: Int) : Failure(RuntimeException(error.toString()))
|
||||||
object SuccessError : Failure(RuntimeException(RuntimeException("SuccessResult is false")))
|
object SuccessError : Failure(RuntimeException(RuntimeException("SuccessResult is false")))
|
||||||
|
|
||||||
// When server send an error, but it cannot be interpreted as a MatrixError
|
// When server send an error, but it cannot be interpreted as a MatrixError
|
||||||
data class OtherServerError(val errorBody: String, val httpCode: Int) : Failure(RuntimeException("HTTP $httpCode: $errorBody"))
|
data class OtherServerError(val errorBody: String, val httpCode: Int) : Failure(RuntimeException("HTTP $httpCode: $errorBody"))
|
||||||
|
|
||||||
|
@ -16,8 +16,11 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.api.failure
|
package im.vector.matrix.android.api.failure
|
||||||
|
|
||||||
|
import im.vector.matrix.android.internal.network.ssl.Fingerprint
|
||||||
|
|
||||||
// This class will be sent to the bus
|
// This class will be sent to the bus
|
||||||
sealed class GlobalError {
|
sealed class GlobalError {
|
||||||
data class InvalidToken(val softLogout: Boolean) : GlobalError()
|
data class InvalidToken(val softLogout: Boolean) : GlobalError()
|
||||||
data class ConsentNotGivenError(val consentUri: String) : GlobalError()
|
data class ConsentNotGivenError(val consentUri: String) : GlobalError()
|
||||||
|
data class CertificateError(val fingerprint: Fingerprint) : GlobalError()
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ import im.vector.matrix.android.api.session.call.CallSignalingService
|
|||||||
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
||||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||||
|
import im.vector.matrix.android.api.session.file.ContentDownloadStateTracker
|
||||||
import im.vector.matrix.android.api.session.file.FileService
|
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.group.GroupService
|
||||||
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService
|
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService
|
||||||
@ -59,7 +60,6 @@ interface Session :
|
|||||||
CacheService,
|
CacheService,
|
||||||
SignOutService,
|
SignOutService,
|
||||||
FilterService,
|
FilterService,
|
||||||
FileService,
|
|
||||||
TermsService,
|
TermsService,
|
||||||
ProfileService,
|
ProfileService,
|
||||||
PushRuleService,
|
PushRuleService,
|
||||||
@ -152,6 +152,11 @@ interface Session :
|
|||||||
*/
|
*/
|
||||||
fun typingUsersTracker(): TypingUsersTracker
|
fun typingUsersTracker(): TypingUsersTracker
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the ContentDownloadStateTracker associated with the session
|
||||||
|
*/
|
||||||
|
fun contentDownloadProgressTracker(): ContentDownloadStateTracker
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the cryptoService associated with the session
|
* Returns the cryptoService associated with the session
|
||||||
*/
|
*/
|
||||||
@ -177,6 +182,11 @@ interface Session :
|
|||||||
*/
|
*/
|
||||||
fun callSignalingService(): CallSignalingService
|
fun callSignalingService(): CallSignalingService
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the file download service associated with the session
|
||||||
|
*/
|
||||||
|
fun fileService(): FileService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a listener to the session.
|
* Add a listener to the session.
|
||||||
* @param listener the listener to add.
|
* @param listener the listener to add.
|
||||||
|
@ -36,6 +36,7 @@ import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
|||||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
|
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldContent
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||||
@ -145,4 +146,8 @@ interface CryptoService {
|
|||||||
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
|
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
|
||||||
|
|
||||||
fun getGossipingEventsTrail(): List<Event>
|
fun getGossipingEventsTrail(): List<Event>
|
||||||
|
|
||||||
|
// For testing shared session
|
||||||
|
fun getSharedWithInfo(roomId: String?, sessionId: String) : MXUsersDevicesMap<Int>
|
||||||
|
fun getWithHeldMegolmSession(roomId: String, sessionId: String) : RoomKeyWithHeldContent?
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,8 @@ sealed class MXCryptoError : Throwable() {
|
|||||||
MISSING_PROPERTY,
|
MISSING_PROPERTY,
|
||||||
OLM,
|
OLM,
|
||||||
UNKNOWN_DEVICES,
|
UNKNOWN_DEVICES,
|
||||||
UNKNOWN_MESSAGE_INDEX
|
UNKNOWN_MESSAGE_INDEX,
|
||||||
|
KEYS_WITHHELD
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -82,6 +82,9 @@ data class Event(
|
|||||||
@Transient
|
@Transient
|
||||||
var mCryptoError: MXCryptoError.ErrorType? = null
|
var mCryptoError: MXCryptoError.ErrorType? = null
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
var mCryptoErrorReason: String? = null
|
||||||
|
|
||||||
@Transient
|
@Transient
|
||||||
var sendState: SendState = SendState.UNKNOWN
|
var sendState: SendState = SendState.UNKNOWN
|
||||||
|
|
||||||
@ -182,6 +185,7 @@ data class Event(
|
|||||||
if (redacts != other.redacts) return false
|
if (redacts != other.redacts) return false
|
||||||
if (mxDecryptionResult != other.mxDecryptionResult) return false
|
if (mxDecryptionResult != other.mxDecryptionResult) return false
|
||||||
if (mCryptoError != other.mCryptoError) return false
|
if (mCryptoError != other.mCryptoError) return false
|
||||||
|
if (mCryptoErrorReason != other.mCryptoErrorReason) return false
|
||||||
if (sendState != other.sendState) return false
|
if (sendState != other.sendState) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@ -200,6 +204,7 @@ data class Event(
|
|||||||
result = 31 * result + (redacts?.hashCode() ?: 0)
|
result = 31 * result + (redacts?.hashCode() ?: 0)
|
||||||
result = 31 * result + (mxDecryptionResult?.hashCode() ?: 0)
|
result = 31 * result + (mxDecryptionResult?.hashCode() ?: 0)
|
||||||
result = 31 * result + (mCryptoError?.hashCode() ?: 0)
|
result = 31 * result + (mCryptoError?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (mCryptoErrorReason?.hashCode() ?: 0)
|
||||||
result = 31 * result + sendState.hashCode()
|
result = 31 * result + sendState.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@ -230,3 +235,11 @@ fun Event.isVideoMessage(): Boolean {
|
|||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Event.isFileMessage(): Boolean {
|
||||||
|
return getClearType() == EventType.MESSAGE
|
||||||
|
&& when (getClearContent()?.toModel<MessageContent>()?.msgType) {
|
||||||
|
MessageType.MSGTYPE_FILE -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -66,6 +66,7 @@ object EventType {
|
|||||||
// Key share events
|
// Key share events
|
||||||
const val ROOM_KEY_REQUEST = "m.room_key_request"
|
const val ROOM_KEY_REQUEST = "m.room_key_request"
|
||||||
const val FORWARDED_ROOM_KEY = "m.forwarded_room_key"
|
const val FORWARDED_ROOM_KEY = "m.forwarded_room_key"
|
||||||
|
const val ROOM_KEY_WITHHELD = "org.matrix.room_key.withheld"
|
||||||
|
|
||||||
const val REQUEST_SECRET = "m.secret.request"
|
const val REQUEST_SECRET = "m.secret.request"
|
||||||
const val SEND_SECRET = "m.secret.send"
|
const val SEND_SECRET = "m.secret.send"
|
||||||
|
@ -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.file
|
||||||
|
|
||||||
|
interface ContentDownloadStateTracker {
|
||||||
|
fun track(key: String, updateListener: UpdateListener)
|
||||||
|
fun unTrack(key: String, updateListener: UpdateListener)
|
||||||
|
fun clear()
|
||||||
|
|
||||||
|
sealed class State {
|
||||||
|
object Idle : State()
|
||||||
|
data class Downloading(val current: Long, val total: Long, val indeterminate: Boolean) : State()
|
||||||
|
object Decrypting : State()
|
||||||
|
object Success : State()
|
||||||
|
data class Failure(val errorCode: Int) : State()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateListener {
|
||||||
|
fun onDownloadStateUpdate(state: State)
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.api.session.file
|
package im.vector.matrix.android.api.session.file
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.util.Cancelable
|
import im.vector.matrix.android.api.util.Cancelable
|
||||||
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
|
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
|
||||||
@ -31,26 +32,58 @@ interface FileService {
|
|||||||
* Download file in external storage
|
* Download file in external storage
|
||||||
*/
|
*/
|
||||||
TO_EXPORT,
|
TO_EXPORT,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download file in cache
|
* Download file in cache
|
||||||
*/
|
*/
|
||||||
FOR_INTERNAL_USE,
|
FOR_INTERNAL_USE,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download file in file provider path
|
* Download file in file provider path
|
||||||
*/
|
*/
|
||||||
FOR_EXTERNAL_SHARE
|
FOR_EXTERNAL_SHARE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class FileState {
|
||||||
|
IN_CACHE,
|
||||||
|
DOWNLOADING,
|
||||||
|
UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download a file.
|
* Download a file.
|
||||||
* Result will be a decrypted file, stored in the cache folder. id parameter will be used to create a sub folder to avoid name collision.
|
* Result will be a decrypted file, stored in the cache folder. url parameter will be used to create unique filename to avoid name collision.
|
||||||
* You can pass the eventId
|
|
||||||
*/
|
*/
|
||||||
fun downloadFile(
|
fun downloadFile(
|
||||||
downloadMode: DownloadMode,
|
downloadMode: DownloadMode,
|
||||||
id: String,
|
id: String,
|
||||||
fileName: String,
|
fileName: String,
|
||||||
|
mimeType: String?,
|
||||||
url: String?,
|
url: String?,
|
||||||
elementToDecrypt: ElementToDecrypt?,
|
elementToDecrypt: ElementToDecrypt?,
|
||||||
callback: MatrixCallback<File>): Cancelable
|
callback: MatrixCallback<File>): Cancelable
|
||||||
|
|
||||||
|
fun isFileInCache(mxcUrl: String, mimeType: String?): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this URI and pass it to intent using flag Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
* (if not other app won't be able to access it)
|
||||||
|
*/
|
||||||
|
fun getTemporarySharableURI(mxcUrl: String, mimeType: String?): Uri?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get information on the given file.
|
||||||
|
* Mimetype should be the same one as passed to downloadFile (limitation for now)
|
||||||
|
*/
|
||||||
|
fun fileState(mxcUrl: String, mimeType: String?): FileState
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all the files downloaded by the service
|
||||||
|
*/
|
||||||
|
fun clearCache()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get size of cached files
|
||||||
|
*/
|
||||||
|
fun getCacheSize(): Int
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* 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.file
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We have to declare our own file provider to avoid collision with apps using the sdk
|
||||||
|
* and having their own
|
||||||
|
*/
|
||||||
|
class MatrixSDKFileProvider : FileProvider() {
|
||||||
|
override fun getType(uri: Uri): String? {
|
||||||
|
return super.getType(uri) ?: "plain/text"
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.api.session.profile
|
package im.vector.matrix.android.api.session.profile
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.session.identity.ThreePid
|
import im.vector.matrix.android.api.session.identity.ThreePid
|
||||||
@ -48,6 +49,14 @@ interface ProfileService {
|
|||||||
*/
|
*/
|
||||||
fun setDisplayName(userId: String, newDisplayName: String, matrixCallback: MatrixCallback<Unit>): Cancelable
|
fun setDisplayName(userId: String, newDisplayName: String, matrixCallback: MatrixCallback<Unit>): Cancelable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the avatar for this user
|
||||||
|
* @param userId the userId to update the avatar of
|
||||||
|
* @param newAvatarUri the new avatar uri of the user
|
||||||
|
* @param fileName the fileName of selected image
|
||||||
|
*/
|
||||||
|
fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String, matrixCallback: MatrixCallback<Unit>): Cancelable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the current avatarUrl for this user.
|
* Return the current avatarUrl for this user.
|
||||||
* @param userId the userId param to look for
|
* @param userId the userId param to look for
|
||||||
|
@ -19,6 +19,7 @@ package im.vector.matrix.android.api.session.room.model
|
|||||||
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
|
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
|
||||||
import im.vector.matrix.android.api.session.room.model.tag.RoomTag
|
import im.vector.matrix.android.api.session.room.model.tag.RoomTag
|
||||||
import im.vector.matrix.android.api.session.room.send.UserDraft
|
import im.vector.matrix.android.api.session.room.send.UserDraft
|
||||||
|
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.api.session.room.timeline.TimelineEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -27,7 +28,9 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
|||||||
*/
|
*/
|
||||||
data class RoomSummary constructor(
|
data class RoomSummary constructor(
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
|
// Computed display name
|
||||||
val displayName: String = "",
|
val displayName: String = "",
|
||||||
|
val name: String = "",
|
||||||
val topic: String = "",
|
val topic: String = "",
|
||||||
val avatarUrl: String = "",
|
val avatarUrl: String = "",
|
||||||
val canonicalAlias: String? = null,
|
val canonicalAlias: String? = null,
|
||||||
@ -47,6 +50,7 @@ data class RoomSummary constructor(
|
|||||||
val userDrafts: List<UserDraft> = emptyList(),
|
val userDrafts: List<UserDraft> = emptyList(),
|
||||||
val isEncrypted: Boolean,
|
val isEncrypted: Boolean,
|
||||||
val encryptionEventTs: Long?,
|
val encryptionEventTs: Long?,
|
||||||
|
val typingUsers: List<SenderInfo>,
|
||||||
val inviterId: String? = null,
|
val inviterId: String? = null,
|
||||||
val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS,
|
val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS,
|
||||||
val roomEncryptionTrustLevel: RoomEncryptionTrustLevel? = null
|
val roomEncryptionTrustLevel: RoomEncryptionTrustLevel? = null
|
||||||
|
@ -51,4 +51,8 @@ data class MessageAudioContent(
|
|||||||
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
|
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
|
||||||
*/
|
*/
|
||||||
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
|
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
|
||||||
) : MessageWithAttachmentContent
|
) : MessageWithAttachmentContent {
|
||||||
|
|
||||||
|
override val mimeType: String?
|
||||||
|
get() = encryptedFileInfo?.mimetype ?: audioInfo?.mimeType
|
||||||
|
}
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.api.session.room.model.message
|
package im.vector.matrix.android.api.session.room.model.message
|
||||||
|
|
||||||
import android.content.ClipDescription
|
import android.webkit.MimeTypeMap
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import im.vector.matrix.android.api.session.events.model.Content
|
import im.vector.matrix.android.api.session.events.model.Content
|
||||||
@ -59,12 +59,12 @@ data class MessageFileContent(
|
|||||||
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
|
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
|
||||||
) : MessageWithAttachmentContent {
|
) : MessageWithAttachmentContent {
|
||||||
|
|
||||||
fun getMimeType(): String {
|
override val mimeType: String?
|
||||||
// Mimetype default to plain text, should not be used
|
get() = encryptedFileInfo?.mimetype
|
||||||
return encryptedFileInfo?.mimetype
|
|
||||||
?: info?.mimeType
|
?: info?.mimeType
|
||||||
?: ClipDescription.MIMETYPE_TEXT_PLAIN
|
?: MimeTypeMap.getFileExtensionFromUrl(filename ?: body)?.let { extension ->
|
||||||
}
|
MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
||||||
|
}
|
||||||
|
|
||||||
fun getFileName(): String {
|
fun getFileName(): String {
|
||||||
return filename ?: body
|
return filename ?: body
|
||||||
|
@ -52,4 +52,7 @@ data class MessageImageContent(
|
|||||||
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
|
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
|
||||||
*/
|
*/
|
||||||
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
|
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
|
||||||
) : MessageImageInfoContent
|
) : MessageImageInfoContent {
|
||||||
|
override val mimeType: String?
|
||||||
|
get() = encryptedFileInfo?.mimetype ?: info?.mimeType ?: "image/*"
|
||||||
|
}
|
||||||
|
@ -52,4 +52,7 @@ data class MessageStickerContent(
|
|||||||
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
|
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
|
||||||
*/
|
*/
|
||||||
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
|
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
|
||||||
) : MessageImageInfoContent
|
) : MessageImageInfoContent {
|
||||||
|
override val mimeType: String?
|
||||||
|
get() = encryptedFileInfo?.mimetype ?: info?.mimeType
|
||||||
|
}
|
||||||
|
@ -51,4 +51,7 @@ data class MessageVideoContent(
|
|||||||
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
|
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
|
||||||
*/
|
*/
|
||||||
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
|
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
|
||||||
) : MessageWithAttachmentContent
|
) : MessageWithAttachmentContent {
|
||||||
|
override val mimeType: String?
|
||||||
|
get() = encryptedFileInfo?.mimetype ?: videoInfo?.mimeType
|
||||||
|
}
|
||||||
|
@ -31,9 +31,13 @@ interface MessageWithAttachmentContent : MessageContent {
|
|||||||
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
|
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
|
||||||
*/
|
*/
|
||||||
val encryptedFileInfo: EncryptedFileInfo?
|
val encryptedFileInfo: EncryptedFileInfo?
|
||||||
|
|
||||||
|
val mimeType: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the url of the encrypted file or of the file
|
* Get the url of the encrypted file or of the file
|
||||||
*/
|
*/
|
||||||
fun MessageWithAttachmentContent.getFileUrl() = encryptedFileInfo?.url ?: url
|
fun MessageWithAttachmentContent.getFileUrl() = encryptedFileInfo?.url ?: url
|
||||||
|
|
||||||
|
fun MessageWithAttachmentContent.getFileName() = (this as? MessageFileContent)?.getFileName() ?: body
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.api.session.room.powerlevels
|
package im.vector.matrix.android.api.session.room.powerlevels
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
|
import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -123,4 +124,59 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
|
|||||||
else -> Role.Moderator.value
|
else -> Role.Moderator.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user have the necessary power level to change room name
|
||||||
|
* @param userId the id of the user to check for.
|
||||||
|
* @return true if able to change room name
|
||||||
|
*/
|
||||||
|
fun isUserAbleToChangeRoomName(userId: String): Boolean {
|
||||||
|
val powerLevel = getUserPowerLevelValue(userId)
|
||||||
|
val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_NAME] ?: powerLevelsContent.stateDefault
|
||||||
|
return powerLevel >= minPowerLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user have the necessary power level to change room topic
|
||||||
|
* @param userId the id of the user to check for.
|
||||||
|
* @return true if able to change room topic
|
||||||
|
*/
|
||||||
|
fun isUserAbleToChangeRoomTopic(userId: String): Boolean {
|
||||||
|
val powerLevel = getUserPowerLevelValue(userId)
|
||||||
|
val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_TOPIC] ?: powerLevelsContent.stateDefault
|
||||||
|
return powerLevel >= minPowerLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user have the necessary power level to change room canonical alias
|
||||||
|
* @param userId the id of the user to check for.
|
||||||
|
* @return true if able to change room canonical alias
|
||||||
|
*/
|
||||||
|
fun isUserAbleToChangeRoomCanonicalAlias(userId: String): Boolean {
|
||||||
|
val powerLevel = getUserPowerLevelValue(userId)
|
||||||
|
val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_CANONICAL_ALIAS] ?: powerLevelsContent.stateDefault
|
||||||
|
return powerLevel >= minPowerLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user have the necessary power level to change room history readability
|
||||||
|
* @param userId the id of the user to check for.
|
||||||
|
* @return true if able to change room history readability
|
||||||
|
*/
|
||||||
|
fun isUserAbleToChangeRoomHistoryReadability(userId: String): Boolean {
|
||||||
|
val powerLevel = getUserPowerLevelValue(userId)
|
||||||
|
val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_HISTORY_VISIBILITY] ?: powerLevelsContent.stateDefault
|
||||||
|
return powerLevel >= minPowerLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user have the necessary power level to change room avatar
|
||||||
|
* @param userId the id of the user to check for.
|
||||||
|
* @return true if able to change room avatar
|
||||||
|
*/
|
||||||
|
fun isUserAbleToChangeRoomAvatar(userId: String): Boolean {
|
||||||
|
val powerLevel = getUserPowerLevelValue(userId)
|
||||||
|
val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_AVATAR] ?: powerLevelsContent.stateDefault
|
||||||
|
return powerLevel >= minPowerLevel
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,10 +16,12 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.api.session.room.state
|
package im.vector.matrix.android.api.session.room.state
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.query.QueryStringValue
|
import im.vector.matrix.android.api.query.QueryStringValue
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
|
||||||
import im.vector.matrix.android.api.util.Cancelable
|
import im.vector.matrix.android.api.util.Cancelable
|
||||||
import im.vector.matrix.android.api.util.JsonDict
|
import im.vector.matrix.android.api.util.JsonDict
|
||||||
import im.vector.matrix.android.api.util.Optional
|
import im.vector.matrix.android.api.util.Optional
|
||||||
@ -31,6 +33,31 @@ interface StateService {
|
|||||||
*/
|
*/
|
||||||
fun updateTopic(topic: String, callback: MatrixCallback<Unit>): Cancelable
|
fun updateTopic(topic: String, callback: MatrixCallback<Unit>): Cancelable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the name of the room
|
||||||
|
*/
|
||||||
|
fun updateName(name: String, callback: MatrixCallback<Unit>): Cancelable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add new alias to the room.
|
||||||
|
*/
|
||||||
|
fun addRoomAlias(roomAlias: String, callback: MatrixCallback<Unit>): Cancelable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the canonical alias of the room
|
||||||
|
*/
|
||||||
|
fun updateCanonicalAlias(alias: String, callback: MatrixCallback<Unit>): Cancelable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the history readability of the room
|
||||||
|
*/
|
||||||
|
fun updateHistoryReadability(readability: RoomHistoryVisibility, callback: MatrixCallback<Unit>): Cancelable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the avatar of the room
|
||||||
|
*/
|
||||||
|
fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback<Unit>): Cancelable
|
||||||
|
|
||||||
fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict, callback: MatrixCallback<Unit>): Cancelable
|
fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict, callback: MatrixCallback<Unit>): Cancelable
|
||||||
|
|
||||||
fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event?
|
fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event?
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.api.session.typing
|
package im.vector.matrix.android.api.session.typing
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import im.vector.matrix.android.api.session.room.sender.SenderInfo
|
import im.vector.matrix.android.api.session.room.sender.SenderInfo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -29,9 +28,4 @@ interface TypingUsersTracker {
|
|||||||
* Returns the sender information of all currently typing users in a room, excluding yourself.
|
* Returns the sender information of all currently typing users in a room, excluding yourself.
|
||||||
*/
|
*/
|
||||||
fun getTypingUsers(roomId: String): List<SenderInfo>
|
fun getTypingUsers(roomId: String): List<SenderInfo>
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a LiveData of the sender information of all currently typing users in a room, excluding yourself.
|
|
||||||
*/
|
|
||||||
fun getTypingUsersLive(roomId: String): LiveData<List<SenderInfo>>
|
|
||||||
}
|
}
|
||||||
|
@ -43,6 +43,8 @@ import im.vector.matrix.android.internal.auth.version.isSupportedBySdk
|
|||||||
import im.vector.matrix.android.internal.di.Unauthenticated
|
import im.vector.matrix.android.internal.di.Unauthenticated
|
||||||
import im.vector.matrix.android.internal.network.RetrofitFactory
|
import im.vector.matrix.android.internal.network.RetrofitFactory
|
||||||
import im.vector.matrix.android.internal.network.executeRequest
|
import im.vector.matrix.android.internal.network.executeRequest
|
||||||
|
import im.vector.matrix.android.internal.network.httpclient.addSocketFactory
|
||||||
|
import im.vector.matrix.android.internal.network.ssl.UnrecognizedCertificateException
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
import im.vector.matrix.android.internal.task.configureWith
|
import im.vector.matrix.android.internal.task.configureWith
|
||||||
import im.vector.matrix.android.internal.task.launchToCallback
|
import im.vector.matrix.android.internal.task.launchToCallback
|
||||||
@ -121,7 +123,11 @@ internal class DefaultAuthenticationService @Inject constructor(
|
|||||||
callback.onSuccess(it)
|
callback.onSuccess(it)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
callback.onFailure(it)
|
if (it is UnrecognizedCertificateException) {
|
||||||
|
callback.onFailure(Failure.UnrecognizedCertificateFailure(homeServerConnectionConfig.homeServerUri.toString(), it.fingerprint))
|
||||||
|
} else {
|
||||||
|
callback.onFailure(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -209,7 +215,7 @@ internal class DefaultAuthenticationService @Inject constructor(
|
|||||||
|
|
||||||
// Create a fake userId, for the getWellknown task
|
// Create a fake userId, for the getWellknown task
|
||||||
val fakeUserId = "@alice:$domain"
|
val fakeUserId = "@alice:$domain"
|
||||||
val wellknownResult = getWellknownTask.execute(GetWellknownTask.Params(fakeUserId))
|
val wellknownResult = getWellknownTask.execute(GetWellknownTask.Params(fakeUserId, homeServerConnectionConfig))
|
||||||
|
|
||||||
return when (wellknownResult) {
|
return when (wellknownResult) {
|
||||||
is WellknownResult.Prompt -> {
|
is WellknownResult.Prompt -> {
|
||||||
@ -248,7 +254,7 @@ internal class DefaultAuthenticationService @Inject constructor(
|
|||||||
?: let {
|
?: let {
|
||||||
pendingSessionData?.homeServerConnectionConfig?.let {
|
pendingSessionData?.homeServerConnectionConfig?.let {
|
||||||
DefaultRegistrationWizard(
|
DefaultRegistrationWizard(
|
||||||
okHttpClient,
|
buildClient(it),
|
||||||
retrofitFactory,
|
retrofitFactory,
|
||||||
coroutineDispatchers,
|
coroutineDispatchers,
|
||||||
sessionCreator,
|
sessionCreator,
|
||||||
@ -269,7 +275,7 @@ internal class DefaultAuthenticationService @Inject constructor(
|
|||||||
?: let {
|
?: let {
|
||||||
pendingSessionData?.homeServerConnectionConfig?.let {
|
pendingSessionData?.homeServerConnectionConfig?.let {
|
||||||
DefaultLoginWizard(
|
DefaultLoginWizard(
|
||||||
okHttpClient,
|
buildClient(it),
|
||||||
retrofitFactory,
|
retrofitFactory,
|
||||||
coroutineDispatchers,
|
coroutineDispatchers,
|
||||||
sessionCreator,
|
sessionCreator,
|
||||||
@ -321,9 +327,11 @@ internal class DefaultAuthenticationService @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getWellKnownData(matrixId: String, callback: MatrixCallback<WellknownResult>): Cancelable {
|
override fun getWellKnownData(matrixId: String,
|
||||||
|
homeServerConnectionConfig: HomeServerConnectionConfig?,
|
||||||
|
callback: MatrixCallback<WellknownResult>): Cancelable {
|
||||||
return getWellknownTask
|
return getWellknownTask
|
||||||
.configureWith(GetWellknownTask.Params(matrixId)) {
|
.configureWith(GetWellknownTask.Params(matrixId, homeServerConnectionConfig)) {
|
||||||
this.callback = callback
|
this.callback = callback
|
||||||
}
|
}
|
||||||
.executeBy(taskExecutor)
|
.executeBy(taskExecutor)
|
||||||
@ -347,7 +355,14 @@ internal class DefaultAuthenticationService @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI {
|
private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI {
|
||||||
val retrofit = retrofitFactory.create(okHttpClient, homeServerConnectionConfig.homeServerUri.toString())
|
val retrofit = retrofitFactory.create(buildClient(homeServerConnectionConfig), homeServerConnectionConfig.homeServerUri.toString())
|
||||||
return retrofit.create(AuthAPI::class.java)
|
return retrofit.create(AuthAPI::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildClient(homeServerConnectionConfig: HomeServerConnectionConfig): OkHttpClient {
|
||||||
|
return okHttpClient.get()
|
||||||
|
.newBuilder()
|
||||||
|
.addSocketFactory(homeServerConnectionConfig)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
package im.vector.matrix.android.internal.auth.login
|
package im.vector.matrix.android.internal.auth.login
|
||||||
|
|
||||||
import android.util.Patterns
|
import android.util.Patterns
|
||||||
import dagger.Lazy
|
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.auth.data.Credentials
|
import im.vector.matrix.android.api.auth.data.Credentials
|
||||||
import im.vector.matrix.android.api.auth.login.LoginWizard
|
import im.vector.matrix.android.api.auth.login.LoginWizard
|
||||||
@ -44,7 +43,7 @@ import kotlinx.coroutines.withContext
|
|||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
internal class DefaultLoginWizard(
|
internal class DefaultLoginWizard(
|
||||||
okHttpClient: Lazy<OkHttpClient>,
|
okHttpClient: OkHttpClient,
|
||||||
retrofitFactory: RetrofitFactory,
|
retrofitFactory: RetrofitFactory,
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
private val sessionCreator: SessionCreator,
|
private val sessionCreator: SessionCreator,
|
||||||
|
@ -26,6 +26,7 @@ import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
|
|||||||
import im.vector.matrix.android.internal.di.Unauthenticated
|
import im.vector.matrix.android.internal.di.Unauthenticated
|
||||||
import im.vector.matrix.android.internal.network.RetrofitFactory
|
import im.vector.matrix.android.internal.network.RetrofitFactory
|
||||||
import im.vector.matrix.android.internal.network.executeRequest
|
import im.vector.matrix.android.internal.network.executeRequest
|
||||||
|
import im.vector.matrix.android.internal.network.httpclient.addSocketFactory
|
||||||
import im.vector.matrix.android.internal.task.Task
|
import im.vector.matrix.android.internal.task.Task
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -47,7 +48,8 @@ internal class DefaultDirectLoginTask @Inject constructor(
|
|||||||
) : DirectLoginTask {
|
) : DirectLoginTask {
|
||||||
|
|
||||||
override suspend fun execute(params: DirectLoginTask.Params): Session {
|
override suspend fun execute(params: DirectLoginTask.Params): Session {
|
||||||
val authAPI = retrofitFactory.create(okHttpClient, params.homeServerConnectionConfig.homeServerUri.toString())
|
val client = buildClient(params.homeServerConnectionConfig)
|
||||||
|
val authAPI = retrofitFactory.create(client, params.homeServerConnectionConfig.homeServerUri.toString())
|
||||||
.create(AuthAPI::class.java)
|
.create(AuthAPI::class.java)
|
||||||
|
|
||||||
val loginParams = PasswordLoginParams.userIdentifier(params.userId, params.password, params.deviceName)
|
val loginParams = PasswordLoginParams.userIdentifier(params.userId, params.password, params.deviceName)
|
||||||
@ -58,4 +60,11 @@ internal class DefaultDirectLoginTask @Inject constructor(
|
|||||||
|
|
||||||
return sessionCreator.createSession(credentials, params.homeServerConnectionConfig)
|
return sessionCreator.createSession(credentials, params.homeServerConnectionConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildClient(homeServerConnectionConfig: HomeServerConnectionConfig): OkHttpClient {
|
||||||
|
return okHttpClient.get()
|
||||||
|
.newBuilder()
|
||||||
|
.addSocketFactory(homeServerConnectionConfig)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.internal.auth.registration
|
package im.vector.matrix.android.internal.auth.registration
|
||||||
|
|
||||||
import dagger.Lazy
|
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
|
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
|
||||||
import im.vector.matrix.android.api.auth.registration.RegisterThreePid
|
import im.vector.matrix.android.api.auth.registration.RegisterThreePid
|
||||||
@ -41,7 +40,7 @@ import okhttp3.OkHttpClient
|
|||||||
* This class execute the registration request and is responsible to keep the session of interactive authentication
|
* This class execute the registration request and is responsible to keep the session of interactive authentication
|
||||||
*/
|
*/
|
||||||
internal class DefaultRegistrationWizard(
|
internal class DefaultRegistrationWizard(
|
||||||
private val okHttpClient: Lazy<OkHttpClient>,
|
private val okHttpClient: OkHttpClient,
|
||||||
private val retrofitFactory: RetrofitFactory,
|
private val retrofitFactory: RetrofitFactory,
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
private val sessionCreator: SessionCreator,
|
private val sessionCreator: SessionCreator,
|
||||||
|
@ -52,6 +52,7 @@ import im.vector.matrix.android.internal.crypto.actions.MegolmSessionDataImporte
|
|||||||
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
|
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
|
||||||
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
|
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
|
||||||
import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting
|
import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting
|
||||||
|
import im.vector.matrix.android.internal.crypto.algorithms.IMXWithHeldExtension
|
||||||
import im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
|
import im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
|
||||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
|
import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
|
||||||
import im.vector.matrix.android.internal.crypto.crosssigning.DefaultCrossSigningService
|
import im.vector.matrix.android.internal.crypto.crosssigning.DefaultCrossSigningService
|
||||||
@ -65,6 +66,7 @@ import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
|||||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||||
import im.vector.matrix.android.internal.crypto.model.event.OlmEventContent
|
import im.vector.matrix.android.internal.crypto.model.event.OlmEventContent
|
||||||
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
|
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldContent
|
||||||
import im.vector.matrix.android.internal.crypto.model.event.SecretSendEventContent
|
import im.vector.matrix.android.internal.crypto.model.event.SecretSendEventContent
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
||||||
@ -807,6 +809,9 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
cryptoStore.saveGossipingEvent(event)
|
cryptoStore.saveGossipingEvent(event)
|
||||||
onSecretSendReceived(event)
|
onSecretSendReceived(event)
|
||||||
}
|
}
|
||||||
|
EventType.ROOM_KEY_WITHHELD -> {
|
||||||
|
onKeyWithHeldReceived(event)
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
@ -834,6 +839,20 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
alg.onRoomKeyEvent(event, keysBackupService)
|
alg.onRoomKeyEvent(event, keysBackupService)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onKeyWithHeldReceived(event: Event) {
|
||||||
|
val withHeldContent = event.getClearContent().toModel<RoomKeyWithHeldContent>() ?: return Unit.also {
|
||||||
|
Timber.e("## CRYPTO | Malformed onKeyWithHeldReceived() : missing fields")
|
||||||
|
}
|
||||||
|
Timber.d("## CRYPTO | onKeyWithHeldReceived() received : content <$withHeldContent>")
|
||||||
|
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(withHeldContent.roomId, withHeldContent.algorithm)
|
||||||
|
if (alg is IMXWithHeldExtension) {
|
||||||
|
alg.onRoomKeyWithHeldEvent(withHeldContent)
|
||||||
|
} else {
|
||||||
|
Timber.e("## CRYPTO | onKeyWithHeldReceived() : Unable to handle WithHeldContent for ${withHeldContent.algorithm}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun onSecretSendReceived(event: Event) {
|
private fun onSecretSendReceived(event: Event) {
|
||||||
Timber.i("## CRYPTO | GOSSIP onSecretSend() : onSecretSendReceived ${event.content?.get("sender_key")}")
|
Timber.i("## CRYPTO | GOSSIP onSecretSend() : onSecretSendReceived ${event.content?.get("sender_key")}")
|
||||||
if (!event.isEncrypted()) {
|
if (!event.isEncrypted()) {
|
||||||
@ -1197,7 +1216,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
// }
|
// }
|
||||||
roomDecryptorProvider
|
roomDecryptorProvider
|
||||||
.getOrCreateRoomDecryptor(event.roomId, wireContent.algorithm)
|
.getOrCreateRoomDecryptor(event.roomId, wireContent.algorithm)
|
||||||
?.requestKeysForEvent(event) ?: run {
|
?.requestKeysForEvent(event, false) ?: run {
|
||||||
Timber.v("## CRYPTO | requestRoomKeyForEvent() : No room decryptor for roomId:${event.roomId} algorithm:${wireContent.algorithm}")
|
Timber.v("## CRYPTO | requestRoomKeyForEvent() : No room decryptor for roomId:${event.roomId} algorithm:${wireContent.algorithm}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1311,6 +1330,13 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
return cryptoStore.getGossipingEventsTrail()
|
return cryptoStore.getGossipingEventsTrail()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int> {
|
||||||
|
return cryptoStore.getSharedWithInfo(roomId, sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent? {
|
||||||
|
return cryptoStore.getWithHeldMegolmSession(roomId, sessionId)
|
||||||
|
}
|
||||||
/* ==========================================================================================
|
/* ==========================================================================================
|
||||||
* For test only
|
* For test only
|
||||||
* ========================================================================================== */
|
* ========================================================================================== */
|
||||||
|
@ -71,5 +71,5 @@ internal interface IMXDecrypting {
|
|||||||
|
|
||||||
fun shareSecretWithDevice(request: IncomingSecretShareRequest, secretValue : String) {}
|
fun shareSecretWithDevice(request: IncomingSecretShareRequest, secretValue : String) {}
|
||||||
|
|
||||||
fun requestKeysForEvent(event: Event)
|
fun requestKeysForEvent(event: Event, withHeld: Boolean)
|
||||||
}
|
}
|
||||||
|
@ -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.internal.crypto.algorithms
|
||||||
|
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldContent
|
||||||
|
|
||||||
|
internal interface IMXWithHeldExtension {
|
||||||
|
fun onRoomKeyWithHeldEvent(withHeldInfo: RoomKeyWithHeldContent)
|
||||||
|
}
|
@ -30,10 +30,12 @@ import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
|
|||||||
import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
||||||
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
|
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
|
||||||
import im.vector.matrix.android.internal.crypto.algorithms.IMXDecrypting
|
import im.vector.matrix.android.internal.crypto.algorithms.IMXDecrypting
|
||||||
|
import im.vector.matrix.android.internal.crypto.algorithms.IMXWithHeldExtension
|
||||||
import im.vector.matrix.android.internal.crypto.keysbackup.DefaultKeysBackupService
|
import im.vector.matrix.android.internal.crypto.keysbackup.DefaultKeysBackupService
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||||
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
|
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldContent
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.ForwardedRoomKeyContent
|
import im.vector.matrix.android.internal.crypto.model.rest.ForwardedRoomKeyContent
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||||
@ -53,7 +55,7 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||||||
private val sendToDeviceTask: SendToDeviceTask,
|
private val sendToDeviceTask: SendToDeviceTask,
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
private val cryptoCoroutineScope: CoroutineScope
|
private val cryptoCoroutineScope: CoroutineScope
|
||||||
) : IMXDecrypting {
|
) : IMXDecrypting, IMXWithHeldExtension {
|
||||||
|
|
||||||
var newSessionListener: NewSessionListener? = null
|
var newSessionListener: NewSessionListener? = null
|
||||||
|
|
||||||
@ -61,7 +63,7 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||||||
* Events which we couldn't decrypt due to unknown sessions / indexes: map from
|
* Events which we couldn't decrypt due to unknown sessions / indexes: map from
|
||||||
* senderKey|sessionId to timelines to list of MatrixEvents.
|
* senderKey|sessionId to timelines to list of MatrixEvents.
|
||||||
*/
|
*/
|
||||||
private var pendingEvents: MutableMap<String /* senderKey|sessionId */, MutableMap<String /* timelineId */, MutableList<Event>>> = HashMap()
|
// private var pendingEvents: MutableMap<String /* senderKey|sessionId */, MutableMap<String /* timelineId */, MutableList<Event>>> = HashMap()
|
||||||
|
|
||||||
@Throws(MXCryptoError::class)
|
@Throws(MXCryptoError::class)
|
||||||
override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||||
@ -113,9 +115,21 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||||||
if (throwable is MXCryptoError.OlmError) {
|
if (throwable is MXCryptoError.OlmError) {
|
||||||
// TODO Check the value of .message
|
// TODO Check the value of .message
|
||||||
if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") {
|
if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") {
|
||||||
addEventToPendingList(event, timeline)
|
// addEventToPendingList(event, timeline)
|
||||||
|
// The session might has been partially withheld (and only pass ratcheted)
|
||||||
|
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
|
||||||
|
if (withHeldInfo != null) {
|
||||||
|
if (requestKeysOnFail) {
|
||||||
|
requestKeysForEvent(event, true)
|
||||||
|
}
|
||||||
|
// Encapsulate as withHeld exception
|
||||||
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.KEYS_WITHHELD,
|
||||||
|
withHeldInfo.code?.value ?: "",
|
||||||
|
withHeldInfo.reason)
|
||||||
|
}
|
||||||
|
|
||||||
if (requestKeysOnFail) {
|
if (requestKeysOnFail) {
|
||||||
requestKeysForEvent(event)
|
requestKeysForEvent(event, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,10 +142,25 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||||||
detailedReason)
|
detailedReason)
|
||||||
}
|
}
|
||||||
if (throwable is MXCryptoError.Base) {
|
if (throwable is MXCryptoError.Base) {
|
||||||
if (throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
|
if (
|
||||||
addEventToPendingList(event, timeline)
|
/** if the session is unknown*/
|
||||||
if (requestKeysOnFail) {
|
throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID
|
||||||
requestKeysForEvent(event)
|
) {
|
||||||
|
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
|
||||||
|
if (withHeldInfo != null) {
|
||||||
|
if (requestKeysOnFail) {
|
||||||
|
requestKeysForEvent(event, true)
|
||||||
|
}
|
||||||
|
// Encapsulate as withHeld exception
|
||||||
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.KEYS_WITHHELD,
|
||||||
|
withHeldInfo.code?.value ?: "",
|
||||||
|
withHeldInfo.reason)
|
||||||
|
} else {
|
||||||
|
// This is un-used in riotX SDK, not sure if needed
|
||||||
|
// addEventToPendingList(event, timeline)
|
||||||
|
if (requestKeysOnFail) {
|
||||||
|
requestKeysForEvent(event, false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -147,12 +176,12 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||||||
*
|
*
|
||||||
* @param event the event
|
* @param event the event
|
||||||
*/
|
*/
|
||||||
override fun requestKeysForEvent(event: Event) {
|
override fun requestKeysForEvent(event: Event, withHeld: Boolean) {
|
||||||
val sender = event.senderId ?: return
|
val sender = event.senderId ?: return
|
||||||
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
|
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
|
||||||
val senderDevice = encryptedEventContent?.deviceId ?: return
|
val senderDevice = encryptedEventContent?.deviceId ?: return
|
||||||
|
|
||||||
val recipients = if (event.senderId == userId) {
|
val recipients = if (event.senderId == userId || withHeld) {
|
||||||
mapOf(
|
mapOf(
|
||||||
userId to listOf("*")
|
userId to listOf("*")
|
||||||
)
|
)
|
||||||
@ -176,25 +205,25 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||||||
outgoingGossipingRequestManager.sendRoomKeyRequest(requestBody, recipients)
|
outgoingGossipingRequestManager.sendRoomKeyRequest(requestBody, recipients)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Add an event to the list of those we couldn't decrypt the first time we
|
// * Add an event to the list of those we couldn't decrypt the first time we
|
||||||
* saw them.
|
// * saw them.
|
||||||
*
|
// *
|
||||||
* @param event the event to try to decrypt later
|
// * @param event the event to try to decrypt later
|
||||||
* @param timelineId the timeline identifier
|
// * @param timelineId the timeline identifier
|
||||||
*/
|
// */
|
||||||
private fun addEventToPendingList(event: Event, timelineId: String) {
|
// private fun addEventToPendingList(event: Event, timelineId: String) {
|
||||||
val encryptedEventContent = event.content.toModel<EncryptedEventContent>() ?: return
|
// val encryptedEventContent = event.content.toModel<EncryptedEventContent>() ?: return
|
||||||
val pendingEventsKey = "${encryptedEventContent.senderKey}|${encryptedEventContent.sessionId}"
|
// val pendingEventsKey = "${encryptedEventContent.senderKey}|${encryptedEventContent.sessionId}"
|
||||||
|
//
|
||||||
val timeline = pendingEvents.getOrPut(pendingEventsKey) { HashMap() }
|
// val timeline = pendingEvents.getOrPut(pendingEventsKey) { HashMap() }
|
||||||
val events = timeline.getOrPut(timelineId) { ArrayList() }
|
// val events = timeline.getOrPut(timelineId) { ArrayList() }
|
||||||
|
//
|
||||||
if (event !in events) {
|
// if (event !in events) {
|
||||||
Timber.v("## CRYPTO | addEventToPendingList() : add Event ${event.eventId} in room id ${event.roomId}")
|
// Timber.v("## CRYPTO | addEventToPendingList() : add Event ${event.eventId} in room id ${event.roomId}")
|
||||||
events.add(event)
|
// events.add(event)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle a key event.
|
* Handle a key event.
|
||||||
@ -349,4 +378,10 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onRoomKeyWithHeldEvent(withHeldInfo: RoomKeyWithHeldContent) {
|
||||||
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
|
cryptoStore.addWithHeldMegolmSession(withHeldInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.internal.crypto.algorithms.megolm
|
package im.vector.matrix.android.internal.crypto.algorithms.megolm
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.auth.data.Credentials
|
import im.vector.matrix.android.api.auth.data.Credentials
|
||||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||||
import im.vector.matrix.android.api.session.events.model.Content
|
import im.vector.matrix.android.api.session.events.model.Content
|
||||||
@ -31,9 +32,14 @@ import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting
|
|||||||
import im.vector.matrix.android.internal.crypto.keysbackup.DefaultKeysBackupService
|
import im.vector.matrix.android.internal.crypto.keysbackup.DefaultKeysBackupService
|
||||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldContent
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.forEach
|
||||||
import im.vector.matrix.android.internal.crypto.repository.WarnOnUnknownDeviceRepository
|
import im.vector.matrix.android.internal.crypto.repository.WarnOnUnknownDeviceRepository
|
||||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||||
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
|
import im.vector.matrix.android.internal.task.configureWith
|
||||||
import im.vector.matrix.android.internal.util.JsonCanonicalizer
|
import im.vector.matrix.android.internal.util.JsonCanonicalizer
|
||||||
import im.vector.matrix.android.internal.util.convertToUTF8
|
import im.vector.matrix.android.internal.util.convertToUTF8
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@ -49,7 +55,8 @@ internal class MXMegolmEncryption(
|
|||||||
private val credentials: Credentials,
|
private val credentials: Credentials,
|
||||||
private val sendToDeviceTask: SendToDeviceTask,
|
private val sendToDeviceTask: SendToDeviceTask,
|
||||||
private val messageEncrypter: MessageEncrypter,
|
private val messageEncrypter: MessageEncrypter,
|
||||||
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository
|
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
|
||||||
|
private val taskExecutor: TaskExecutor
|
||||||
) : IMXEncrypting {
|
) : IMXEncrypting {
|
||||||
|
|
||||||
// OutboundSessionInfo. Null if we haven't yet started setting one up. Note
|
// OutboundSessionInfo. Null if we haven't yet started setting one up. Note
|
||||||
@ -69,9 +76,26 @@ internal class MXMegolmEncryption(
|
|||||||
val ts = System.currentTimeMillis()
|
val ts = System.currentTimeMillis()
|
||||||
Timber.v("## CRYPTO | encryptEventContent : getDevicesInRoom")
|
Timber.v("## CRYPTO | encryptEventContent : getDevicesInRoom")
|
||||||
val devices = getDevicesInRoom(userIds)
|
val devices = getDevicesInRoom(userIds)
|
||||||
Timber.v("## CRYPTO | encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.map}")
|
Timber.v("## CRYPTO | encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.map}")
|
||||||
val outboundSession = ensureOutboundSession(devices)
|
val outboundSession = ensureOutboundSession(devices.allowedDevices)
|
||||||
|
|
||||||
return encryptContent(outboundSession, eventType, eventContent)
|
return encryptContent(outboundSession, eventType, eventContent)
|
||||||
|
.also {
|
||||||
|
notifyWithheldForSession(devices.withHeldDevices, outboundSession)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notifyWithheldForSession(devices: MXUsersDevicesMap<WithHeldCode>, outboundSession: MXOutboundSessionInfo) {
|
||||||
|
mutableListOf<Pair<UserDevice, WithHeldCode>>().apply {
|
||||||
|
devices.forEach { userId, deviceId, withheldCode ->
|
||||||
|
this.add(UserDevice(userId, deviceId) to withheldCode)
|
||||||
|
}
|
||||||
|
}.groupBy(
|
||||||
|
{ it.second },
|
||||||
|
{ it.first }
|
||||||
|
).forEach { (code, targets) ->
|
||||||
|
notifyKeyWithHeld(targets, outboundSession.sessionId, olmDevice.deviceCurve25519Key, code)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun discardSessionKey() {
|
override fun discardSessionKey() {
|
||||||
@ -95,7 +119,7 @@ internal class MXMegolmEncryption(
|
|||||||
|
|
||||||
defaultKeysBackupService.maybeBackupKeys()
|
defaultKeysBackupService.maybeBackupKeys()
|
||||||
|
|
||||||
return MXOutboundSessionInfo(sessionId)
|
return MXOutboundSessionInfo(sessionId, SharedWithHelper(roomId, sessionId, cryptoStore))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -121,7 +145,7 @@ internal class MXMegolmEncryption(
|
|||||||
val deviceIds = devicesInRoom.getUserDeviceIds(userId)
|
val deviceIds = devicesInRoom.getUserDeviceIds(userId)
|
||||||
for (deviceId in deviceIds!!) {
|
for (deviceId in deviceIds!!) {
|
||||||
val deviceInfo = devicesInRoom.getObject(userId, deviceId)
|
val deviceInfo = devicesInRoom.getObject(userId, deviceId)
|
||||||
if (deviceInfo != null && null == safeSession.sharedWithDevices.getObject(userId, deviceId)) {
|
if (deviceInfo != null && !cryptoStore.wasSessionSharedWithUser(roomId, safeSession.sessionId, userId, deviceId).found) {
|
||||||
val devices = shareMap.getOrPut(userId) { ArrayList() }
|
val devices = shareMap.getOrPut(userId) { ArrayList() }
|
||||||
devices.add(deviceInfo)
|
devices.add(deviceInfo)
|
||||||
}
|
}
|
||||||
@ -198,15 +222,17 @@ internal class MXMegolmEncryption(
|
|||||||
if (sessionResult?.sessionId == null) {
|
if (sessionResult?.sessionId == null) {
|
||||||
// no session with this device, probably because there
|
// no session with this device, probably because there
|
||||||
// were no one-time keys.
|
// were no one-time keys.
|
||||||
//
|
|
||||||
// we could send them a to_device message anyway, as a
|
// MSC 2399
|
||||||
// signal that they have missed out on the key sharing
|
// send withheld m.no_olm: an olm session could not be established.
|
||||||
// message because of the lack of keys, but there's not
|
// This may happen, for example, if the sender was unable to obtain a one-time key from the recipient.
|
||||||
// much point in that really; it will mostly serve to clog
|
notifyKeyWithHeld(
|
||||||
// up to_device inboxes.
|
listOf(UserDevice(userId, deviceID)),
|
||||||
//
|
session.sessionId,
|
||||||
// ensureOlmSessionsForUsers has already done the logging,
|
olmDevice.deviceCurve25519Key,
|
||||||
// so just skip it.
|
WithHeldCode.NO_OLM
|
||||||
|
)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
Timber.v("## CRYPTO | shareUserDevicesKey() : Sharing keys with device $userId:$deviceID")
|
Timber.v("## CRYPTO | shareUserDevicesKey() : Sharing keys with device $userId:$deviceID")
|
||||||
@ -214,29 +240,59 @@ internal class MXMegolmEncryption(
|
|||||||
haveTargets = true
|
haveTargets = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add the devices we have shared with to session.sharedWithDevices.
|
||||||
|
// we deliberately iterate over devicesByUser (ie, the devices we
|
||||||
|
// attempted to share with) rather than the contentMap (those we did
|
||||||
|
// share with), because we don't want to try to claim a one-time-key
|
||||||
|
// for dead devices on every message.
|
||||||
|
for ((userId, devicesToShareWith) in devicesByUser) {
|
||||||
|
for ((deviceId) in devicesToShareWith) {
|
||||||
|
session.sharedWithHelper.markedSessionAsShared(userId, deviceId, chainIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (haveTargets) {
|
if (haveTargets) {
|
||||||
t0 = System.currentTimeMillis()
|
t0 = System.currentTimeMillis()
|
||||||
Timber.v("## CRYPTO | shareUserDevicesKey() : has target")
|
Timber.v("## CRYPTO | shareUserDevicesKey() : has target")
|
||||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap)
|
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap)
|
||||||
sendToDeviceTask.execute(sendToDeviceParams)
|
try {
|
||||||
Timber.v("## CRYPTO | shareUserDevicesKey() : sendToDevice succeeds after "
|
sendToDeviceTask.execute(sendToDeviceParams)
|
||||||
+ (System.currentTimeMillis() - t0) + " ms")
|
Timber.v("## CRYPTO | shareUserDevicesKey() : sendToDevice succeeds after ${System.currentTimeMillis() - t0} ms")
|
||||||
|
} catch (failure: Throwable) {
|
||||||
// Add the devices we have shared with to session.sharedWithDevices.
|
// What to do here...
|
||||||
// we deliberately iterate over devicesByUser (ie, the devices we
|
Timber.e("## CRYPTO | shareUserDevicesKey() : Failed to share session <${session.sessionId}> with $devicesByUser ")
|
||||||
// attempted to share with) rather than the contentMap (those we did
|
|
||||||
// share with), because we don't want to try to claim a one-time-key
|
|
||||||
// for dead devices on every message.
|
|
||||||
for ((userId, devicesToShareWith) in devicesByUser) {
|
|
||||||
for ((deviceId) in devicesToShareWith) {
|
|
||||||
session.sharedWithDevices.setObject(userId, deviceId, chainIndex)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Timber.v("## CRYPTO | shareUserDevicesKey() : no need to sharekey")
|
Timber.v("## CRYPTO | shareUserDevicesKey() : no need to sharekey")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun notifyKeyWithHeld(targets: List<UserDevice>, sessionId: String, senderKey: String?, code: WithHeldCode) {
|
||||||
|
val withHeldContent = RoomKeyWithHeldContent(
|
||||||
|
roomId = roomId,
|
||||||
|
senderKey = senderKey,
|
||||||
|
algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
|
||||||
|
sessionId = sessionId,
|
||||||
|
codeString = code.value
|
||||||
|
)
|
||||||
|
val params = SendToDeviceTask.Params(
|
||||||
|
EventType.ROOM_KEY_WITHHELD,
|
||||||
|
MXUsersDevicesMap<Any>().apply {
|
||||||
|
targets.forEach {
|
||||||
|
setObject(it.userId, it.deviceId, withHeldContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
sendToDeviceTask.configureWith(params) {
|
||||||
|
callback = object : MatrixCallback<Unit> {
|
||||||
|
override fun onFailure(failure: Throwable) {
|
||||||
|
Timber.e("## CRYPTO | notifyKeyWithHeld() : Failed to notify withheld key for $targets session: $sessionId ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.executeBy(taskExecutor)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* process the pending encryptions
|
* process the pending encryptions
|
||||||
*/
|
*/
|
||||||
@ -271,7 +327,7 @@ internal class MXMegolmEncryption(
|
|||||||
*
|
*
|
||||||
* @param userIds the user ids whose devices must be checked.
|
* @param userIds the user ids whose devices must be checked.
|
||||||
*/
|
*/
|
||||||
private suspend fun getDevicesInRoom(userIds: List<String>): MXUsersDevicesMap<CryptoDeviceInfo> {
|
private suspend fun getDevicesInRoom(userIds: List<String>): DeviceInRoomInfo {
|
||||||
// We are happy to use a cached version here: we assume that if we already
|
// We are happy to use a cached version here: we assume that if we already
|
||||||
// have a list of the user's devices, then we already share an e2e room
|
// have a list of the user's devices, then we already share an e2e room
|
||||||
// with them, which means that they will have announced any new devices via
|
// with them, which means that they will have announced any new devices via
|
||||||
@ -280,7 +336,7 @@ internal class MXMegolmEncryption(
|
|||||||
val encryptToVerifiedDevicesOnly = cryptoStore.getGlobalBlacklistUnverifiedDevices()
|
val encryptToVerifiedDevicesOnly = cryptoStore.getGlobalBlacklistUnverifiedDevices()
|
||||||
|| cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(roomId)
|
|| cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(roomId)
|
||||||
|
|
||||||
val devicesInRoom = MXUsersDevicesMap<CryptoDeviceInfo>()
|
val devicesInRoom = DeviceInRoomInfo()
|
||||||
val unknownDevices = MXUsersDevicesMap<CryptoDeviceInfo>()
|
val unknownDevices = MXUsersDevicesMap<CryptoDeviceInfo>()
|
||||||
|
|
||||||
for (userId in keys.userIds) {
|
for (userId in keys.userIds) {
|
||||||
@ -294,10 +350,12 @@ internal class MXMegolmEncryption(
|
|||||||
}
|
}
|
||||||
if (deviceInfo.isBlocked) {
|
if (deviceInfo.isBlocked) {
|
||||||
// Remove any blocked devices
|
// Remove any blocked devices
|
||||||
|
devicesInRoom.withHeldDevices.setObject(userId, deviceId, WithHeldCode.BLACKLISTED)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!deviceInfo.isVerified && encryptToVerifiedDevicesOnly) {
|
if (!deviceInfo.isVerified && encryptToVerifiedDevicesOnly) {
|
||||||
|
devicesInRoom.withHeldDevices.setObject(userId, deviceId, WithHeldCode.UNVERIFIED)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -305,7 +363,7 @@ internal class MXMegolmEncryption(
|
|||||||
// Don't bother sending to ourself
|
// Don't bother sending to ourself
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
devicesInRoom.setObject(userId, deviceId, deviceInfo)
|
devicesInRoom.allowedDevices.setObject(userId, deviceId, deviceInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (unknownDevices.isEmpty) {
|
if (unknownDevices.isEmpty) {
|
||||||
@ -324,8 +382,12 @@ internal class MXMegolmEncryption(
|
|||||||
.also { Timber.w("Device not found") }
|
.also { Timber.w("Device not found") }
|
||||||
|
|
||||||
// Get the chain index of the key we previously sent this device
|
// Get the chain index of the key we previously sent this device
|
||||||
val chainIndex = outboundSession?.sharedWithDevices?.getObject(userId, deviceId)?.toLong() ?: return false
|
val chainIndex = outboundSession?.sharedWithHelper?.wasSharedWith(userId, deviceId) ?: return false
|
||||||
.also { Timber.w("[MXMegolmEncryption] reshareKey : ERROR : Never share megolm with this device") }
|
.also {
|
||||||
|
// Send a room key with held
|
||||||
|
notifyKeyWithHeld(listOf(UserDevice(userId, deviceId)), sessionId, senderKey, WithHeldCode.UNAUTHORISED)
|
||||||
|
Timber.w("[MXMegolmEncryption] reshareKey : ERROR : Never share megolm with this device")
|
||||||
|
}
|
||||||
|
|
||||||
val devicesByUser = mapOf(userId to listOf(deviceInfo))
|
val devicesByUser = mapOf(userId to listOf(deviceInfo))
|
||||||
val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
||||||
@ -343,7 +405,7 @@ internal class MXMegolmEncryption(
|
|||||||
.fold(
|
.fold(
|
||||||
{
|
{
|
||||||
// TODO
|
// TODO
|
||||||
payloadJson["content"] = it.exportKeys(chainIndex) ?: ""
|
payloadJson["content"] = it.exportKeys(chainIndex.toLong()) ?: ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// TODO
|
// TODO
|
||||||
@ -354,9 +416,24 @@ internal class MXMegolmEncryption(
|
|||||||
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
||||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
||||||
sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
|
sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
|
||||||
Timber.v("## CRYPTO | CRYPTO | shareKeysWithDevice() : sending to $userId:$deviceId")
|
Timber.v("## CRYPTO | CRYPTO | reshareKey() : sending to $userId:$deviceId")
|
||||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
||||||
sendToDeviceTask.execute(sendToDeviceParams)
|
return try {
|
||||||
return true
|
sendToDeviceTask.execute(sendToDeviceParams)
|
||||||
|
true
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
Timber.v("## CRYPTO | CRYPTO | reshareKey() : fail to send <$sessionId> to $userId:$deviceId")
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class DeviceInRoomInfo(
|
||||||
|
val allowedDevices: MXUsersDevicesMap<CryptoDeviceInfo> = MXUsersDevicesMap(),
|
||||||
|
val withHeldDevices: MXUsersDevicesMap<WithHeldCode> = MXUsersDevicesMap()
|
||||||
|
)
|
||||||
|
|
||||||
|
data class UserDevice(
|
||||||
|
val userId: String,
|
||||||
|
val deviceId: String
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ import im.vector.matrix.android.internal.crypto.keysbackup.DefaultKeysBackupServ
|
|||||||
import im.vector.matrix.android.internal.crypto.repository.WarnOnUnknownDeviceRepository
|
import im.vector.matrix.android.internal.crypto.repository.WarnOnUnknownDeviceRepository
|
||||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||||
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class MXMegolmEncryptionFactory @Inject constructor(
|
internal class MXMegolmEncryptionFactory @Inject constructor(
|
||||||
@ -36,7 +37,8 @@ internal class MXMegolmEncryptionFactory @Inject constructor(
|
|||||||
private val credentials: Credentials,
|
private val credentials: Credentials,
|
||||||
private val sendToDeviceTask: SendToDeviceTask,
|
private val sendToDeviceTask: SendToDeviceTask,
|
||||||
private val messageEncrypter: MessageEncrypter,
|
private val messageEncrypter: MessageEncrypter,
|
||||||
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository) {
|
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
|
||||||
|
private val taskExecutor: TaskExecutor) {
|
||||||
|
|
||||||
fun create(roomId: String): MXMegolmEncryption {
|
fun create(roomId: String): MXMegolmEncryption {
|
||||||
return MXMegolmEncryption(
|
return MXMegolmEncryption(
|
||||||
@ -49,6 +51,8 @@ internal class MXMegolmEncryptionFactory @Inject constructor(
|
|||||||
credentials,
|
credentials,
|
||||||
sendToDeviceTask,
|
sendToDeviceTask,
|
||||||
messageEncrypter,
|
messageEncrypter,
|
||||||
warnOnUnknownDevicesRepository)
|
warnOnUnknownDevicesRepository,
|
||||||
|
taskExecutor
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,17 +23,14 @@ import timber.log.Timber
|
|||||||
|
|
||||||
internal class MXOutboundSessionInfo(
|
internal class MXOutboundSessionInfo(
|
||||||
// The id of the session
|
// The id of the session
|
||||||
val sessionId: String) {
|
val sessionId: String,
|
||||||
|
val sharedWithHelper: SharedWithHelper) {
|
||||||
// When the session was created
|
// When the session was created
|
||||||
private val creationTime = System.currentTimeMillis()
|
private val creationTime = System.currentTimeMillis()
|
||||||
|
|
||||||
// Number of times this session has been used
|
// Number of times this session has been used
|
||||||
var useCount: Int = 0
|
var useCount: Int = 0
|
||||||
|
|
||||||
// Devices with which we have shared the session key
|
|
||||||
// userId -> {deviceId -> msgindex}
|
|
||||||
val sharedWithDevices: MXUsersDevicesMap<Int> = MXUsersDevicesMap()
|
|
||||||
|
|
||||||
fun needsRotation(rotationPeriodMsgs: Int, rotationPeriodMs: Int): Boolean {
|
fun needsRotation(rotationPeriodMsgs: Int, rotationPeriodMs: Int): Boolean {
|
||||||
var needsRotation = false
|
var needsRotation = false
|
||||||
val sessionLifetime = System.currentTimeMillis() - creationTime
|
val sessionLifetime = System.currentTimeMillis() - creationTime
|
||||||
@ -53,6 +50,7 @@ internal class MXOutboundSessionInfo(
|
|||||||
* @return true if we have shared the session with devices which aren't in devicesInRoom.
|
* @return true if we have shared the session with devices which aren't in devicesInRoom.
|
||||||
*/
|
*/
|
||||||
fun sharedWithTooManyDevices(devicesInRoom: MXUsersDevicesMap<CryptoDeviceInfo>): Boolean {
|
fun sharedWithTooManyDevices(devicesInRoom: MXUsersDevicesMap<CryptoDeviceInfo>): Boolean {
|
||||||
|
val sharedWithDevices = sharedWithHelper.sharedWithDevices()
|
||||||
val userIds = sharedWithDevices.userIds
|
val userIds = sharedWithDevices.userIds
|
||||||
|
|
||||||
for (userId in userIds) {
|
for (userId in userIds) {
|
||||||
|
@ -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.internal.crypto.algorithms.megolm
|
||||||
|
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||||
|
|
||||||
|
internal class SharedWithHelper(
|
||||||
|
private val roomId: String,
|
||||||
|
private val sessionId: String,
|
||||||
|
private val cryptoStore: IMXCryptoStore) {
|
||||||
|
|
||||||
|
fun sharedWithDevices(): MXUsersDevicesMap<Int> {
|
||||||
|
return cryptoStore.getSharedWithInfo(roomId, sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun wasSharedWith(userId: String, deviceId: String): Int? {
|
||||||
|
return cryptoStore.wasSessionSharedWithUser(roomId, sessionId, userId, deviceId).chainIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
fun markedSessionAsShared(userId: String, deviceId: String, chainIndex: Int) {
|
||||||
|
cryptoStore.markedSessionAsShared(roomId, sessionId, userId, deviceId, chainIndex)
|
||||||
|
}
|
||||||
|
}
|
@ -212,7 +212,7 @@ internal class MXOlmDecryption(
|
|||||||
return res["payload"]
|
return res["payload"]
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun requestKeysForEvent(event: Event) {
|
override fun requestKeysForEvent(event: Event, withHeld: Boolean) {
|
||||||
// nop
|
// nop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,3 +119,13 @@ class MXUsersDevicesMap<E> {
|
|||||||
return "MXUsersDevicesMap $map"
|
return "MXUsersDevicesMap $map"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline fun <T> MXUsersDevicesMap<T>.forEach(action: (String, String, T) -> Unit) {
|
||||||
|
userIds.forEach { userId ->
|
||||||
|
getUserDeviceIds(userId)?.forEach { deviceId ->
|
||||||
|
getObject(userId, deviceId)?.let {
|
||||||
|
action(userId, deviceId, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package im.vector.matrix.android.internal.crypto.model.event
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing an sharekey content
|
||||||
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class RoomKeyWithHeldContent(
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required if code is not m.no_olm. The ID of the room that the session belongs to.
|
||||||
|
*/
|
||||||
|
@Json(name = "room_id") val roomId: String? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required. The encryption algorithm that the key is for.
|
||||||
|
*/
|
||||||
|
@Json(name = "algorithm") val algorithm: String? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required if code is not m.no_olm. The ID of the session.
|
||||||
|
*/
|
||||||
|
@Json(name = "session_id") val sessionId: String? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required. The key of the session creator.
|
||||||
|
*/
|
||||||
|
@Json(name = "sender_key") val senderKey: String? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required. A machine-readable code for why the key was not sent
|
||||||
|
*/
|
||||||
|
@Json(name = "code") val codeString: String? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A human-readable reason for why the key was not sent. The receiving client should only use this string if it does not understand the code.
|
||||||
|
*/
|
||||||
|
@Json(name = "reason") val reason: String? = null
|
||||||
|
|
||||||
|
) {
|
||||||
|
val code: WithHeldCode?
|
||||||
|
get() {
|
||||||
|
return WithHeldCode.fromCode(codeString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class WithHeldCode(val value: String) {
|
||||||
|
/**
|
||||||
|
* the user/device was blacklisted
|
||||||
|
*/
|
||||||
|
BLACKLISTED("m.blacklisted"),
|
||||||
|
/**
|
||||||
|
* the user/devices is unverified
|
||||||
|
*/
|
||||||
|
UNVERIFIED("m.unverified"),
|
||||||
|
/**
|
||||||
|
* the user/device is not allowed have the key. For example, this would usually be sent in response
|
||||||
|
* to a key request if the user was not in the room when the message was sent
|
||||||
|
*/
|
||||||
|
UNAUTHORISED("m.unauthorised"),
|
||||||
|
/**
|
||||||
|
* Sent in reply to a key request if the device that the key is requested from does not have the requested key
|
||||||
|
*/
|
||||||
|
UNAVAILABLE("m.unavailable"),
|
||||||
|
/**
|
||||||
|
* An olm session could not be established.
|
||||||
|
* This may happen, for example, if the sender was unable to obtain a one-time key from the recipient.
|
||||||
|
*/
|
||||||
|
NO_OLM("m.no_olm");
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromCode(code: String?): WithHeldCode? {
|
||||||
|
return when (code) {
|
||||||
|
BLACKLISTED.value -> BLACKLISTED
|
||||||
|
UNVERIFIED.value -> UNVERIFIED
|
||||||
|
UNAUTHORISED.value -> UNAUTHORISED
|
||||||
|
UNAVAILABLE.value -> UNAVAILABLE
|
||||||
|
NO_OLM.value -> NO_OLM
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2016 OpenMarket Ltd
|
* Copyright 2016 OpenMarket Ltd
|
||||||
* Copyright 2018 New Vector Ltd
|
* Copyright 2018 New Vector Ltd
|
||||||
@ -30,8 +31,10 @@ import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
|
|||||||
import im.vector.matrix.android.internal.crypto.OutgoingSecretRequest
|
import im.vector.matrix.android.internal.crypto.OutgoingSecretRequest
|
||||||
import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
|
import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
|
||||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||||
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
||||||
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
|
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldContent
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
|
||||||
@ -416,6 +419,13 @@ internal interface IMXCryptoStore {
|
|||||||
|
|
||||||
fun updateUsersTrust(check: (String) -> Boolean)
|
fun updateUsersTrust(check: (String) -> Boolean)
|
||||||
|
|
||||||
|
fun addWithHeldMegolmSession(withHeldContent: RoomKeyWithHeldContent)
|
||||||
|
fun getWithHeldMegolmSession(roomId: String, sessionId: String) : RoomKeyWithHeldContent?
|
||||||
|
|
||||||
|
fun markedSessionAsShared(roomId: String?, sessionId: String, userId: String, deviceId: String, chainIndex: Int)
|
||||||
|
fun wasSessionSharedWithUser(roomId: String?, sessionId: String, userId: String, deviceId: String) : SharedSessionResult
|
||||||
|
data class SharedSessionResult(val found: Boolean, val chainIndex: Int?)
|
||||||
|
fun getSharedWithInfo(roomId: String?, sessionId: String) : MXUsersDevicesMap<Int>
|
||||||
// Dev tools
|
// Dev tools
|
||||||
|
|
||||||
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
|
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
|
||||||
|
@ -31,6 +31,7 @@ import im.vector.matrix.android.internal.crypto.GossipingRequestState
|
|||||||
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
||||||
import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest
|
import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest
|
||||||
import im.vector.matrix.android.internal.crypto.IncomingShareRequestCommon
|
import im.vector.matrix.android.internal.crypto.IncomingShareRequestCommon
|
||||||
|
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
import im.vector.matrix.android.internal.crypto.NewSessionListener
|
import im.vector.matrix.android.internal.crypto.NewSessionListener
|
||||||
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestState
|
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestState
|
||||||
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
|
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
|
||||||
@ -38,8 +39,10 @@ import im.vector.matrix.android.internal.crypto.OutgoingSecretRequest
|
|||||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||||
import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
|
import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
|
||||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||||
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
||||||
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
|
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldContent
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||||
import im.vector.matrix.android.internal.crypto.model.toEntity
|
import im.vector.matrix.android.internal.crypto.model.toEntity
|
||||||
@ -66,10 +69,13 @@ import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity
|
|||||||
import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntityFields
|
import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntityFields
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields
|
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.db.model.SharedSessionEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields
|
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.db.model.WithHeldSessionEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.createPrimaryKey
|
import im.vector.matrix.android.internal.crypto.store.db.model.createPrimaryKey
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.db.query.create
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.query.delete
|
import im.vector.matrix.android.internal.crypto.store.db.query.delete
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.query.get
|
import im.vector.matrix.android.internal.crypto.store.db.query.get
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.query.getById
|
import im.vector.matrix.android.internal.crypto.store.db.query.getById
|
||||||
@ -1427,4 +1433,68 @@ internal class RealmCryptoStore @Inject constructor(
|
|||||||
return existing
|
return existing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun addWithHeldMegolmSession(withHeldContent: RoomKeyWithHeldContent) {
|
||||||
|
val roomId = withHeldContent.roomId ?: return
|
||||||
|
val sessionId = withHeldContent.sessionId ?: return
|
||||||
|
if (withHeldContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return
|
||||||
|
doRealmTransaction(realmConfiguration) { realm ->
|
||||||
|
WithHeldSessionEntity.getOrCreate(realm, roomId, sessionId)?.let {
|
||||||
|
it.code = withHeldContent.code
|
||||||
|
it.senderKey = withHeldContent.senderKey
|
||||||
|
it.reason = withHeldContent.reason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent? {
|
||||||
|
return doWithRealm(realmConfiguration) { realm ->
|
||||||
|
WithHeldSessionEntity.get(realm, roomId, sessionId)?.let {
|
||||||
|
RoomKeyWithHeldContent(
|
||||||
|
roomId = roomId,
|
||||||
|
sessionId = sessionId,
|
||||||
|
algorithm = it.algorithm,
|
||||||
|
codeString = it.codeString,
|
||||||
|
reason = it.reason,
|
||||||
|
senderKey = it.senderKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun markedSessionAsShared(roomId: String?, sessionId: String, userId: String, deviceId: String, chainIndex: Int) {
|
||||||
|
doRealmTransaction(realmConfiguration) { realm ->
|
||||||
|
SharedSessionEntity.create(
|
||||||
|
realm = realm,
|
||||||
|
roomId = roomId,
|
||||||
|
sessionId = sessionId,
|
||||||
|
userId = userId,
|
||||||
|
deviceId = deviceId,
|
||||||
|
chainIndex = chainIndex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun wasSessionSharedWithUser(roomId: String?, sessionId: String, userId: String, deviceId: String): IMXCryptoStore.SharedSessionResult {
|
||||||
|
return doWithRealm(realmConfiguration) { realm ->
|
||||||
|
SharedSessionEntity.get(realm, roomId, sessionId, userId, deviceId)?.let {
|
||||||
|
IMXCryptoStore.SharedSessionResult(true, it.chainIndex)
|
||||||
|
} ?: IMXCryptoStore.SharedSessionResult(false, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int> {
|
||||||
|
return doWithRealm(realmConfiguration) { realm ->
|
||||||
|
val result = MXUsersDevicesMap<Int>()
|
||||||
|
SharedSessionEntity.get(realm, roomId, sessionId)
|
||||||
|
.groupBy { it.userId }
|
||||||
|
.forEach { (userId, shared) ->
|
||||||
|
shared.forEach {
|
||||||
|
result.setObject(userId, it.deviceId, it.chainIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,8 +36,10 @@ import im.vector.matrix.android.internal.crypto.store.db.model.MyDeviceLastSeenI
|
|||||||
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields
|
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntityFields
|
import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntityFields
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields
|
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.db.model.SharedSessionEntityFields
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntityFields
|
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntityFields
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields
|
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.db.model.WithHeldSessionEntityFields
|
||||||
import im.vector.matrix.android.internal.di.SerializeNulls
|
import im.vector.matrix.android.internal.di.SerializeNulls
|
||||||
import io.realm.DynamicRealm
|
import io.realm.DynamicRealm
|
||||||
import io.realm.RealmMigration
|
import io.realm.RealmMigration
|
||||||
@ -52,7 +54,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
|||||||
// 0, 1, 2: legacy Riot-Android
|
// 0, 1, 2: legacy Riot-Android
|
||||||
// 3: migrate to RiotX schema
|
// 3: migrate to RiotX schema
|
||||||
// 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6)
|
// 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6)
|
||||||
const val CRYPTO_STORE_SCHEMA_VERSION = 9L
|
const val CRYPTO_STORE_SCHEMA_VERSION = 10L
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||||
@ -67,6 +69,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
|||||||
if (oldVersion <= 6) migrateTo7(realm)
|
if (oldVersion <= 6) migrateTo7(realm)
|
||||||
if (oldVersion <= 7) migrateTo8(realm)
|
if (oldVersion <= 7) migrateTo8(realm)
|
||||||
if (oldVersion <= 8) migrateTo9(realm)
|
if (oldVersion <= 8) migrateTo9(realm)
|
||||||
|
if (oldVersion <= 9) migrateTo10(realm)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun migrateTo1Legacy(realm: DynamicRealm) {
|
private fun migrateTo1Legacy(realm: DynamicRealm) {
|
||||||
@ -416,4 +419,30 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Version 10L added WithHeld Keys Info (MSC2399)
|
||||||
|
private fun migrateTo10(realm: DynamicRealm) {
|
||||||
|
Timber.d("Step 9 -> 10")
|
||||||
|
realm.schema.create("WithHeldSessionEntity")
|
||||||
|
.addField(WithHeldSessionEntityFields.ROOM_ID, String::class.java)
|
||||||
|
.addField(WithHeldSessionEntityFields.ALGORITHM, String::class.java)
|
||||||
|
.addField(WithHeldSessionEntityFields.SESSION_ID, String::class.java)
|
||||||
|
.addIndex(WithHeldSessionEntityFields.SESSION_ID)
|
||||||
|
.addField(WithHeldSessionEntityFields.SENDER_KEY, String::class.java)
|
||||||
|
.addIndex(WithHeldSessionEntityFields.SENDER_KEY)
|
||||||
|
.addField(WithHeldSessionEntityFields.CODE_STRING, String::class.java)
|
||||||
|
.addField(WithHeldSessionEntityFields.REASON, String::class.java)
|
||||||
|
|
||||||
|
realm.schema.create("SharedSessionEntity")
|
||||||
|
.addField(SharedSessionEntityFields.ROOM_ID, String::class.java)
|
||||||
|
.addField(SharedSessionEntityFields.ALGORITHM, String::class.java)
|
||||||
|
.addField(SharedSessionEntityFields.SESSION_ID, String::class.java)
|
||||||
|
.addIndex(SharedSessionEntityFields.SESSION_ID)
|
||||||
|
.addField(SharedSessionEntityFields.USER_ID, String::class.java)
|
||||||
|
.addIndex(SharedSessionEntityFields.USER_ID)
|
||||||
|
.addField(SharedSessionEntityFields.DEVICE_ID, String::class.java)
|
||||||
|
.addIndex(SharedSessionEntityFields.DEVICE_ID)
|
||||||
|
.addField(SharedSessionEntityFields.CHAIN_INDEX, Long::class.java)
|
||||||
|
.setNullable(SharedSessionEntityFields.CHAIN_INDEX, true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,8 +28,10 @@ import im.vector.matrix.android.internal.crypto.store.db.model.MyDeviceLastSeenI
|
|||||||
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntity
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.db.model.SharedSessionEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.db.model.WithHeldSessionEntity
|
||||||
import io.realm.annotations.RealmModule
|
import io.realm.annotations.RealmModule
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -50,6 +52,8 @@ import io.realm.annotations.RealmModule
|
|||||||
GossipingEventEntity::class,
|
GossipingEventEntity::class,
|
||||||
IncomingGossipingRequestEntity::class,
|
IncomingGossipingRequestEntity::class,
|
||||||
OutgoingGossipingRequestEntity::class,
|
OutgoingGossipingRequestEntity::class,
|
||||||
MyDeviceLastSeenInfoEntity::class
|
MyDeviceLastSeenInfoEntity::class,
|
||||||
|
WithHeldSessionEntity::class,
|
||||||
|
SharedSessionEntity::class
|
||||||
])
|
])
|
||||||
internal class RealmCryptoStoreModule
|
internal class RealmCryptoStoreModule
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.crypto.store.db.model
|
||||||
|
|
||||||
|
import io.realm.RealmObject
|
||||||
|
import io.realm.annotations.Index
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep a record of to whom (user/device) a given session should have been shared.
|
||||||
|
* It will be used to reply to keyshare requests from other users, in order to see if
|
||||||
|
* this session was originaly shared with a given user
|
||||||
|
*/
|
||||||
|
internal open class SharedSessionEntity(
|
||||||
|
var roomId: String? = null,
|
||||||
|
var algorithm: String? = null,
|
||||||
|
@Index var sessionId: String? = null,
|
||||||
|
@Index var userId: String? = null,
|
||||||
|
@Index var deviceId: String? = null,
|
||||||
|
var chainIndex: Int? = null
|
||||||
|
) : RealmObject() {
|
||||||
|
|
||||||
|
companion object
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.crypto.store.db.model
|
||||||
|
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode
|
||||||
|
import io.realm.RealmObject
|
||||||
|
import io.realm.annotations.Index
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When an encrypted message is sent in a room, the megolm key might not be sent to all devices present in the room.
|
||||||
|
* Sometimes this may be inadvertent (for example, if the sending device is not aware of some devices that have joined),
|
||||||
|
* but some times, this may be purposeful.
|
||||||
|
* For example, the sender may have blacklisted certain devices or users,
|
||||||
|
* or may be choosing to not send the megolm key to devices that they have not verified yet.
|
||||||
|
*/
|
||||||
|
internal open class WithHeldSessionEntity(
|
||||||
|
var roomId: String? = null,
|
||||||
|
var algorithm: String? = null,
|
||||||
|
@Index var sessionId: String? = null,
|
||||||
|
@Index var senderKey: String? = null,
|
||||||
|
var codeString: String? = null,
|
||||||
|
var reason: String? = null
|
||||||
|
) : RealmObject() {
|
||||||
|
|
||||||
|
var code: WithHeldCode?
|
||||||
|
get() {
|
||||||
|
return WithHeldCode.fromCode(codeString)
|
||||||
|
}
|
||||||
|
set(code) {
|
||||||
|
codeString = code?.value
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.crypto.store.db.query
|
||||||
|
|
||||||
|
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.db.model.SharedSessionEntity
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.db.model.SharedSessionEntityFields
|
||||||
|
import io.realm.Realm
|
||||||
|
import io.realm.RealmResults
|
||||||
|
import io.realm.kotlin.createObject
|
||||||
|
import io.realm.kotlin.where
|
||||||
|
|
||||||
|
internal fun SharedSessionEntity.Companion.get(realm: Realm, roomId: String?, sessionId: String, userId: String, deviceId: String)
|
||||||
|
: SharedSessionEntity? {
|
||||||
|
return realm.where<SharedSessionEntity>()
|
||||||
|
.equalTo(SharedSessionEntityFields.ROOM_ID, roomId)
|
||||||
|
.equalTo(SharedSessionEntityFields.SESSION_ID, sessionId)
|
||||||
|
.equalTo(SharedSessionEntityFields.ALGORITHM, MXCRYPTO_ALGORITHM_MEGOLM)
|
||||||
|
.equalTo(SharedSessionEntityFields.USER_ID, userId)
|
||||||
|
.equalTo(SharedSessionEntityFields.DEVICE_ID, deviceId)
|
||||||
|
.findFirst()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun SharedSessionEntity.Companion.get(realm: Realm, roomId: String?, sessionId: String)
|
||||||
|
: RealmResults<SharedSessionEntity> {
|
||||||
|
return realm.where<SharedSessionEntity>()
|
||||||
|
.equalTo(SharedSessionEntityFields.ROOM_ID, roomId)
|
||||||
|
.equalTo(SharedSessionEntityFields.SESSION_ID, sessionId)
|
||||||
|
.equalTo(SharedSessionEntityFields.ALGORITHM, MXCRYPTO_ALGORITHM_MEGOLM)
|
||||||
|
.findAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun SharedSessionEntity.Companion.create(realm: Realm, roomId: String?, sessionId: String, userId: String, deviceId: String, chainIndex: Int)
|
||||||
|
: SharedSessionEntity {
|
||||||
|
return realm.createObject<SharedSessionEntity>().apply {
|
||||||
|
this.roomId = roomId
|
||||||
|
this.algorithm = MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
|
this.sessionId = sessionId
|
||||||
|
this.userId = userId
|
||||||
|
this.deviceId = deviceId
|
||||||
|
this.chainIndex = chainIndex
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.crypto.store.db.query
|
||||||
|
|
||||||
|
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.db.model.WithHeldSessionEntity
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.db.model.WithHeldSessionEntityFields
|
||||||
|
import io.realm.Realm
|
||||||
|
import io.realm.kotlin.createObject
|
||||||
|
import io.realm.kotlin.where
|
||||||
|
|
||||||
|
internal fun WithHeldSessionEntity.Companion.get(realm: Realm, roomId: String, sessionId: String): WithHeldSessionEntity? {
|
||||||
|
return realm.where<WithHeldSessionEntity>()
|
||||||
|
.equalTo(WithHeldSessionEntityFields.ROOM_ID, roomId)
|
||||||
|
.equalTo(WithHeldSessionEntityFields.SESSION_ID, sessionId)
|
||||||
|
.equalTo(WithHeldSessionEntityFields.ALGORITHM, MXCRYPTO_ALGORITHM_MEGOLM)
|
||||||
|
.findFirst()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun WithHeldSessionEntity.Companion.getOrCreate(realm: Realm, roomId: String, sessionId: String): WithHeldSessionEntity? {
|
||||||
|
return get(realm, roomId, sessionId)
|
||||||
|
?: realm.createObject<WithHeldSessionEntity>().apply {
|
||||||
|
this.roomId = roomId
|
||||||
|
this.algorithm = MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
|
this.sessionId = sessionId
|
||||||
|
}
|
||||||
|
}
|
@ -45,6 +45,12 @@ internal object EventMapper {
|
|||||||
eventEntity.redacts = event.redacts
|
eventEntity.redacts = event.redacts
|
||||||
eventEntity.age = event.unsignedData?.age ?: event.originServerTs
|
eventEntity.age = event.unsignedData?.age ?: event.originServerTs
|
||||||
eventEntity.unsignedData = uds
|
eventEntity.unsignedData = uds
|
||||||
|
|
||||||
|
eventEntity.decryptionResultJson = event.mxDecryptionResult?.let {
|
||||||
|
MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java).toJson(it)
|
||||||
|
}
|
||||||
|
eventEntity.decryptionErrorReason = event.mCryptoErrorReason
|
||||||
|
eventEntity.decryptionErrorCode = event.mCryptoError?.name
|
||||||
return eventEntity
|
return eventEntity
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,6 +91,7 @@ internal object EventMapper {
|
|||||||
it.mCryptoError = eventEntity.decryptionErrorCode?.let { errorCode ->
|
it.mCryptoError = eventEntity.decryptionErrorCode?.let { errorCode ->
|
||||||
MXCryptoError.ErrorType.valueOf(errorCode)
|
MXCryptoError.ErrorType.valueOf(errorCode)
|
||||||
}
|
}
|
||||||
|
it.mCryptoErrorReason = eventEntity.decryptionErrorReason
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,9 +19,11 @@ package im.vector.matrix.android.internal.database.mapper
|
|||||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||||
import im.vector.matrix.android.api.session.room.model.tag.RoomTag
|
import im.vector.matrix.android.api.session.room.model.tag.RoomTag
|
||||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||||
|
import im.vector.matrix.android.internal.session.typing.DefaultTypingUsersTracker
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class RoomSummaryMapper @Inject constructor(private val timelineEventMapper: TimelineEventMapper) {
|
internal class RoomSummaryMapper @Inject constructor(private val timelineEventMapper: TimelineEventMapper,
|
||||||
|
private val typingUsersTracker: DefaultTypingUsersTracker) {
|
||||||
|
|
||||||
fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary {
|
fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary {
|
||||||
val tags = roomSummaryEntity.tags.map {
|
val tags = roomSummaryEntity.tags.map {
|
||||||
@ -31,10 +33,13 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
|
|||||||
val latestEvent = roomSummaryEntity.latestPreviewableEvent?.let {
|
val latestEvent = roomSummaryEntity.latestPreviewableEvent?.let {
|
||||||
timelineEventMapper.map(it, buildReadReceipts = false)
|
timelineEventMapper.map(it, buildReadReceipts = false)
|
||||||
}
|
}
|
||||||
|
// typings are updated through the sync where room summary entity gets updated no matter what, so it's ok get there
|
||||||
|
val typingUsers = typingUsersTracker.getTypingUsers(roomSummaryEntity.roomId)
|
||||||
|
|
||||||
return RoomSummary(
|
return RoomSummary(
|
||||||
roomId = roomSummaryEntity.roomId,
|
roomId = roomSummaryEntity.roomId,
|
||||||
displayName = roomSummaryEntity.displayName ?: "",
|
displayName = roomSummaryEntity.displayName ?: "",
|
||||||
|
name = roomSummaryEntity.name ?: "",
|
||||||
topic = roomSummaryEntity.topic ?: "",
|
topic = roomSummaryEntity.topic ?: "",
|
||||||
avatarUrl = roomSummaryEntity.avatarUrl ?: "",
|
avatarUrl = roomSummaryEntity.avatarUrl ?: "",
|
||||||
isDirect = roomSummaryEntity.isDirect,
|
isDirect = roomSummaryEntity.isDirect,
|
||||||
@ -46,6 +51,7 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
|
|||||||
notificationCount = roomSummaryEntity.notificationCount,
|
notificationCount = roomSummaryEntity.notificationCount,
|
||||||
hasUnreadMessages = roomSummaryEntity.hasUnreadMessages,
|
hasUnreadMessages = roomSummaryEntity.hasUnreadMessages,
|
||||||
tags = tags,
|
tags = tags,
|
||||||
|
typingUsers = typingUsers,
|
||||||
membership = roomSummaryEntity.membership,
|
membership = roomSummaryEntity.membership,
|
||||||
versioningState = roomSummaryEntity.versioningState,
|
versioningState = roomSummaryEntity.versioningState,
|
||||||
readMarkerId = roomSummaryEntity.readMarkerId,
|
readMarkerId = roomSummaryEntity.readMarkerId,
|
||||||
|
@ -37,6 +37,7 @@ internal open class EventEntity(@Index var eventId: String = "",
|
|||||||
var redacts: String? = null,
|
var redacts: String? = null,
|
||||||
var decryptionResultJson: String? = null,
|
var decryptionResultJson: String? = null,
|
||||||
var decryptionErrorCode: String? = null,
|
var decryptionErrorCode: String? = null,
|
||||||
|
var decryptionErrorReason: String? = null,
|
||||||
var ageLocalTs: Long? = null
|
var ageLocalTs: Long? = null
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
||||||
@ -62,5 +63,6 @@ internal open class EventEntity(@Index var eventId: String = "",
|
|||||||
val adapter = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java)
|
val adapter = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java)
|
||||||
decryptionResultJson = adapter.toJson(decryptionResult)
|
decryptionResultJson = adapter.toJson(decryptionResult)
|
||||||
decryptionErrorCode = null
|
decryptionErrorCode = null
|
||||||
|
decryptionErrorReason = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ internal open class RoomSummaryEntity(
|
|||||||
@PrimaryKey var roomId: String = "",
|
@PrimaryKey var roomId: String = "",
|
||||||
var displayName: String? = "",
|
var displayName: String? = "",
|
||||||
var avatarUrl: String? = "",
|
var avatarUrl: String? = "",
|
||||||
|
var name: String? = "",
|
||||||
var topic: String? = "",
|
var topic: String? = "",
|
||||||
var latestPreviewableEvent: TimelineEventEntity? = null,
|
var latestPreviewableEvent: TimelineEventEntity? = null,
|
||||||
var heroes: RealmList<String> = RealmList(),
|
var heroes: RealmList<String> = RealmList(),
|
||||||
|
@ -29,3 +29,11 @@ internal annotation class AuthenticatedIdentity
|
|||||||
@Qualifier
|
@Qualifier
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
internal annotation class Unauthenticated
|
internal annotation class Unauthenticated
|
||||||
|
|
||||||
|
@Qualifier
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
internal annotation class UnauthenticatedWithCertificate
|
||||||
|
|
||||||
|
@Qualifier
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
internal annotation class UnauthenticatedWithCertificateWithProgress
|
||||||
|
@ -24,7 +24,7 @@ internal annotation class SessionFilesDirectory
|
|||||||
|
|
||||||
@Qualifier
|
@Qualifier
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
internal annotation class SessionCacheDirectory
|
internal annotation class SessionDownloadsDirectory
|
||||||
|
|
||||||
@Qualifier
|
@Qualifier
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@ -27,6 +27,8 @@ import im.vector.matrix.android.api.auth.AuthenticationService
|
|||||||
import im.vector.matrix.android.internal.SessionManager
|
import im.vector.matrix.android.internal.SessionManager
|
||||||
import im.vector.matrix.android.internal.auth.AuthModule
|
import im.vector.matrix.android.internal.auth.AuthModule
|
||||||
import im.vector.matrix.android.internal.auth.SessionParamsStore
|
import im.vector.matrix.android.internal.auth.SessionParamsStore
|
||||||
|
import im.vector.matrix.android.internal.session.MockHttpInterceptor
|
||||||
|
import im.vector.matrix.android.internal.session.TestInterceptor
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
|
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
|
||||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||||
@ -34,7 +36,7 @@ import okhttp3.OkHttpClient
|
|||||||
import org.matrix.olm.OlmManager
|
import org.matrix.olm.OlmManager
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@Component(modules = [MatrixModule::class, NetworkModule::class, AuthModule::class])
|
@Component(modules = [MatrixModule::class, NetworkModule::class, AuthModule::class, NoOpTestModule::class])
|
||||||
@MatrixScope
|
@MatrixScope
|
||||||
internal interface MatrixComponent {
|
internal interface MatrixComponent {
|
||||||
|
|
||||||
@ -45,6 +47,9 @@ internal interface MatrixComponent {
|
|||||||
@Unauthenticated
|
@Unauthenticated
|
||||||
fun okHttpClient(): OkHttpClient
|
fun okHttpClient(): OkHttpClient
|
||||||
|
|
||||||
|
@MockHttpInterceptor
|
||||||
|
fun testInterceptor(): TestInterceptor?
|
||||||
|
|
||||||
fun authenticationService(): AuthenticationService
|
fun authenticationService(): AuthenticationService
|
||||||
|
|
||||||
fun context(): Context
|
fun context(): Context
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* 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.di
|
||||||
|
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import im.vector.matrix.android.internal.session.MockHttpInterceptor
|
||||||
|
import im.vector.matrix.android.internal.session.TestInterceptor
|
||||||
|
|
||||||
|
@Module
|
||||||
|
internal object NoOpTestModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@JvmStatic
|
||||||
|
@MockHttpInterceptor
|
||||||
|
fun providesTestInterceptor(): TestInterceptor? = null
|
||||||
|
}
|
@ -1,284 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.legacy.riot;
|
|
||||||
|
|
||||||
import android.util.Pair;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import java.security.KeyStore;
|
|
||||||
import java.security.KeyStoreException;
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.cert.Certificate;
|
|
||||||
import java.security.cert.CertificateException;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import javax.net.ssl.HostnameVerifier;
|
|
||||||
import javax.net.ssl.HttpsURLConnection;
|
|
||||||
import javax.net.ssl.SSLContext;
|
|
||||||
import javax.net.ssl.SSLPeerUnverifiedException;
|
|
||||||
import javax.net.ssl.SSLSession;
|
|
||||||
import javax.net.ssl.SSLSocketFactory;
|
|
||||||
import javax.net.ssl.TrustManager;
|
|
||||||
import javax.net.ssl.TrustManagerFactory;
|
|
||||||
import javax.net.ssl.X509TrustManager;
|
|
||||||
|
|
||||||
import okhttp3.CipherSuite;
|
|
||||||
import okhttp3.ConnectionSpec;
|
|
||||||
import okhttp3.TlsVersion;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Various utility classes for dealing with X509Certificates
|
|
||||||
*/
|
|
||||||
public class CertUtil {
|
|
||||||
/**
|
|
||||||
* Generates the SHA-256 fingerprint of the given certificate
|
|
||||||
*
|
|
||||||
* @param cert the certificate.
|
|
||||||
* @return the finger print
|
|
||||||
* @throws CertificateException the certificate exception
|
|
||||||
*/
|
|
||||||
public static byte[] generateSha256Fingerprint(X509Certificate cert) throws CertificateException {
|
|
||||||
return generateFingerprint(cert, "SHA-256");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates the SHA-1 fingerprint of the given certificate
|
|
||||||
*
|
|
||||||
* @param cert the certificated
|
|
||||||
* @return the SHA1 fingerprint
|
|
||||||
* @throws CertificateException the certificate exception
|
|
||||||
*/
|
|
||||||
public static byte[] generateSha1Fingerprint(X509Certificate cert) throws CertificateException {
|
|
||||||
return generateFingerprint(cert, "SHA-1");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate the fingerprint for a dedicated type.
|
|
||||||
*
|
|
||||||
* @param cert the certificate
|
|
||||||
* @param type the type
|
|
||||||
* @return the fingerprint
|
|
||||||
* @throws CertificateException certificate exception
|
|
||||||
*/
|
|
||||||
private static byte[] generateFingerprint(X509Certificate cert, String type) throws CertificateException {
|
|
||||||
final byte[] fingerprint;
|
|
||||||
final MessageDigest md;
|
|
||||||
try {
|
|
||||||
md = MessageDigest.getInstance(type);
|
|
||||||
} catch (Exception e) {
|
|
||||||
// This really *really* shouldn't throw, as java should always have a SHA-256 and SHA-1 impl.
|
|
||||||
throw new CertificateException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
fingerprint = md.digest(cert.getEncoded());
|
|
||||||
|
|
||||||
return fingerprint;
|
|
||||||
}
|
|
||||||
|
|
||||||
final private static char[] hexArray = "0123456789ABCDEF".toCharArray();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert the fingerprint to an hexa string.
|
|
||||||
*
|
|
||||||
* @param fingerprint the fingerprint
|
|
||||||
* @return the hexa string.
|
|
||||||
*/
|
|
||||||
public static String fingerprintToHexString(byte[] fingerprint) {
|
|
||||||
return fingerprintToHexString(fingerprint, ' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String fingerprintToHexString(byte[] fingerprint, char sep) {
|
|
||||||
char[] hexChars = new char[fingerprint.length * 3];
|
|
||||||
for (int j = 0; j < fingerprint.length; j++) {
|
|
||||||
int v = fingerprint[j] & 0xFF;
|
|
||||||
hexChars[j * 3] = hexArray[v >>> 4];
|
|
||||||
hexChars[j * 3 + 1] = hexArray[v & 0x0F];
|
|
||||||
hexChars[j * 3 + 2] = sep;
|
|
||||||
}
|
|
||||||
return new String(hexChars, 0, hexChars.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively checks the exception to see if it was caused by an
|
|
||||||
* UnrecognizedCertificateException
|
|
||||||
*
|
|
||||||
* @param e the throwable.
|
|
||||||
* @return The UnrecognizedCertificateException if exists, else null.
|
|
||||||
*/
|
|
||||||
public static UnrecognizedCertificateException getCertificateException(Throwable e) {
|
|
||||||
int i = 0; // Just in case there is a getCause loop
|
|
||||||
while (e != null && i < 10) {
|
|
||||||
if (e instanceof UnrecognizedCertificateException) {
|
|
||||||
return (UnrecognizedCertificateException) e;
|
|
||||||
}
|
|
||||||
e = e.getCause();
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a SSLSocket factory for a HS config.
|
|
||||||
*
|
|
||||||
* @param hsConfig the HS config.
|
|
||||||
* @return SSLSocket factory
|
|
||||||
*/
|
|
||||||
public static Pair<SSLSocketFactory, X509TrustManager> newPinnedSSLSocketFactory(HomeServerConnectionConfig hsConfig) {
|
|
||||||
X509TrustManager defaultTrustManager = null;
|
|
||||||
|
|
||||||
// If we haven't specified that we wanted to pin the certs, fallback to standard
|
|
||||||
// X509 checks if fingerprints don't match.
|
|
||||||
if (!hsConfig.shouldPin()) {
|
|
||||||
TrustManagerFactory trustManagerFactory = null;
|
|
||||||
|
|
||||||
// get the PKIX instance
|
|
||||||
try {
|
|
||||||
trustManagerFactory = TrustManagerFactory.getInstance("PKIX");
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
Timber.e(e, "## newPinnedSSLSocketFactory() : TrustManagerFactory.getInstance failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
// it doesn't exist, use the default one.
|
|
||||||
if (trustManagerFactory == null) {
|
|
||||||
try {
|
|
||||||
trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
Timber.e(e, "## newPinnedSSLSocketFactory() : TrustManagerFactory.getInstance with default algorithm failed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trustManagerFactory != null) {
|
|
||||||
try {
|
|
||||||
trustManagerFactory.init((KeyStore) null);
|
|
||||||
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
|
|
||||||
|
|
||||||
for (int i = 0; i < trustManagers.length; i++) {
|
|
||||||
if (trustManagers[i] instanceof X509TrustManager) {
|
|
||||||
defaultTrustManager = (X509TrustManager) trustManagers[i];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (KeyStoreException e) {
|
|
||||||
Timber.e(e, "## newPinnedSSLSocketFactory()");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
X509TrustManager trustManager = new PinnedTrustManager(hsConfig.getAllowedFingerprints(), defaultTrustManager);
|
|
||||||
|
|
||||||
TrustManager[] trustManagers = new TrustManager[]{
|
|
||||||
trustManager
|
|
||||||
};
|
|
||||||
|
|
||||||
SSLSocketFactory sslSocketFactory;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (hsConfig.forceUsageOfTlsVersions() && hsConfig.getAcceptedTlsVersions() != null) {
|
|
||||||
// Force usage of accepted Tls Versions for Android < 20
|
|
||||||
sslSocketFactory = new TLSSocketFactory(trustManagers, hsConfig.getAcceptedTlsVersions());
|
|
||||||
} else {
|
|
||||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
|
||||||
sslContext.init(null, trustManagers, new java.security.SecureRandom());
|
|
||||||
sslSocketFactory = sslContext.getSocketFactory();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
// This is too fatal
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Pair<>(sslSocketFactory, trustManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a Host name verifier for a hs config.
|
|
||||||
*
|
|
||||||
* @param hsConfig the hs config.
|
|
||||||
* @return a new HostnameVerifier.
|
|
||||||
*/
|
|
||||||
public static HostnameVerifier newHostnameVerifier(HomeServerConnectionConfig hsConfig) {
|
|
||||||
final HostnameVerifier defaultVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
|
|
||||||
final List<Fingerprint> trusted_fingerprints = hsConfig.getAllowedFingerprints();
|
|
||||||
|
|
||||||
return new HostnameVerifier() {
|
|
||||||
@Override
|
|
||||||
public boolean verify(String hostname, SSLSession session) {
|
|
||||||
if (defaultVerifier.verify(hostname, session)) return true;
|
|
||||||
if (trusted_fingerprints == null || trusted_fingerprints.size() == 0) return false;
|
|
||||||
|
|
||||||
// If remote cert matches an allowed fingerprint, just accept it.
|
|
||||||
try {
|
|
||||||
for (Certificate cert : session.getPeerCertificates()) {
|
|
||||||
for (Fingerprint allowedFingerprint : trusted_fingerprints) {
|
|
||||||
if (allowedFingerprint != null && cert instanceof X509Certificate && allowedFingerprint.matchesCert((X509Certificate) cert)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (SSLPeerUnverifiedException e) {
|
|
||||||
return false;
|
|
||||||
} catch (CertificateException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a list of accepted TLS specifications for a hs config.
|
|
||||||
*
|
|
||||||
* @param hsConfig the hs config.
|
|
||||||
* @param url the url of the end point, used to check if we have to enable CLEARTEXT communication.
|
|
||||||
* @return a list of accepted TLS specifications.
|
|
||||||
*/
|
|
||||||
public static List<ConnectionSpec> newConnectionSpecs(@NonNull HomeServerConnectionConfig hsConfig, @NonNull String url) {
|
|
||||||
final ConnectionSpec.Builder builder = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS);
|
|
||||||
|
|
||||||
final List<TlsVersion> tlsVersions = hsConfig.getAcceptedTlsVersions();
|
|
||||||
if (null != tlsVersions) {
|
|
||||||
builder.tlsVersions(tlsVersions.toArray(new TlsVersion[0]));
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<CipherSuite> tlsCipherSuites = hsConfig.getAcceptedTlsCipherSuites();
|
|
||||||
if (null != tlsCipherSuites) {
|
|
||||||
builder.cipherSuites(tlsCipherSuites.toArray(new CipherSuite[0]));
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.supportsTlsExtensions(hsConfig.shouldAcceptTlsExtensions());
|
|
||||||
|
|
||||||
List<ConnectionSpec> list = new ArrayList<>();
|
|
||||||
|
|
||||||
list.add(builder.build());
|
|
||||||
|
|
||||||
if (url.startsWith("http://")) {
|
|
||||||
list.add(ConnectionSpec.CLEARTEXT);
|
|
||||||
}
|
|
||||||
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
}
|
|
@ -21,8 +21,6 @@ import android.util.Base64;
|
|||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
import java.security.cert.CertificateException;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -40,20 +38,10 @@ public class Fingerprint {
|
|||||||
|
|
||||||
private final HashType mHashType;
|
private final HashType mHashType;
|
||||||
private final byte[] mBytes;
|
private final byte[] mBytes;
|
||||||
private String mDisplayableHexRepr;
|
|
||||||
|
|
||||||
public Fingerprint(HashType hashType, byte[] bytes) {
|
public Fingerprint(HashType hashType, byte[] bytes) {
|
||||||
mHashType = hashType;
|
mHashType = hashType;
|
||||||
mBytes = bytes;
|
mBytes = bytes;
|
||||||
mDisplayableHexRepr = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Fingerprint newSha256Fingerprint(X509Certificate cert) throws CertificateException {
|
|
||||||
return new Fingerprint(HashType.SHA256, CertUtil.generateSha256Fingerprint(cert));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Fingerprint newSha1Fingerprint(X509Certificate cert) throws CertificateException {
|
|
||||||
return new Fingerprint(HashType.SHA1, CertUtil.generateSha1Fingerprint(cert));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public HashType getType() {
|
public HashType getType() {
|
||||||
@ -64,14 +52,6 @@ public class Fingerprint {
|
|||||||
return mBytes;
|
return mBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getBytesAsHexString() {
|
|
||||||
if (mDisplayableHexRepr == null) {
|
|
||||||
mDisplayableHexRepr = CertUtil.fingerprintToHexString(mBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
return mDisplayableHexRepr;
|
|
||||||
}
|
|
||||||
|
|
||||||
public JSONObject toJson() throws JSONException {
|
public JSONObject toJson() throws JSONException {
|
||||||
JSONObject obj = new JSONObject();
|
JSONObject obj = new JSONObject();
|
||||||
obj.put("bytes", Base64.encodeToString(getBytes(), Base64.DEFAULT));
|
obj.put("bytes", Base64.encodeToString(getBytes(), Base64.DEFAULT));
|
||||||
@ -95,24 +75,6 @@ public class Fingerprint {
|
|||||||
return new Fingerprint(hashType, fingerprintBytes);
|
return new Fingerprint(hashType, fingerprintBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean matchesCert(X509Certificate cert) throws CertificateException {
|
|
||||||
Fingerprint o = null;
|
|
||||||
switch (mHashType) {
|
|
||||||
case SHA256:
|
|
||||||
o = Fingerprint.newSha256Fingerprint(cert);
|
|
||||||
break;
|
|
||||||
case SHA1:
|
|
||||||
o = Fingerprint.newSha1Fingerprint(cert);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return equals(o);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String toString() {
|
|
||||||
return String.format("Fingerprint{type: '%s', fingeprint: '%s'}", mHashType.toString(), getBytesAsHexString());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (this == o) return true;
|
if (this == o) return true;
|
||||||
|
@ -1,107 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.legacy.riot;
|
|
||||||
|
|
||||||
import java.security.cert.CertificateException;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import javax.net.ssl.X509TrustManager;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implements a TrustManager that checks Certificates against an explicit list of known
|
|
||||||
* fingerprints.
|
|
||||||
*/
|
|
||||||
public class PinnedTrustManager implements X509TrustManager {
|
|
||||||
private final List<Fingerprint> mFingerprints;
|
|
||||||
@Nullable
|
|
||||||
private final X509TrustManager mDefaultTrustManager;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param fingerprints An array of SHA256 cert fingerprints
|
|
||||||
* @param defaultTrustManager Optional trust manager to fall back on if cert does not match
|
|
||||||
* any of the fingerprints. Can be null.
|
|
||||||
*/
|
|
||||||
public PinnedTrustManager(List<Fingerprint> fingerprints, @Nullable X509TrustManager defaultTrustManager) {
|
|
||||||
mFingerprints = fingerprints;
|
|
||||||
mDefaultTrustManager = defaultTrustManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void checkClientTrusted(X509Certificate[] chain, String s) throws CertificateException {
|
|
||||||
try {
|
|
||||||
if (mDefaultTrustManager != null) {
|
|
||||||
mDefaultTrustManager.checkClientTrusted(
|
|
||||||
chain, s
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (CertificateException e) {
|
|
||||||
// If there is an exception we fall back to checking fingerprints
|
|
||||||
if (mFingerprints == null || mFingerprints.size() == 0) {
|
|
||||||
throw new UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.getCause());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
checkTrusted("client", chain);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void checkServerTrusted(X509Certificate[] chain, String s) throws CertificateException {
|
|
||||||
try {
|
|
||||||
if (mDefaultTrustManager != null) {
|
|
||||||
mDefaultTrustManager.checkServerTrusted(
|
|
||||||
chain, s
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (CertificateException e) {
|
|
||||||
// If there is an exception we fall back to checking fingerprints
|
|
||||||
if (mFingerprints == null || mFingerprints.isEmpty()) {
|
|
||||||
throw new UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.getCause());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
checkTrusted("server", chain);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkTrusted(String type, X509Certificate[] chain) throws CertificateException {
|
|
||||||
X509Certificate cert = chain[0];
|
|
||||||
|
|
||||||
boolean found = false;
|
|
||||||
if (mFingerprints != null) {
|
|
||||||
for (Fingerprint allowedFingerprint : mFingerprints) {
|
|
||||||
if (allowedFingerprint != null && allowedFingerprint.matchesCert(cert)) {
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!found) {
|
|
||||||
throw new UnrecognizedCertificateException(cert, Fingerprint.newSha256Fingerprint(cert), null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public X509Certificate[] getAcceptedIssuers() {
|
|
||||||
return new X509Certificate[0];
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,135 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.legacy.riot;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.Socket;
|
|
||||||
import java.net.UnknownHostException;
|
|
||||||
import java.security.KeyManagementException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import javax.net.ssl.SSLContext;
|
|
||||||
import javax.net.ssl.SSLSocket;
|
|
||||||
import javax.net.ssl.SSLSocketFactory;
|
|
||||||
import javax.net.ssl.TrustManager;
|
|
||||||
|
|
||||||
import okhttp3.TlsVersion;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Force the usage of Tls versions on every created socket
|
|
||||||
* Inspired from https://blog.dev-area.net/2015/08/13/android-4-1-enable-tls-1-1-and-tls-1-2/
|
|
||||||
*/
|
|
||||||
/*package*/ class TLSSocketFactory extends SSLSocketFactory {
|
|
||||||
private SSLSocketFactory internalSSLSocketFactory;
|
|
||||||
|
|
||||||
private String[] enabledProtocols;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
*
|
|
||||||
* @param trustPinned
|
|
||||||
* @param acceptedTlsVersions
|
|
||||||
* @throws KeyManagementException
|
|
||||||
* @throws NoSuchAlgorithmException
|
|
||||||
*/
|
|
||||||
/*package*/ TLSSocketFactory(TrustManager[] trustPinned, List<TlsVersion> acceptedTlsVersions) throws KeyManagementException, NoSuchAlgorithmException {
|
|
||||||
SSLContext context = SSLContext.getInstance("TLS");
|
|
||||||
context.init(null, trustPinned, new SecureRandom());
|
|
||||||
internalSSLSocketFactory = context.getSocketFactory();
|
|
||||||
|
|
||||||
enabledProtocols = new String[acceptedTlsVersions.size()];
|
|
||||||
int i = 0;
|
|
||||||
for (TlsVersion tlsVersion : acceptedTlsVersions) {
|
|
||||||
enabledProtocols[i] = tlsVersion.javaName();
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String[] getDefaultCipherSuites() {
|
|
||||||
return internalSSLSocketFactory.getDefaultCipherSuites();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String[] getSupportedCipherSuites() {
|
|
||||||
return internalSSLSocketFactory.getSupportedCipherSuites();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Socket createSocket() throws IOException {
|
|
||||||
return enableTLSOnSocket(internalSSLSocketFactory.createSocket());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
|
|
||||||
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
|
|
||||||
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
|
|
||||||
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Socket createSocket(InetAddress host, int port) throws IOException {
|
|
||||||
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
|
|
||||||
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Socket enableTLSOnSocket(Socket socket) {
|
|
||||||
if (socket != null && (socket instanceof SSLSocket)) {
|
|
||||||
SSLSocket sslSocket = (SSLSocket) socket;
|
|
||||||
|
|
||||||
List<String> supportedProtocols = Arrays.asList(sslSocket.getSupportedProtocols());
|
|
||||||
List<String> filteredEnabledProtocols = new ArrayList<>();
|
|
||||||
|
|
||||||
for (String protocol : enabledProtocols) {
|
|
||||||
if (supportedProtocols.contains(protocol)) {
|
|
||||||
filteredEnabledProtocols.add(protocol);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!filteredEnabledProtocols.isEmpty()) {
|
|
||||||
try {
|
|
||||||
sslSocket.setEnabledProtocols(filteredEnabledProtocols.toArray(new String[filteredEnabledProtocols.size()]));
|
|
||||||
} catch (Exception e) {
|
|
||||||
Timber.e(e, "Exception");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return socket;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.legacy.riot;
|
|
||||||
|
|
||||||
import java.security.cert.CertificateException;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Thrown when we are given a certificate that does match the certificate we were told to
|
|
||||||
* expect.
|
|
||||||
*/
|
|
||||||
public class UnrecognizedCertificateException extends CertificateException {
|
|
||||||
private final X509Certificate mCert;
|
|
||||||
private final Fingerprint mFingerprint;
|
|
||||||
|
|
||||||
public UnrecognizedCertificateException(X509Certificate cert, Fingerprint fingerprint, Throwable cause) {
|
|
||||||
super("Unrecognized certificate with unknown fingerprint: " + cert.getSubjectDN(), cause);
|
|
||||||
mCert = cert;
|
|
||||||
mFingerprint = fingerprint;
|
|
||||||
}
|
|
||||||
|
|
||||||
public X509Certificate getCertificate() {
|
|
||||||
return mCert;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Fingerprint getFingerprint() {
|
|
||||||
return mFingerprint;
|
|
||||||
}
|
|
||||||
}
|
|
@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.network
|
|||||||
|
|
||||||
import im.vector.matrix.android.api.failure.Failure
|
import im.vector.matrix.android.api.failure.Failure
|
||||||
import im.vector.matrix.android.api.failure.shouldBeRetried
|
import im.vector.matrix.android.api.failure.shouldBeRetried
|
||||||
|
import im.vector.matrix.android.internal.network.ssl.CertUtil
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
@ -26,7 +27,7 @@ import retrofit2.awaitResponse
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
internal suspend inline fun <DATA : Any> executeRequest(eventBus: EventBus?,
|
internal suspend inline fun <DATA : Any> executeRequest(eventBus: EventBus?,
|
||||||
block: Request<DATA>.() -> Unit) = Request<DATA>(eventBus).apply(block).execute()
|
block: Request<DATA>.() -> Unit) = Request<DATA>(eventBus).apply(block).execute()
|
||||||
|
|
||||||
internal class Request<DATA : Any>(private val eventBus: EventBus?) {
|
internal class Request<DATA : Any>(private val eventBus: EventBus?) {
|
||||||
|
|
||||||
@ -48,6 +49,15 @@ internal class Request<DATA : Any>(private val eventBus: EventBus?) {
|
|||||||
throw response.toFailure(eventBus)
|
throw response.toFailure(eventBus)
|
||||||
}
|
}
|
||||||
} catch (exception: Throwable) {
|
} catch (exception: Throwable) {
|
||||||
|
// Check if this is a certificateException
|
||||||
|
CertUtil.getCertificateException(exception)
|
||||||
|
// TODO Support certificate error once logged
|
||||||
|
// ?.also { unrecognizedCertificateException ->
|
||||||
|
// // Send the error to the bus, for a global management
|
||||||
|
// eventBus?.post(GlobalError.CertificateError(unrecognizedCertificateException))
|
||||||
|
// }
|
||||||
|
?.also { unrecognizedCertificateException -> throw unrecognizedCertificateException }
|
||||||
|
|
||||||
if (isRetryable && currentRetryCount++ < maxRetryCount && exception.shouldBeRetried()) {
|
if (isRetryable && currentRetryCount++ < maxRetryCount && exception.shouldBeRetried()) {
|
||||||
delay(currentDelay)
|
delay(currentDelay)
|
||||||
currentDelay = (currentDelay * 2L).coerceAtMost(maxDelay)
|
currentDelay = (currentDelay * 2L).coerceAtMost(maxDelay)
|
||||||
|
@ -26,7 +26,19 @@ import retrofit2.Retrofit
|
|||||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class RetrofitFactory @Inject constructor(private val moshi: Moshi) {
|
internal class RetrofitFactory @Inject constructor(private val moshi: Moshi) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use only for authentication service
|
||||||
|
*/
|
||||||
|
fun create(okHttpClient: OkHttpClient, baseUrl: String): Retrofit {
|
||||||
|
return Retrofit.Builder()
|
||||||
|
.baseUrl(baseUrl.ensureTrailingSlash())
|
||||||
|
.client(okHttpClient)
|
||||||
|
.addConverterFactory(UnitConverterFactory)
|
||||||
|
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
fun create(okHttpClient: Lazy<OkHttpClient>, baseUrl: String): Retrofit {
|
fun create(okHttpClient: Lazy<OkHttpClient>, baseUrl: String): Retrofit {
|
||||||
return Retrofit.Builder()
|
return Retrofit.Builder()
|
||||||
|
@ -16,24 +16,38 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.internal.network.httpclient
|
package im.vector.matrix.android.internal.network.httpclient
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
||||||
import im.vector.matrix.android.internal.network.AccessTokenInterceptor
|
import im.vector.matrix.android.internal.network.AccessTokenInterceptor
|
||||||
import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor
|
import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor
|
||||||
|
import im.vector.matrix.android.internal.network.ssl.CertUtil
|
||||||
import im.vector.matrix.android.internal.network.token.AccessTokenProvider
|
import im.vector.matrix.android.internal.network.token.AccessTokenProvider
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
internal fun OkHttpClient.addAccessTokenInterceptor(accessTokenProvider: AccessTokenProvider): OkHttpClient {
|
internal fun OkHttpClient.Builder.addAccessTokenInterceptor(accessTokenProvider: AccessTokenProvider): OkHttpClient.Builder {
|
||||||
return newBuilder()
|
// Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor
|
||||||
.apply {
|
val existingCurlInterceptors = interceptors().filterIsInstance<CurlLoggingInterceptor>()
|
||||||
// Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor
|
interceptors().removeAll(existingCurlInterceptors)
|
||||||
val existingCurlInterceptors = interceptors().filterIsInstance<CurlLoggingInterceptor>()
|
|
||||||
interceptors().removeAll(existingCurlInterceptors)
|
|
||||||
|
|
||||||
addInterceptor(AccessTokenInterceptor(accessTokenProvider))
|
addInterceptor(AccessTokenInterceptor(accessTokenProvider))
|
||||||
|
|
||||||
// Re add eventually the curl logging interceptors
|
// Re add eventually the curl logging interceptors
|
||||||
existingCurlInterceptors.forEach {
|
existingCurlInterceptors.forEach {
|
||||||
addInterceptor(it)
|
addInterceptor(it)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.build()
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun OkHttpClient.Builder.addSocketFactory(homeServerConnectionConfig: HomeServerConnectionConfig): OkHttpClient.Builder {
|
||||||
|
try {
|
||||||
|
val pair = CertUtil.newPinnedSSLSocketFactory(homeServerConnectionConfig)
|
||||||
|
sslSocketFactory(pair.sslSocketFactory, pair.x509TrustManager)
|
||||||
|
hostnameVerifier(CertUtil.newHostnameVerifier(homeServerConnectionConfig))
|
||||||
|
connectionSpecs(CertUtil.newConnectionSpecs(homeServerConnectionConfig))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "addSocketFactory failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
|
@ -16,29 +16,30 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.internal.network.ssl
|
package im.vector.matrix.android.internal.network.ssl
|
||||||
|
|
||||||
import android.util.Pair
|
|
||||||
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
||||||
import okhttp3.ConnectionSpec
|
import okhttp3.ConnectionSpec
|
||||||
|
import okhttp3.internal.tls.OkHostnameVerifier
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.security.KeyStore
|
import java.security.KeyStore
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.security.cert.CertificateException
|
import java.security.cert.CertificateException
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import javax.net.ssl.HostnameVerifier
|
import javax.net.ssl.HostnameVerifier
|
||||||
import javax.net.ssl.HttpsURLConnection
|
|
||||||
import javax.net.ssl.SSLContext
|
import javax.net.ssl.SSLContext
|
||||||
import javax.net.ssl.SSLPeerUnverifiedException
|
import javax.net.ssl.SSLPeerUnverifiedException
|
||||||
import javax.net.ssl.SSLSocketFactory
|
import javax.net.ssl.SSLSocketFactory
|
||||||
import javax.net.ssl.TrustManager
|
import javax.net.ssl.TrustManager
|
||||||
import javax.net.ssl.TrustManagerFactory
|
import javax.net.ssl.TrustManagerFactory
|
||||||
import javax.net.ssl.X509TrustManager
|
import javax.net.ssl.X509TrustManager
|
||||||
import kotlin.experimental.and
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Various utility classes for dealing with X509Certificates
|
* Various utility classes for dealing with X509Certificates
|
||||||
*/
|
*/
|
||||||
internal object CertUtil {
|
internal object CertUtil {
|
||||||
|
|
||||||
|
// Set to false to do some test
|
||||||
|
private const val USE_DEFAULT_HOSTNAME_VERIFIER = true
|
||||||
|
|
||||||
private val hexArray = "0123456789ABCDEF".toCharArray()
|
private val hexArray = "0123456789ABCDEF".toCharArray()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -95,11 +96,10 @@ internal object CertUtil {
|
|||||||
* @param fingerprint the fingerprint
|
* @param fingerprint the fingerprint
|
||||||
* @return the hexa string.
|
* @return the hexa string.
|
||||||
*/
|
*/
|
||||||
@JvmOverloads
|
|
||||||
fun fingerprintToHexString(fingerprint: ByteArray, sep: Char = ' '): String {
|
fun fingerprintToHexString(fingerprint: ByteArray, sep: Char = ' '): String {
|
||||||
val hexChars = CharArray(fingerprint.size * 3)
|
val hexChars = CharArray(fingerprint.size * 3)
|
||||||
for (j in fingerprint.indices) {
|
for (j in fingerprint.indices) {
|
||||||
val v = (fingerprint[j] and 0xFF.toByte()).toInt()
|
val v = (fingerprint[j].toInt() and 0xFF)
|
||||||
hexChars[j * 3] = hexArray[v.ushr(4)]
|
hexChars[j * 3] = hexArray[v.ushr(4)]
|
||||||
hexChars[j * 3 + 1] = hexArray[v and 0x0F]
|
hexChars[j * 3 + 1] = hexArray[v and 0x0F]
|
||||||
hexChars[j * 3 + 2] = sep
|
hexChars[j * 3 + 2] = sep
|
||||||
@ -128,13 +128,18 @@ internal object CertUtil {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal data class PinnedSSLSocketFactory(
|
||||||
|
val sslSocketFactory: SSLSocketFactory,
|
||||||
|
val x509TrustManager: X509TrustManager
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a SSLSocket factory for a HS config.
|
* Create a SSLSocket factory for a HS config.
|
||||||
*
|
*
|
||||||
* @param hsConfig the HS config.
|
* @param hsConfig the HS config.
|
||||||
* @return SSLSocket factory
|
* @return SSLSocket factory
|
||||||
*/
|
*/
|
||||||
fun newPinnedSSLSocketFactory(hsConfig: HomeServerConnectionConfig): Pair<SSLSocketFactory, X509TrustManager> {
|
fun newPinnedSSLSocketFactory(hsConfig: HomeServerConnectionConfig): PinnedSSLSocketFactory {
|
||||||
try {
|
try {
|
||||||
var defaultTrustManager: X509TrustManager? = null
|
var defaultTrustManager: X509TrustManager? = null
|
||||||
|
|
||||||
@ -155,7 +160,7 @@ internal object CertUtil {
|
|||||||
try {
|
try {
|
||||||
tf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
tf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## addRule : onBingRuleUpdateFailure failed")
|
Timber.e(e, "## newPinnedSSLSocketFactory() : TrustManagerFactory.getInstance of default failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,7 +188,7 @@ internal object CertUtil {
|
|||||||
sslSocketFactory = sslContext.socketFactory
|
sslSocketFactory = sslContext.socketFactory
|
||||||
}
|
}
|
||||||
|
|
||||||
return Pair<SSLSocketFactory, X509TrustManager>(sslSocketFactory, defaultTrustManager)
|
return PinnedSSLSocketFactory(sslSocketFactory, defaultTrustManager!!)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw RuntimeException(e)
|
throw RuntimeException(e)
|
||||||
}
|
}
|
||||||
@ -196,11 +201,14 @@ internal object CertUtil {
|
|||||||
* @return a new HostnameVerifier.
|
* @return a new HostnameVerifier.
|
||||||
*/
|
*/
|
||||||
fun newHostnameVerifier(hsConfig: HomeServerConnectionConfig): HostnameVerifier {
|
fun newHostnameVerifier(hsConfig: HomeServerConnectionConfig): HostnameVerifier {
|
||||||
val defaultVerifier = HttpsURLConnection.getDefaultHostnameVerifier()
|
val defaultVerifier: HostnameVerifier = OkHostnameVerifier // HttpsURLConnection.getDefaultHostnameVerifier()
|
||||||
val trustedFingerprints = hsConfig.allowedFingerprints
|
val trustedFingerprints = hsConfig.allowedFingerprints
|
||||||
|
|
||||||
return HostnameVerifier { hostname, session ->
|
return HostnameVerifier { hostname, session ->
|
||||||
if (defaultVerifier.verify(hostname, session)) return@HostnameVerifier true
|
if (USE_DEFAULT_HOSTNAME_VERIFIER) {
|
||||||
|
if (defaultVerifier.verify(hostname, session)) return@HostnameVerifier true
|
||||||
|
}
|
||||||
|
// TODO How to recover from this error?
|
||||||
if (trustedFingerprints.isEmpty()) return@HostnameVerifier false
|
if (trustedFingerprints.isEmpty()) return@HostnameVerifier false
|
||||||
|
|
||||||
// If remote cert matches an allowed fingerprint, just accept it.
|
// If remote cert matches an allowed fingerprint, just accept it.
|
||||||
|
@ -32,7 +32,7 @@ data class Fingerprint(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(CertificateException::class)
|
@Throws(CertificateException::class)
|
||||||
fun matchesCert(cert: X509Certificate): Boolean {
|
internal fun matchesCert(cert: X509Certificate): Boolean {
|
||||||
val o: Fingerprint? = when (hashType) {
|
val o: Fingerprint? = when (hashType) {
|
||||||
HashType.SHA256 -> newSha256Fingerprint(cert)
|
HashType.SHA256 -> newSha256Fingerprint(cert)
|
||||||
HashType.SHA1 -> newSha1Fingerprint(cert)
|
HashType.SHA1 -> newSha1Fingerprint(cert)
|
||||||
@ -57,7 +57,7 @@ data class Fingerprint(
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
internal companion object {
|
||||||
|
|
||||||
@Throws(CertificateException::class)
|
@Throws(CertificateException::class)
|
||||||
fun newSha256Fingerprint(cert: X509Certificate): Fingerprint {
|
fun newSha256Fingerprint(cert: X509Certificate): Fingerprint {
|
||||||
@ -79,6 +79,6 @@ data class Fingerprint(
|
|||||||
@JsonClass(generateAdapter = false)
|
@JsonClass(generateAdapter = false)
|
||||||
enum class HashType {
|
enum class HashType {
|
||||||
@Json(name = "sha-1") SHA1,
|
@Json(name = "sha-1") SHA1,
|
||||||
@Json(name = "sha-256")SHA256
|
@Json(name = "sha-256") SHA256
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,16 +34,19 @@ import javax.net.ssl.X509TrustManager
|
|||||||
internal class PinnedTrustManager(private val fingerprints: List<Fingerprint>?,
|
internal class PinnedTrustManager(private val fingerprints: List<Fingerprint>?,
|
||||||
private val defaultTrustManager: X509TrustManager?) : X509TrustManager {
|
private val defaultTrustManager: X509TrustManager?) : X509TrustManager {
|
||||||
|
|
||||||
|
// Set to false to perform some test
|
||||||
|
private val USE_DEFAULT_TRUST_MANAGER = true
|
||||||
|
|
||||||
@Throws(CertificateException::class)
|
@Throws(CertificateException::class)
|
||||||
override fun checkClientTrusted(chain: Array<X509Certificate>, s: String) {
|
override fun checkClientTrusted(chain: Array<X509Certificate>, s: String) {
|
||||||
try {
|
try {
|
||||||
if (defaultTrustManager != null) {
|
if (defaultTrustManager != null && USE_DEFAULT_TRUST_MANAGER) {
|
||||||
defaultTrustManager.checkClientTrusted(chain, s)
|
defaultTrustManager.checkClientTrusted(chain, s)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} catch (e: CertificateException) {
|
} catch (e: CertificateException) {
|
||||||
// If there is an exception we fall back to checking fingerprints
|
// If there is an exception we fall back to checking fingerprints
|
||||||
if (fingerprints == null || fingerprints.isEmpty()) {
|
if (fingerprints.isNullOrEmpty()) {
|
||||||
throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause)
|
throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -54,14 +57,14 @@ internal class PinnedTrustManager(private val fingerprints: List<Fingerprint>?,
|
|||||||
@Throws(CertificateException::class)
|
@Throws(CertificateException::class)
|
||||||
override fun checkServerTrusted(chain: Array<X509Certificate>, s: String) {
|
override fun checkServerTrusted(chain: Array<X509Certificate>, s: String) {
|
||||||
try {
|
try {
|
||||||
if (defaultTrustManager != null) {
|
if (defaultTrustManager != null && USE_DEFAULT_TRUST_MANAGER) {
|
||||||
defaultTrustManager.checkServerTrusted(chain, s)
|
defaultTrustManager.checkServerTrusted(chain, s)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} catch (e: CertificateException) {
|
} catch (e: CertificateException) {
|
||||||
// If there is an exception we fall back to checking fingerprints
|
// If there is an exception we fall back to checking fingerprints
|
||||||
if (fingerprints == null || fingerprints.isEmpty()) {
|
if (fingerprints == null || fingerprints.isEmpty()) {
|
||||||
throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause)
|
throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause /* BMA: Shouldn't be `e` ? */)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,18 +16,24 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.internal.session
|
package im.vector.matrix.android.internal.session
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
import arrow.core.Try
|
import arrow.core.Try
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
|
import im.vector.matrix.android.api.extensions.tryThis
|
||||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||||
import im.vector.matrix.android.api.session.file.FileService
|
import im.vector.matrix.android.api.session.file.FileService
|
||||||
import im.vector.matrix.android.api.util.Cancelable
|
import im.vector.matrix.android.api.util.Cancelable
|
||||||
|
import im.vector.matrix.android.api.util.NoOpCancellable
|
||||||
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
|
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
|
||||||
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
|
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
|
||||||
import im.vector.matrix.android.internal.di.CacheDirectory
|
import im.vector.matrix.android.internal.di.CacheDirectory
|
||||||
import im.vector.matrix.android.internal.di.ExternalFilesDirectory
|
import im.vector.matrix.android.internal.di.ExternalFilesDirectory
|
||||||
import im.vector.matrix.android.internal.di.SessionCacheDirectory
|
import im.vector.matrix.android.internal.di.SessionDownloadsDirectory
|
||||||
import im.vector.matrix.android.internal.di.Unauthenticated
|
import im.vector.matrix.android.internal.di.UnauthenticatedWithCertificateWithProgress
|
||||||
import im.vector.matrix.android.internal.extensions.foldToCallback
|
import im.vector.matrix.android.internal.session.download.DownloadProgressInterceptor.Companion.DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||||
import im.vector.matrix.android.internal.util.toCancelable
|
import im.vector.matrix.android.internal.util.toCancelable
|
||||||
@ -36,49 +42,88 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
import okio.buffer
|
||||||
|
import okio.sink
|
||||||
|
import okio.source
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.net.URLEncoder
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class DefaultFileService @Inject constructor(
|
internal class DefaultFileService @Inject constructor(
|
||||||
|
private val context: Context,
|
||||||
@CacheDirectory
|
@CacheDirectory
|
||||||
private val cacheDirectory: File,
|
private val cacheDirectory: File,
|
||||||
@ExternalFilesDirectory
|
@ExternalFilesDirectory
|
||||||
private val externalFilesDirectory: File?,
|
private val externalFilesDirectory: File?,
|
||||||
@SessionCacheDirectory
|
@SessionDownloadsDirectory
|
||||||
private val sessionCacheDirectory: File,
|
private val sessionCacheDirectory: File,
|
||||||
private val contentUrlResolver: ContentUrlResolver,
|
private val contentUrlResolver: ContentUrlResolver,
|
||||||
@Unauthenticated
|
@UnauthenticatedWithCertificateWithProgress
|
||||||
private val okHttpClient: OkHttpClient,
|
private val okHttpClient: OkHttpClient,
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
private val taskExecutor: TaskExecutor
|
private val taskExecutor: TaskExecutor
|
||||||
) : FileService {
|
) : FileService {
|
||||||
|
|
||||||
|
private fun String.safeFileName() = URLEncoder.encode(this, Charsets.US_ASCII.displayName())
|
||||||
|
|
||||||
|
private val downloadFolder = File(sessionCacheDirectory, "MF")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retain ongoing downloads to avoid re-downloading and already downloading file
|
||||||
|
* map of mxCurl to callbacks
|
||||||
|
*/
|
||||||
|
private val ongoing = mutableMapOf<String, ArrayList<MatrixCallback<File>>>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download file in the cache folder, and eventually decrypt it
|
* Download file in the cache folder, and eventually decrypt it
|
||||||
* TODO implement clear file, to delete "MF"
|
* TODO looks like files are copied 3 times
|
||||||
*/
|
*/
|
||||||
override fun downloadFile(downloadMode: FileService.DownloadMode,
|
override fun downloadFile(downloadMode: FileService.DownloadMode,
|
||||||
id: String,
|
id: String,
|
||||||
fileName: String,
|
fileName: String,
|
||||||
|
mimeType: String?,
|
||||||
url: String?,
|
url: String?,
|
||||||
elementToDecrypt: ElementToDecrypt?,
|
elementToDecrypt: ElementToDecrypt?,
|
||||||
callback: MatrixCallback<File>): Cancelable {
|
callback: MatrixCallback<File>): Cancelable {
|
||||||
|
val unwrappedUrl = url ?: return NoOpCancellable.also {
|
||||||
|
callback.onFailure(IllegalArgumentException("url is null"))
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.v("## FileService downloadFile $unwrappedUrl")
|
||||||
|
|
||||||
|
synchronized(ongoing) {
|
||||||
|
val existing = ongoing[unwrappedUrl]
|
||||||
|
if (existing != null) {
|
||||||
|
Timber.v("## FileService downloadFile is already downloading.. ")
|
||||||
|
existing.add(callback)
|
||||||
|
return NoOpCancellable
|
||||||
|
} else {
|
||||||
|
// mark as tracked
|
||||||
|
ongoing[unwrappedUrl] = ArrayList()
|
||||||
|
// and proceed to download
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return taskExecutor.executorScope.launch(coroutineDispatchers.main) {
|
return taskExecutor.executorScope.launch(coroutineDispatchers.main) {
|
||||||
withContext(coroutineDispatchers.io) {
|
withContext(coroutineDispatchers.io) {
|
||||||
Try {
|
Try {
|
||||||
val folder = File(sessionCacheDirectory, "MF")
|
if (!downloadFolder.exists()) {
|
||||||
if (!folder.exists()) {
|
downloadFolder.mkdirs()
|
||||||
folder.mkdirs()
|
|
||||||
}
|
}
|
||||||
File(folder, fileName)
|
// ensure we use unique file name by using URL (mapped to suitable file name)
|
||||||
|
// Also we need to add extension for the FileProvider, if not it lot's of app that it's
|
||||||
|
// shared with will not function well (even if mime type is passed in the intent)
|
||||||
|
File(downloadFolder, fileForUrl(unwrappedUrl, mimeType))
|
||||||
}.flatMap { destFile ->
|
}.flatMap { destFile ->
|
||||||
if (!destFile.exists()) {
|
if (!destFile.exists()) {
|
||||||
val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: return@flatMap Try.Failure(IllegalArgumentException("url is null"))
|
val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: return@flatMap Try.Failure(IllegalArgumentException("url is null"))
|
||||||
|
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(resolvedUrl)
|
.url(resolvedUrl)
|
||||||
|
.header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, url)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val response = try {
|
val response = try {
|
||||||
@ -87,30 +132,104 @@ internal class DefaultFileService @Inject constructor(
|
|||||||
return@flatMap Try.Failure(e)
|
return@flatMap Try.Failure(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
var inputStream = response.body?.byteStream()
|
if (!response.isSuccessful) {
|
||||||
Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${inputStream?.available()}")
|
|
||||||
|
|
||||||
if (!response.isSuccessful || inputStream == null) {
|
|
||||||
return@flatMap Try.Failure(IOException())
|
return@flatMap Try.Failure(IOException())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val source = response.body?.source()
|
||||||
|
?: return@flatMap Try.Failure(IOException())
|
||||||
|
|
||||||
|
Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${!source.exhausted()}")
|
||||||
|
|
||||||
if (elementToDecrypt != null) {
|
if (elementToDecrypt != null) {
|
||||||
Timber.v("## decrypt file")
|
Timber.v("## decrypt file")
|
||||||
inputStream = MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt)
|
val decryptedStream = MXEncryptedAttachments.decryptAttachment(source.inputStream(), elementToDecrypt)
|
||||||
?: return@flatMap Try.Failure(IllegalStateException("Decryption error"))
|
response.close()
|
||||||
|
if (decryptedStream == null) {
|
||||||
|
return@flatMap Try.Failure(IllegalStateException("Decryption error"))
|
||||||
|
} else {
|
||||||
|
decryptedStream.use {
|
||||||
|
writeToFile(decryptedStream, destFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
writeToFile(source.inputStream(), destFile)
|
||||||
|
response.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
writeToFile(inputStream, destFile)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Try.just(copyFile(destFile, downloadMode))
|
Try.just(copyFile(destFile, downloadMode))
|
||||||
}
|
}
|
||||||
}
|
}.fold({
|
||||||
.foldToCallback(callback)
|
callback.onFailure(it)
|
||||||
|
// notify concurrent requests
|
||||||
|
val toNotify = synchronized(ongoing) {
|
||||||
|
ongoing[unwrappedUrl]?.also {
|
||||||
|
ongoing.remove(unwrappedUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toNotify?.forEach { otherCallbacks ->
|
||||||
|
tryThis { otherCallbacks.onFailure(it) }
|
||||||
|
}
|
||||||
|
}, { file ->
|
||||||
|
callback.onSuccess(file)
|
||||||
|
// notify concurrent requests
|
||||||
|
val toNotify = synchronized(ongoing) {
|
||||||
|
ongoing[unwrappedUrl]?.also {
|
||||||
|
ongoing.remove(unwrappedUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Timber.v("## FileService additional to notify ${toNotify?.size ?: 0} ")
|
||||||
|
toNotify?.forEach { otherCallbacks ->
|
||||||
|
tryThis { otherCallbacks.onSuccess(file) }
|
||||||
|
}
|
||||||
|
})
|
||||||
}.toCancelable()
|
}.toCancelable()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun storeDataFor(url: String, mimeType: String?, inputStream: InputStream) {
|
||||||
|
val file = File(downloadFolder, fileForUrl(url, mimeType))
|
||||||
|
val source = inputStream.source().buffer()
|
||||||
|
file.sink().buffer().let { sink ->
|
||||||
|
source.use { input ->
|
||||||
|
sink.use { output ->
|
||||||
|
output.writeAll(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fileForUrl(url: String, mimeType: String?): String {
|
||||||
|
val extension = mimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) }
|
||||||
|
return if (extension != null) "${url.safeFileName()}.$extension" else url.safeFileName()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isFileInCache(mxcUrl: String, mimeType: String?): Boolean {
|
||||||
|
return File(downloadFolder, fileForUrl(mxcUrl, mimeType)).exists()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fileState(mxcUrl: String, mimeType: String?): FileService.FileState {
|
||||||
|
if (isFileInCache(mxcUrl, mimeType)) return FileService.FileState.IN_CACHE
|
||||||
|
val isDownloading = synchronized(ongoing) {
|
||||||
|
ongoing[mxcUrl] != null
|
||||||
|
}
|
||||||
|
return if (isDownloading) FileService.FileState.DOWNLOADING else FileService.FileState.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this URI and pass it to intent using flag Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
* (if not other app won't be able to access it)
|
||||||
|
*/
|
||||||
|
override fun getTemporarySharableURI(mxcUrl: String, mimeType: String?): Uri? {
|
||||||
|
// this string could be extracted no?
|
||||||
|
val authority = "${context.packageName}.mx-sdk.fileprovider"
|
||||||
|
val targetFile = File(downloadFolder, fileForUrl(mxcUrl, mimeType))
|
||||||
|
if (!targetFile.exists()) return null
|
||||||
|
return FileProvider.getUriForFile(context, authority, targetFile)
|
||||||
|
}
|
||||||
|
|
||||||
private fun copyFile(file: File, downloadMode: FileService.DownloadMode): File {
|
private fun copyFile(file: File, downloadMode: FileService.DownloadMode): File {
|
||||||
|
// TODO some of this seems outdated, will need to be re-worked
|
||||||
return when (downloadMode) {
|
return when (downloadMode) {
|
||||||
FileService.DownloadMode.TO_EXPORT ->
|
FileService.DownloadMode.TO_EXPORT ->
|
||||||
file.copyTo(File(externalFilesDirectory, file.name), true)
|
file.copyTo(File(externalFilesDirectory, file.name), true)
|
||||||
@ -120,4 +239,17 @@ internal class DefaultFileService @Inject constructor(
|
|||||||
file
|
file
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getCacheSize(): Int {
|
||||||
|
return downloadFolder.walkTopDown()
|
||||||
|
.onEnter {
|
||||||
|
Timber.v("Get size of ${it.absolutePath}")
|
||||||
|
true
|
||||||
|
}
|
||||||
|
.sumBy { it.length().toInt() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearCache() {
|
||||||
|
downloadFolder.deleteRecursively()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,7 @@ import im.vector.matrix.android.api.session.call.CallSignalingService
|
|||||||
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
||||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||||
|
import im.vector.matrix.android.api.session.file.ContentDownloadStateTracker
|
||||||
import im.vector.matrix.android.api.session.file.FileService
|
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.group.GroupService
|
||||||
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService
|
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService
|
||||||
@ -104,7 +105,7 @@ internal class DefaultSession @Inject constructor(
|
|||||||
private val pushersService: Lazy<PushersService>,
|
private val pushersService: Lazy<PushersService>,
|
||||||
private val termsService: Lazy<TermsService>,
|
private val termsService: Lazy<TermsService>,
|
||||||
private val cryptoService: Lazy<DefaultCryptoService>,
|
private val cryptoService: Lazy<DefaultCryptoService>,
|
||||||
private val fileService: Lazy<FileService>,
|
private val defaultFileService: Lazy<FileService>,
|
||||||
private val secureStorageService: Lazy<SecureStorageService>,
|
private val secureStorageService: Lazy<SecureStorageService>,
|
||||||
private val profileService: Lazy<ProfileService>,
|
private val profileService: Lazy<ProfileService>,
|
||||||
private val widgetService: Lazy<WidgetService>,
|
private val widgetService: Lazy<WidgetService>,
|
||||||
@ -114,6 +115,7 @@ internal class DefaultSession @Inject constructor(
|
|||||||
private val sessionParamsStore: SessionParamsStore,
|
private val sessionParamsStore: SessionParamsStore,
|
||||||
private val contentUploadProgressTracker: ContentUploadStateTracker,
|
private val contentUploadProgressTracker: ContentUploadStateTracker,
|
||||||
private val typingUsersTracker: TypingUsersTracker,
|
private val typingUsersTracker: TypingUsersTracker,
|
||||||
|
private val contentDownloadStateTracker: ContentDownloadStateTracker,
|
||||||
private val initialSyncProgressService: Lazy<InitialSyncProgressService>,
|
private val initialSyncProgressService: Lazy<InitialSyncProgressService>,
|
||||||
private val homeServerCapabilitiesService: Lazy<HomeServerCapabilitiesService>,
|
private val homeServerCapabilitiesService: Lazy<HomeServerCapabilitiesService>,
|
||||||
private val accountDataService: Lazy<AccountDataService>,
|
private val accountDataService: Lazy<AccountDataService>,
|
||||||
@ -134,7 +136,6 @@ internal class DefaultSession @Inject constructor(
|
|||||||
FilterService by filterService.get(),
|
FilterService by filterService.get(),
|
||||||
PushRuleService by pushRuleService.get(),
|
PushRuleService by pushRuleService.get(),
|
||||||
PushersService by pushersService.get(),
|
PushersService by pushersService.get(),
|
||||||
FileService by fileService.get(),
|
|
||||||
TermsService by termsService.get(),
|
TermsService by termsService.get(),
|
||||||
InitialSyncProgressService by initialSyncProgressService.get(),
|
InitialSyncProgressService by initialSyncProgressService.get(),
|
||||||
SecureStorageService by secureStorageService.get(),
|
SecureStorageService by secureStorageService.get(),
|
||||||
@ -294,10 +295,14 @@ internal class DefaultSession @Inject constructor(
|
|||||||
|
|
||||||
override fun typingUsersTracker() = typingUsersTracker
|
override fun typingUsersTracker() = typingUsersTracker
|
||||||
|
|
||||||
|
override fun contentDownloadProgressTracker(): ContentDownloadStateTracker = contentDownloadStateTracker
|
||||||
|
|
||||||
override fun cryptoService(): CryptoService = cryptoService.get()
|
override fun cryptoService(): CryptoService = cryptoService.get()
|
||||||
|
|
||||||
override fun identityService() = defaultIdentityService
|
override fun identityService() = defaultIdentityService
|
||||||
|
|
||||||
|
override fun fileService(): FileService = defaultFileService.get()
|
||||||
|
|
||||||
override fun widgetService(): WidgetService = widgetService.get()
|
override fun widgetService(): WidgetService = widgetService.get()
|
||||||
|
|
||||||
override fun integrationManagerService() = integrationManagerService
|
override fun integrationManagerService() = integrationManagerService
|
||||||
|
@ -43,11 +43,13 @@ import im.vector.matrix.android.internal.crypto.verification.VerificationMessage
|
|||||||
import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory
|
import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory
|
||||||
import im.vector.matrix.android.internal.di.Authenticated
|
import im.vector.matrix.android.internal.di.Authenticated
|
||||||
import im.vector.matrix.android.internal.di.DeviceId
|
import im.vector.matrix.android.internal.di.DeviceId
|
||||||
import im.vector.matrix.android.internal.di.SessionCacheDirectory
|
|
||||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||||
|
import im.vector.matrix.android.internal.di.SessionDownloadsDirectory
|
||||||
import im.vector.matrix.android.internal.di.SessionFilesDirectory
|
import im.vector.matrix.android.internal.di.SessionFilesDirectory
|
||||||
import im.vector.matrix.android.internal.di.SessionId
|
import im.vector.matrix.android.internal.di.SessionId
|
||||||
import im.vector.matrix.android.internal.di.Unauthenticated
|
import im.vector.matrix.android.internal.di.Unauthenticated
|
||||||
|
import im.vector.matrix.android.internal.di.UnauthenticatedWithCertificate
|
||||||
|
import im.vector.matrix.android.internal.di.UnauthenticatedWithCertificateWithProgress
|
||||||
import im.vector.matrix.android.internal.di.UserId
|
import im.vector.matrix.android.internal.di.UserId
|
||||||
import im.vector.matrix.android.internal.di.UserMd5
|
import im.vector.matrix.android.internal.di.UserMd5
|
||||||
import im.vector.matrix.android.internal.eventbus.EventBusTimberLogger
|
import im.vector.matrix.android.internal.eventbus.EventBusTimberLogger
|
||||||
@ -58,9 +60,12 @@ import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
|
|||||||
import im.vector.matrix.android.internal.network.PreferredNetworkCallbackStrategy
|
import im.vector.matrix.android.internal.network.PreferredNetworkCallbackStrategy
|
||||||
import im.vector.matrix.android.internal.network.RetrofitFactory
|
import im.vector.matrix.android.internal.network.RetrofitFactory
|
||||||
import im.vector.matrix.android.internal.network.httpclient.addAccessTokenInterceptor
|
import im.vector.matrix.android.internal.network.httpclient.addAccessTokenInterceptor
|
||||||
|
import im.vector.matrix.android.internal.network.httpclient.addSocketFactory
|
||||||
|
import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor
|
||||||
import im.vector.matrix.android.internal.network.token.AccessTokenProvider
|
import im.vector.matrix.android.internal.network.token.AccessTokenProvider
|
||||||
import im.vector.matrix.android.internal.network.token.HomeserverAccessTokenProvider
|
import im.vector.matrix.android.internal.network.token.HomeserverAccessTokenProvider
|
||||||
import im.vector.matrix.android.internal.session.call.CallEventObserver
|
import im.vector.matrix.android.internal.session.call.CallEventObserver
|
||||||
|
import im.vector.matrix.android.internal.session.download.DownloadProgressInterceptor
|
||||||
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
|
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.homeserver.DefaultHomeServerCapabilitiesService
|
||||||
import im.vector.matrix.android.internal.session.identity.DefaultIdentityService
|
import im.vector.matrix.android.internal.session.identity.DefaultIdentityService
|
||||||
@ -80,6 +85,11 @@ import org.greenrobot.eventbus.EventBus
|
|||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import javax.inject.Provider
|
import javax.inject.Provider
|
||||||
|
import javax.inject.Qualifier
|
||||||
|
|
||||||
|
@Qualifier
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
annotation class MockHttpInterceptor
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
internal abstract class SessionModule {
|
internal abstract class SessionModule {
|
||||||
@ -153,10 +163,10 @@ internal abstract class SessionModule {
|
|||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@Provides
|
@Provides
|
||||||
@SessionCacheDirectory
|
@SessionDownloadsDirectory
|
||||||
fun providesCacheDir(@SessionId sessionId: String,
|
fun providesCacheDir(@SessionId sessionId: String,
|
||||||
context: Context): File {
|
context: Context): File {
|
||||||
return File(context.cacheDir, sessionId)
|
return File(context.cacheDir, "downloads/$sessionId")
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@ -177,13 +187,57 @@ internal abstract class SessionModule {
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@Provides
|
||||||
|
@SessionScope
|
||||||
|
@UnauthenticatedWithCertificate
|
||||||
|
fun providesOkHttpClientWithCertificate(@Unauthenticated okHttpClient: OkHttpClient,
|
||||||
|
homeServerConnectionConfig: HomeServerConnectionConfig): OkHttpClient {
|
||||||
|
return okHttpClient
|
||||||
|
.newBuilder()
|
||||||
|
.addSocketFactory(homeServerConnectionConfig)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@Provides
|
@Provides
|
||||||
@SessionScope
|
@SessionScope
|
||||||
@Authenticated
|
@Authenticated
|
||||||
fun providesOkHttpClient(@Unauthenticated okHttpClient: OkHttpClient,
|
fun providesOkHttpClient(@UnauthenticatedWithCertificate okHttpClient: OkHttpClient,
|
||||||
@Authenticated accessTokenProvider: AccessTokenProvider): OkHttpClient {
|
@Authenticated accessTokenProvider: AccessTokenProvider,
|
||||||
return okHttpClient.addAccessTokenInterceptor(accessTokenProvider)
|
@SessionId sessionId: String,
|
||||||
|
@MockHttpInterceptor testInterceptor: TestInterceptor?): OkHttpClient {
|
||||||
|
return okHttpClient
|
||||||
|
.newBuilder()
|
||||||
|
.addAccessTokenInterceptor(accessTokenProvider)
|
||||||
|
.apply {
|
||||||
|
if (testInterceptor != null) {
|
||||||
|
testInterceptor.sessionId = sessionId
|
||||||
|
addInterceptor(testInterceptor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@Provides
|
||||||
|
@SessionScope
|
||||||
|
@UnauthenticatedWithCertificateWithProgress
|
||||||
|
fun providesProgressOkHttpClient(@UnauthenticatedWithCertificate okHttpClient: OkHttpClient,
|
||||||
|
downloadProgressInterceptor: DownloadProgressInterceptor): OkHttpClient {
|
||||||
|
return okHttpClient.newBuilder()
|
||||||
|
.apply {
|
||||||
|
// Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor
|
||||||
|
val existingCurlInterceptors = interceptors().filterIsInstance<CurlLoggingInterceptor>()
|
||||||
|
interceptors().removeAll(existingCurlInterceptors)
|
||||||
|
|
||||||
|
addInterceptor(downloadProgressInterceptor)
|
||||||
|
|
||||||
|
// Re add eventually the curl logging interceptors
|
||||||
|
existingCurlInterceptors.forEach {
|
||||||
|
addInterceptor(it)
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
|
@ -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.internal.session
|
||||||
|
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
|
||||||
|
interface TestInterceptor : Interceptor {
|
||||||
|
var sessionId: String?
|
||||||
|
}
|
@ -22,7 +22,7 @@ import im.vector.matrix.android.internal.auth.SessionParamsStore
|
|||||||
import im.vector.matrix.android.internal.crypto.CryptoModule
|
import im.vector.matrix.android.internal.crypto.CryptoModule
|
||||||
import im.vector.matrix.android.internal.database.RealmKeysUtils
|
import im.vector.matrix.android.internal.database.RealmKeysUtils
|
||||||
import im.vector.matrix.android.internal.di.CryptoDatabase
|
import im.vector.matrix.android.internal.di.CryptoDatabase
|
||||||
import im.vector.matrix.android.internal.di.SessionCacheDirectory
|
import im.vector.matrix.android.internal.di.SessionDownloadsDirectory
|
||||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||||
import im.vector.matrix.android.internal.di.SessionFilesDirectory
|
import im.vector.matrix.android.internal.di.SessionFilesDirectory
|
||||||
import im.vector.matrix.android.internal.di.SessionId
|
import im.vector.matrix.android.internal.di.SessionId
|
||||||
@ -44,7 +44,7 @@ internal class CleanupSession @Inject constructor(
|
|||||||
@SessionDatabase private val clearSessionDataTask: ClearCacheTask,
|
@SessionDatabase private val clearSessionDataTask: ClearCacheTask,
|
||||||
@CryptoDatabase private val clearCryptoDataTask: ClearCacheTask,
|
@CryptoDatabase private val clearCryptoDataTask: ClearCacheTask,
|
||||||
@SessionFilesDirectory private val sessionFiles: File,
|
@SessionFilesDirectory private val sessionFiles: File,
|
||||||
@SessionCacheDirectory private val sessionCache: File,
|
@SessionDownloadsDirectory private val sessionCache: File,
|
||||||
private val realmKeysUtils: RealmKeysUtils,
|
private val realmKeysUtils: RealmKeysUtils,
|
||||||
@SessionDatabase private val realmSessionConfiguration: RealmConfiguration,
|
@SessionDatabase private val realmSessionConfiguration: RealmConfiguration,
|
||||||
@CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration,
|
@CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration,
|
||||||
|
@ -20,6 +20,8 @@ import dagger.Binds
|
|||||||
import dagger.Module
|
import dagger.Module
|
||||||
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
||||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||||
|
import im.vector.matrix.android.api.session.file.ContentDownloadStateTracker
|
||||||
|
import im.vector.matrix.android.internal.session.download.DefaultContentDownloadStateTracker
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
internal abstract class ContentModule {
|
internal abstract class ContentModule {
|
||||||
@ -27,6 +29,9 @@ internal abstract class ContentModule {
|
|||||||
@Binds
|
@Binds
|
||||||
abstract fun bindContentUploadStateTracker(tracker: DefaultContentUploadStateTracker): ContentUploadStateTracker
|
abstract fun bindContentUploadStateTracker(tracker: DefaultContentUploadStateTracker): ContentUploadStateTracker
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindContentDownloadStateTracker(tracker: DefaultContentDownloadStateTracker): ContentDownloadStateTracker
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindContentUrlResolver(resolver: DefaultContentUrlResolver): ContentUrlResolver
|
abstract fun bindContentUrlResolver(resolver: DefaultContentUrlResolver): ContentUrlResolver
|
||||||
}
|
}
|
||||||
|
@ -16,12 +16,16 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.internal.session.content
|
package im.vector.matrix.android.internal.session.content
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||||
import im.vector.matrix.android.internal.di.Authenticated
|
import im.vector.matrix.android.internal.di.Authenticated
|
||||||
import im.vector.matrix.android.internal.network.ProgressRequestBody
|
import im.vector.matrix.android.internal.network.ProgressRequestBody
|
||||||
import im.vector.matrix.android.internal.network.awaitResponse
|
import im.vector.matrix.android.internal.network.awaitResponse
|
||||||
import im.vector.matrix.android.internal.network.toFailure
|
import im.vector.matrix.android.internal.network.toFailure
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
@ -31,12 +35,14 @@ import okhttp3.RequestBody.Companion.asRequestBody
|
|||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.FileNotFoundException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class FileUploader @Inject constructor(@Authenticated
|
internal class FileUploader @Inject constructor(@Authenticated
|
||||||
private val okHttpClient: OkHttpClient,
|
private val okHttpClient: OkHttpClient,
|
||||||
private val eventBus: EventBus,
|
private val eventBus: EventBus,
|
||||||
|
private val context: Context,
|
||||||
contentUrlResolver: ContentUrlResolver,
|
contentUrlResolver: ContentUrlResolver,
|
||||||
moshi: Moshi) {
|
moshi: Moshi) {
|
||||||
|
|
||||||
@ -59,6 +65,19 @@ internal class FileUploader @Inject constructor(@Authenticated
|
|||||||
return upload(uploadBody, filename, progressListener)
|
return upload(uploadBody, filename, progressListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun uploadFromUri(uri: Uri,
|
||||||
|
filename: String?,
|
||||||
|
mimeType: String?,
|
||||||
|
progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
|
||||||
|
val inputStream = withContext(Dispatchers.IO) {
|
||||||
|
context.contentResolver.openInputStream(uri)
|
||||||
|
} ?: throw FileNotFoundException()
|
||||||
|
|
||||||
|
inputStream.use {
|
||||||
|
return uploadByteArray(it.readBytes(), filename, mimeType, progressListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): ContentUploadResponse {
|
private suspend fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): ContentUploadResponse {
|
||||||
val urlBuilder = uploadUrl.toHttpUrlOrNull()?.newBuilder() ?: throw RuntimeException()
|
val urlBuilder = uploadUrl.toHttpUrlOrNull()?.newBuilder() ?: throw RuntimeException()
|
||||||
|
|
||||||
|
@ -35,6 +35,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageVideoConte
|
|||||||
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
|
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
|
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
|
||||||
import im.vector.matrix.android.internal.network.ProgressRequestBody
|
import im.vector.matrix.android.internal.network.ProgressRequestBody
|
||||||
|
import im.vector.matrix.android.internal.session.DefaultFileService
|
||||||
import im.vector.matrix.android.internal.session.room.send.MultipleEventSendingDispatcherWorker
|
import im.vector.matrix.android.internal.session.room.send.MultipleEventSendingDispatcherWorker
|
||||||
import im.vector.matrix.android.internal.worker.SessionWorkerParams
|
import im.vector.matrix.android.internal.worker.SessionWorkerParams
|
||||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||||
@ -71,6 +72,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
|||||||
|
|
||||||
@Inject lateinit var fileUploader: FileUploader
|
@Inject lateinit var fileUploader: FileUploader
|
||||||
@Inject lateinit var contentUploadStateTracker: DefaultContentUploadStateTracker
|
@Inject lateinit var contentUploadStateTracker: DefaultContentUploadStateTracker
|
||||||
|
@Inject lateinit var fileService: DefaultFileService
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||||
@ -210,6 +212,13 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
|||||||
.uploadFile(cacheFile, attachment.name, attachment.getSafeMimeType(), progressListener)
|
.uploadFile(cacheFile, attachment.name, attachment.getSafeMimeType(), progressListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If it's a file update the file service so that it does not redownload?
|
||||||
|
if (params.attachment.type == ContentAttachmentData.Type.FILE) {
|
||||||
|
context.contentResolver.openInputStream(attachment.queryUri)?.let {
|
||||||
|
fileService.storeDataFor(contentUploadResponse.contentUri, params.attachment.getSafeMimeType(), it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleSuccess(params,
|
handleSuccess(params,
|
||||||
contentUploadResponse.contentUri,
|
contentUploadResponse.contentUri,
|
||||||
uploadedFileEncryptedFileInfo,
|
uploadedFileEncryptedFileInfo,
|
||||||
|
@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* 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.session.download
|
||||||
|
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import im.vector.matrix.android.api.extensions.tryThis
|
||||||
|
import im.vector.matrix.android.api.session.file.ContentDownloadStateTracker
|
||||||
|
import im.vector.matrix.android.internal.session.SessionScope
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@SessionScope
|
||||||
|
internal class DefaultContentDownloadStateTracker @Inject constructor() : ProgressListener, ContentDownloadStateTracker {
|
||||||
|
|
||||||
|
private val mainHandler = Handler(Looper.getMainLooper())
|
||||||
|
private val states = mutableMapOf<String, ContentDownloadStateTracker.State>()
|
||||||
|
private val listeners = mutableMapOf<String, MutableList<ContentDownloadStateTracker.UpdateListener>>()
|
||||||
|
|
||||||
|
override fun track(key: String, updateListener: ContentDownloadStateTracker.UpdateListener) {
|
||||||
|
val listeners = listeners.getOrPut(key) { ArrayList() }
|
||||||
|
if (!listeners.contains(updateListener)) {
|
||||||
|
listeners.add(updateListener)
|
||||||
|
}
|
||||||
|
val currentState = states[key] ?: ContentDownloadStateTracker.State.Idle
|
||||||
|
mainHandler.post {
|
||||||
|
try {
|
||||||
|
updateListener.onDownloadStateUpdate(currentState)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "## ContentUploadStateTracker.onUpdate() failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unTrack(key: String, updateListener: ContentDownloadStateTracker.UpdateListener) {
|
||||||
|
listeners[key]?.apply {
|
||||||
|
remove(updateListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clear() {
|
||||||
|
states.clear()
|
||||||
|
listeners.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
// private fun URL.toKey() = toString()
|
||||||
|
|
||||||
|
override fun update(url: String, bytesRead: Long, contentLength: Long, done: Boolean) {
|
||||||
|
Timber.v("## DL Progress url:$url read:$bytesRead total:$contentLength done:$done")
|
||||||
|
if (done) {
|
||||||
|
updateState(url, ContentDownloadStateTracker.State.Success)
|
||||||
|
} else {
|
||||||
|
updateState(url, ContentDownloadStateTracker.State.Downloading(bytesRead, contentLength, contentLength == -1L))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun error(url: String, errorCode: Int) {
|
||||||
|
Timber.v("## DL Progress Error code:$errorCode")
|
||||||
|
updateState(url, ContentDownloadStateTracker.State.Failure(errorCode))
|
||||||
|
listeners[url]?.forEach {
|
||||||
|
tryThis { it.onDownloadStateUpdate(ContentDownloadStateTracker.State.Failure(errorCode)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateState(url: String, state: ContentDownloadStateTracker.State) {
|
||||||
|
states[url] = state
|
||||||
|
listeners[url]?.forEach {
|
||||||
|
tryThis { it.onDownloadStateUpdate(state) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* 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.session.download
|
||||||
|
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Response
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class DownloadProgressInterceptor @Inject constructor(
|
||||||
|
private val downloadStateTracker: DefaultContentDownloadStateTracker
|
||||||
|
) : Interceptor {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER = "matrix-sdk:mxc_URL"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val url = chain.request().url.toUrl()
|
||||||
|
val mxcURl = chain.request().header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER)
|
||||||
|
|
||||||
|
val request = chain.request().newBuilder()
|
||||||
|
.removeHeader(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val originalResponse = chain.proceed(request)
|
||||||
|
if (!originalResponse.isSuccessful) {
|
||||||
|
downloadStateTracker.error(mxcURl ?: url.toExternalForm(), originalResponse.code)
|
||||||
|
return originalResponse
|
||||||
|
}
|
||||||
|
val responseBody = originalResponse.body ?: return originalResponse
|
||||||
|
return originalResponse.newBuilder()
|
||||||
|
.body(ProgressResponseBody(responseBody, mxcURl ?: url.toExternalForm(), downloadStateTracker))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* 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.session.download
|
||||||
|
|
||||||
|
import okhttp3.MediaType
|
||||||
|
import okhttp3.ResponseBody
|
||||||
|
import okio.Buffer
|
||||||
|
import okio.BufferedSource
|
||||||
|
import okio.ForwardingSource
|
||||||
|
import okio.Source
|
||||||
|
import okio.buffer
|
||||||
|
|
||||||
|
class ProgressResponseBody(
|
||||||
|
private val responseBody: ResponseBody,
|
||||||
|
private val chainUrl: String,
|
||||||
|
private val progressListener: ProgressListener) : ResponseBody() {
|
||||||
|
|
||||||
|
private var bufferedSource: BufferedSource? = null
|
||||||
|
|
||||||
|
override fun contentType(): MediaType? = responseBody.contentType()
|
||||||
|
override fun contentLength(): Long = responseBody.contentLength()
|
||||||
|
|
||||||
|
override fun source(): BufferedSource {
|
||||||
|
if (bufferedSource == null) {
|
||||||
|
bufferedSource = source(responseBody.source()).buffer()
|
||||||
|
}
|
||||||
|
return bufferedSource!!
|
||||||
|
}
|
||||||
|
|
||||||
|
fun source(source: Source): Source {
|
||||||
|
return object : ForwardingSource(source) {
|
||||||
|
var totalBytesRead = 0L
|
||||||
|
|
||||||
|
override fun read(sink: Buffer, byteCount: Long): Long {
|
||||||
|
val bytesRead = super.read(sink, byteCount)
|
||||||
|
// read() returns the number of bytes read, or -1 if this source is exhausted.
|
||||||
|
totalBytesRead += if (bytesRead != -1L) bytesRead else 0L
|
||||||
|
progressListener.update(chainUrl, totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
|
||||||
|
return bytesRead
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProgressListener {
|
||||||
|
fun update(url: String, bytesRead: Long, contentLength: Long, done: Boolean)
|
||||||
|
fun error(url: String, errorCode: Int)
|
||||||
|
}
|
@ -17,6 +17,7 @@
|
|||||||
package im.vector.matrix.android.internal.session.homeserver
|
package im.vector.matrix.android.internal.session.homeserver
|
||||||
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
|
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
||||||
import im.vector.matrix.android.api.auth.wellknown.WellknownResult
|
import im.vector.matrix.android.api.auth.wellknown.WellknownResult
|
||||||
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities
|
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities
|
||||||
import im.vector.matrix.android.internal.auth.version.Versions
|
import im.vector.matrix.android.internal.auth.version.Versions
|
||||||
@ -43,6 +44,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
|
|||||||
private val eventBus: EventBus,
|
private val eventBus: EventBus,
|
||||||
private val getWellknownTask: GetWellknownTask,
|
private val getWellknownTask: GetWellknownTask,
|
||||||
private val configExtractor: IntegrationManagerConfigExtractor,
|
private val configExtractor: IntegrationManagerConfigExtractor,
|
||||||
|
private val homeServerConnectionConfig: HomeServerConnectionConfig,
|
||||||
@UserId
|
@UserId
|
||||||
private val userId: String
|
private val userId: String
|
||||||
) : GetHomeServerCapabilitiesTask {
|
) : GetHomeServerCapabilitiesTask {
|
||||||
@ -78,7 +80,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
|
|||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
|
|
||||||
val wellknownResult = runCatching {
|
val wellknownResult = runCatching {
|
||||||
getWellknownTask.execute(GetWellknownTask.Params(userId))
|
getWellknownTask.execute(GetWellknownTask.Params(userId, homeServerConnectionConfig))
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
|
|
||||||
insertInDb(capabilities, uploadCapabilities, versions, wellknownResult)
|
insertInDb(capabilities, uploadCapabilities, versions, wellknownResult)
|
||||||
|
@ -36,7 +36,7 @@ import im.vector.matrix.android.api.session.identity.ThreePid
|
|||||||
import im.vector.matrix.android.api.util.Cancelable
|
import im.vector.matrix.android.api.util.Cancelable
|
||||||
import im.vector.matrix.android.api.util.NoOpCancellable
|
import im.vector.matrix.android.api.util.NoOpCancellable
|
||||||
import im.vector.matrix.android.internal.di.AuthenticatedIdentity
|
import im.vector.matrix.android.internal.di.AuthenticatedIdentity
|
||||||
import im.vector.matrix.android.internal.di.Unauthenticated
|
import im.vector.matrix.android.internal.di.UnauthenticatedWithCertificate
|
||||||
import im.vector.matrix.android.internal.extensions.observeNotNull
|
import im.vector.matrix.android.internal.extensions.observeNotNull
|
||||||
import im.vector.matrix.android.internal.network.RetrofitFactory
|
import im.vector.matrix.android.internal.network.RetrofitFactory
|
||||||
import im.vector.matrix.android.internal.session.SessionLifecycleObserver
|
import im.vector.matrix.android.internal.session.SessionLifecycleObserver
|
||||||
@ -68,7 +68,7 @@ internal class DefaultIdentityService @Inject constructor(
|
|||||||
private val identityPingTask: IdentityPingTask,
|
private val identityPingTask: IdentityPingTask,
|
||||||
private val identityDisconnectTask: IdentityDisconnectTask,
|
private val identityDisconnectTask: IdentityDisconnectTask,
|
||||||
private val identityRequestTokenForBindingTask: IdentityRequestTokenForBindingTask,
|
private val identityRequestTokenForBindingTask: IdentityRequestTokenForBindingTask,
|
||||||
@Unauthenticated
|
@UnauthenticatedWithCertificate
|
||||||
private val unauthenticatedOkHttpClient: Lazy<OkHttpClient>,
|
private val unauthenticatedOkHttpClient: Lazy<OkHttpClient>,
|
||||||
@AuthenticatedIdentity
|
@AuthenticatedIdentity
|
||||||
private val okHttpClient: Lazy<OkHttpClient>,
|
private val okHttpClient: Lazy<OkHttpClient>,
|
||||||
|
@ -23,7 +23,7 @@ import im.vector.matrix.android.internal.database.RealmKeysUtils
|
|||||||
import im.vector.matrix.android.internal.di.AuthenticatedIdentity
|
import im.vector.matrix.android.internal.di.AuthenticatedIdentity
|
||||||
import im.vector.matrix.android.internal.di.IdentityDatabase
|
import im.vector.matrix.android.internal.di.IdentityDatabase
|
||||||
import im.vector.matrix.android.internal.di.SessionFilesDirectory
|
import im.vector.matrix.android.internal.di.SessionFilesDirectory
|
||||||
import im.vector.matrix.android.internal.di.Unauthenticated
|
import im.vector.matrix.android.internal.di.UnauthenticatedWithCertificate
|
||||||
import im.vector.matrix.android.internal.di.UserMd5
|
import im.vector.matrix.android.internal.di.UserMd5
|
||||||
import im.vector.matrix.android.internal.network.httpclient.addAccessTokenInterceptor
|
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.AccessTokenProvider
|
||||||
@ -45,9 +45,12 @@ internal abstract class IdentityModule {
|
|||||||
@Provides
|
@Provides
|
||||||
@SessionScope
|
@SessionScope
|
||||||
@AuthenticatedIdentity
|
@AuthenticatedIdentity
|
||||||
fun providesOkHttpClient(@Unauthenticated okHttpClient: OkHttpClient,
|
fun providesOkHttpClient(@UnauthenticatedWithCertificate okHttpClient: OkHttpClient,
|
||||||
@AuthenticatedIdentity accessTokenProvider: AccessTokenProvider): OkHttpClient {
|
@AuthenticatedIdentity accessTokenProvider: AccessTokenProvider): OkHttpClient {
|
||||||
return okHttpClient.addAccessTokenInterceptor(accessTokenProvider)
|
return okHttpClient
|
||||||
|
.newBuilder()
|
||||||
|
.addAccessTokenInterceptor(accessTokenProvider)
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.internal.session.profile
|
package im.vector.matrix.android.internal.session.profile
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
@ -27,16 +28,22 @@ import im.vector.matrix.android.api.util.JsonDict
|
|||||||
import im.vector.matrix.android.api.util.Optional
|
import im.vector.matrix.android.api.util.Optional
|
||||||
import im.vector.matrix.android.internal.database.model.UserThreePidEntity
|
import im.vector.matrix.android.internal.database.model.UserThreePidEntity
|
||||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||||
|
import im.vector.matrix.android.internal.session.content.FileUploader
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
import im.vector.matrix.android.internal.task.configureWith
|
import im.vector.matrix.android.internal.task.configureWith
|
||||||
|
import im.vector.matrix.android.internal.task.launchToCallback
|
||||||
|
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||||
import io.realm.kotlin.where
|
import io.realm.kotlin.where
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class DefaultProfileService @Inject constructor(private val taskExecutor: TaskExecutor,
|
internal class DefaultProfileService @Inject constructor(private val taskExecutor: TaskExecutor,
|
||||||
@SessionDatabase private val monarchy: Monarchy,
|
@SessionDatabase private val monarchy: Monarchy,
|
||||||
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
private val refreshUserThreePidsTask: RefreshUserThreePidsTask,
|
private val refreshUserThreePidsTask: RefreshUserThreePidsTask,
|
||||||
private val getProfileInfoTask: GetProfileInfoTask,
|
private val getProfileInfoTask: GetProfileInfoTask,
|
||||||
private val setDisplayNameTask: SetDisplayNameTask) : ProfileService {
|
private val setDisplayNameTask: SetDisplayNameTask,
|
||||||
|
private val setAvatarUrlTask: SetAvatarUrlTask,
|
||||||
|
private val fileUploader: FileUploader) : ProfileService {
|
||||||
|
|
||||||
override fun getDisplayName(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable {
|
override fun getDisplayName(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable {
|
||||||
val params = GetProfileInfoTask.Params(userId)
|
val params = GetProfileInfoTask.Params(userId)
|
||||||
@ -64,6 +71,17 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
|
|||||||
.executeBy(taskExecutor)
|
.executeBy(taskExecutor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String, matrixCallback: MatrixCallback<Unit>): Cancelable {
|
||||||
|
return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, matrixCallback) {
|
||||||
|
val response = fileUploader.uploadFromUri(newAvatarUri, fileName, "image/jpeg")
|
||||||
|
setAvatarUrlTask
|
||||||
|
.configureWith(SetAvatarUrlTask.Params(userId = userId, newAvatarUrl = response.contentUri)) {
|
||||||
|
callback = matrixCallback
|
||||||
|
}
|
||||||
|
.executeBy(taskExecutor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun getAvatarUrl(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable {
|
override fun getAvatarUrl(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable {
|
||||||
val params = GetProfileInfoTask.Params(userId)
|
val params = GetProfileInfoTask.Params(userId)
|
||||||
return getProfileInfoTask
|
return getProfileInfoTask
|
||||||
|
@ -49,6 +49,12 @@ internal interface ProfileAPI {
|
|||||||
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}/displayname")
|
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}/displayname")
|
||||||
fun setDisplayName(@Path("userId") userId: String, @Body body: SetDisplayNameBody): Call<Unit>
|
fun setDisplayName(@Path("userId") userId: String, @Body body: SetDisplayNameBody): Call<Unit>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change user avatar url.
|
||||||
|
*/
|
||||||
|
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}/avatar_url")
|
||||||
|
fun setAvatarUrl(@Path("userId") userId: String, @Body body: SetAvatarUrlBody): Call<Unit>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bind a threePid
|
* Bind a threePid
|
||||||
* Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-account-3pid-bind
|
* Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-account-3pid-bind
|
||||||
|
@ -54,4 +54,7 @@ internal abstract class ProfileModule {
|
|||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindSetDisplayNameTask(task: DefaultSetDisplayNameTask): SetDisplayNameTask
|
abstract fun bindSetDisplayNameTask(task: DefaultSetDisplayNameTask): SetDisplayNameTask
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindSetAvatarUrlTask(task: DefaultSetAvatarUrlTask): SetAvatarUrlTask
|
||||||
}
|
}
|
||||||
|
@ -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.session.profile
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class SetAvatarUrlBody(
|
||||||
|
/**
|
||||||
|
* The new avatar url for this user.
|
||||||
|
*/
|
||||||
|
@Json(name = "avatar_url")
|
||||||
|
val avatarUrl: String
|
||||||
|
)
|
@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* 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.session.profile
|
||||||
|
|
||||||
|
import im.vector.matrix.android.internal.network.executeRequest
|
||||||
|
import im.vector.matrix.android.internal.task.Task
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal abstract class SetAvatarUrlTask : Task<SetAvatarUrlTask.Params, Unit> {
|
||||||
|
data class Params(
|
||||||
|
val userId: String,
|
||||||
|
val newAvatarUrl: String
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DefaultSetAvatarUrlTask @Inject constructor(
|
||||||
|
private val profileAPI: ProfileAPI,
|
||||||
|
private val eventBus: EventBus) : SetAvatarUrlTask() {
|
||||||
|
|
||||||
|
override suspend fun execute(params: Params) {
|
||||||
|
return executeRequest(eventBus) {
|
||||||
|
val body = SetAvatarUrlBody(
|
||||||
|
avatarUrl = params.newAvatarUrl
|
||||||
|
)
|
||||||
|
apiCall = profileAPI.setAvatarUrl(params.userId, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -26,6 +26,7 @@ import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRooms
|
|||||||
import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProtocol
|
import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProtocol
|
||||||
import im.vector.matrix.android.api.util.JsonDict
|
import im.vector.matrix.android.api.util.JsonDict
|
||||||
import im.vector.matrix.android.internal.network.NetworkConstants
|
import im.vector.matrix.android.internal.network.NetworkConstants
|
||||||
|
import im.vector.matrix.android.internal.session.room.alias.AddRoomAliasBody
|
||||||
import im.vector.matrix.android.internal.session.room.alias.RoomAliasDescription
|
import im.vector.matrix.android.internal.session.room.alias.RoomAliasDescription
|
||||||
import im.vector.matrix.android.internal.session.room.membership.RoomMembersResponse
|
import im.vector.matrix.android.internal.session.room.membership.RoomMembersResponse
|
||||||
import im.vector.matrix.android.internal.session.room.membership.admin.UserIdAndReason
|
import im.vector.matrix.android.internal.session.room.membership.admin.UserIdAndReason
|
||||||
@ -311,6 +312,14 @@ internal interface RoomAPI {
|
|||||||
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}")
|
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}")
|
||||||
fun getRoomIdByAlias(@Path("roomAlias") roomAlias: String): Call<RoomAliasDescription>
|
fun getRoomIdByAlias(@Path("roomAlias") roomAlias: String): Call<RoomAliasDescription>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add alias to the room.
|
||||||
|
* @param roomAlias the room alias.
|
||||||
|
*/
|
||||||
|
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}")
|
||||||
|
fun addRoomAlias(@Path("roomAlias") roomAlias: String,
|
||||||
|
@Body body: AddRoomAliasBody): Call<Unit>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inform that the user is starting to type or has stopped typing
|
* Inform that the user is starting to type or has stopped typing
|
||||||
*/
|
*/
|
||||||
|
@ -24,6 +24,8 @@ import im.vector.matrix.android.api.session.room.RoomDirectoryService
|
|||||||
import im.vector.matrix.android.api.session.room.RoomService
|
import im.vector.matrix.android.api.session.room.RoomService
|
||||||
import im.vector.matrix.android.internal.session.DefaultFileService
|
import im.vector.matrix.android.internal.session.DefaultFileService
|
||||||
import im.vector.matrix.android.internal.session.SessionScope
|
import im.vector.matrix.android.internal.session.SessionScope
|
||||||
|
import im.vector.matrix.android.internal.session.room.alias.AddRoomAliasTask
|
||||||
|
import im.vector.matrix.android.internal.session.room.alias.DefaultAddRoomAliasTask
|
||||||
import im.vector.matrix.android.internal.session.room.alias.DefaultGetRoomIdByAliasTask
|
import im.vector.matrix.android.internal.session.room.alias.DefaultGetRoomIdByAliasTask
|
||||||
import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask
|
import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask
|
||||||
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
|
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
|
||||||
@ -190,6 +192,9 @@ internal abstract class RoomModule {
|
|||||||
@Binds
|
@Binds
|
||||||
abstract fun bindGetRoomIdByAliasTask(task: DefaultGetRoomIdByAliasTask): GetRoomIdByAliasTask
|
abstract fun bindGetRoomIdByAliasTask(task: DefaultGetRoomIdByAliasTask): GetRoomIdByAliasTask
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindAddRoomAliasTask(task: DefaultAddRoomAliasTask): AddRoomAliasTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindSendTypingTask(task: DefaultSendTypingTask): SendTypingTask
|
abstract fun bindSendTypingTask(task: DefaultSendTypingTask): SendTypingTask
|
||||||
|
|
||||||
|
@ -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.session.room.alias
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class AddRoomAliasBody(
|
||||||
|
/**
|
||||||
|
* Required. The room id which the alias will be added to.
|
||||||
|
*/
|
||||||
|
@Json(name = "room_id") val roomId: String
|
||||||
|
)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user