mirror of
https://github.com/element-hq/element-android
synced 2024-11-27 20:06:51 +03:00
Merge develop into the branch.
This commit is contained in:
commit
12429d8091
112 changed files with 2951 additions and 916 deletions
|
@ -3,12 +3,16 @@ Changes in RiotX 0.19.0 (2020-XX-XX)
|
||||||
|
|
||||||
Features ✨:
|
Features ✨:
|
||||||
- Cross-Signing | Support SSSS secret sharing (#944)
|
- Cross-Signing | Support SSSS secret sharing (#944)
|
||||||
|
- Cross-Signing | Verify new session from existing session (#1134)
|
||||||
|
|
||||||
Improvements 🙌:
|
Improvements 🙌:
|
||||||
- Verification DM / Handle concurrent .start after .ready (#794)
|
- Verification DM / Handle concurrent .start after .ready (#794)
|
||||||
|
- Reimplementation of multiple attachment picker
|
||||||
|
|
||||||
Bugfix 🐛:
|
Bugfix 🐛:
|
||||||
- Missing avatar/displayname after verification request message (#841)
|
- Missing avatar/displayname after verification request message (#841)
|
||||||
|
- Crypto | RiotX sometimes rotate the current device keys (#1170)
|
||||||
|
- RiotX can't restore cross signing keys saved by web in SSSS (#1174)
|
||||||
|
|
||||||
Translations 🗣:
|
Translations 🗣:
|
||||||
-
|
-
|
||||||
|
|
|
@ -119,7 +119,7 @@ dependencies {
|
||||||
implementation "ru.noties.markwon:core:$markwon_version"
|
implementation "ru.noties.markwon:core:$markwon_version"
|
||||||
|
|
||||||
// Image
|
// Image
|
||||||
implementation 'androidx.exifinterface:exifinterface:1.1.0'
|
implementation 'androidx.exifinterface:exifinterface:1.3.0-alpha01'
|
||||||
implementation 'id.zelory:compressor:3.0.0'
|
implementation 'id.zelory:compressor:3.0.0'
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
|
|
|
@ -266,8 +266,8 @@ class CommonTestHelper(context: Context) {
|
||||||
* @param latch
|
* @param latch
|
||||||
* @throws InterruptedException
|
* @throws InterruptedException
|
||||||
*/
|
*/
|
||||||
fun await(latch: CountDownLatch) {
|
fun await(latch: CountDownLatch, timout: Long? = TestConstants.timeOutMillis) {
|
||||||
assertTrue(latch.await(TestConstants.timeOutMillis, TimeUnit.MILLISECONDS))
|
assertTrue(latch.await(timout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) {
|
fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) {
|
||||||
|
@ -282,10 +282,10 @@ class CommonTestHelper(context: Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun waitWithLatch(block: (CountDownLatch) -> Unit) {
|
fun waitWithLatch(timout: Long? = TestConstants.timeOutMillis, block: (CountDownLatch) -> Unit) {
|
||||||
val latch = CountDownLatch(1)
|
val latch = CountDownLatch(1)
|
||||||
block(latch)
|
block(latch)
|
||||||
await(latch)
|
await(latch, timout)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transform a method with a MatrixCallback to a synchronous method
|
// Transform a method with a MatrixCallback to a synchronous method
|
||||||
|
|
|
@ -19,6 +19,12 @@ package im.vector.matrix.android.internal.crypto.gossiping
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import im.vector.matrix.android.InstrumentedTest
|
import im.vector.matrix.android.InstrumentedTest
|
||||||
|
import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction
|
||||||
|
import im.vector.matrix.android.api.session.crypto.verification.SasVerificationTransaction
|
||||||
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod
|
||||||
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationService
|
||||||
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
|
||||||
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
||||||
import im.vector.matrix.android.api.session.events.model.toModel
|
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
|
||||||
|
@ -28,7 +34,11 @@ import im.vector.matrix.android.common.TestConstants
|
||||||
import im.vector.matrix.android.internal.crypto.GossipingRequestState
|
import im.vector.matrix.android.internal.crypto.GossipingRequestState
|
||||||
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestState
|
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestState
|
||||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||||
|
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.event.EncryptedEventContent
|
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||||
|
import junit.framework.TestCase.assertEquals
|
||||||
import junit.framework.TestCase.assertNotNull
|
import junit.framework.TestCase.assertNotNull
|
||||||
import junit.framework.TestCase.assertTrue
|
import junit.framework.TestCase.assertTrue
|
||||||
import junit.framework.TestCase.fail
|
import junit.framework.TestCase.fail
|
||||||
|
@ -174,4 +184,85 @@ class KeyShareTests : InstrumentedTest {
|
||||||
mTestHelper.signOutAndClose(aliceSession)
|
mTestHelper.signOutAndClose(aliceSession)
|
||||||
mTestHelper.signOutAndClose(aliceSession2)
|
mTestHelper.signOutAndClose(aliceSession2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_ShareSSSSSecret() {
|
||||||
|
val aliceSession1 = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
|
||||||
|
|
||||||
|
mTestHelper.doSync<Unit> {
|
||||||
|
aliceSession1.cryptoService().crossSigningService()
|
||||||
|
.initializeCrossSigning(UserPasswordAuth(
|
||||||
|
user = aliceSession1.myUserId,
|
||||||
|
password = TestConstants.PASSWORD
|
||||||
|
), it)
|
||||||
|
}
|
||||||
|
|
||||||
|
val aliceSession2 = mTestHelper.logIntoAccount(aliceSession1.myUserId, SessionTestParams(true))
|
||||||
|
|
||||||
|
val aliceVerificationService1 = aliceSession1.cryptoService().verificationService()
|
||||||
|
val aliceVerificationService2 = aliceSession2.cryptoService().verificationService()
|
||||||
|
|
||||||
|
// force keys download
|
||||||
|
mTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> {
|
||||||
|
aliceSession1.cryptoService().downloadKeys(listOf(aliceSession1.myUserId), true, it)
|
||||||
|
}
|
||||||
|
mTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> {
|
||||||
|
aliceSession2.cryptoService().downloadKeys(listOf(aliceSession2.myUserId), true, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
var session1ShortCode: String? = null
|
||||||
|
var session2ShortCode: String? = null
|
||||||
|
|
||||||
|
aliceVerificationService1.addListener(object : VerificationService.Listener {
|
||||||
|
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||||
|
Log.d("#TEST", "AA: tx incoming?:${tx.isIncoming} state ${tx.state}")
|
||||||
|
if (tx is SasVerificationTransaction) {
|
||||||
|
if (tx.state == VerificationTxState.OnStarted) {
|
||||||
|
(tx as IncomingSasVerificationTransaction).performAccept()
|
||||||
|
}
|
||||||
|
if (tx.state == VerificationTxState.ShortCodeReady) {
|
||||||
|
session1ShortCode = tx.getDecimalCodeRepresentation()
|
||||||
|
tx.userHasVerifiedShortCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
aliceVerificationService2.addListener(object : VerificationService.Listener {
|
||||||
|
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||||
|
Log.d("#TEST", "BB: tx incoming?:${tx.isIncoming} state ${tx.state}")
|
||||||
|
if (tx is SasVerificationTransaction) {
|
||||||
|
if (tx.state == VerificationTxState.ShortCodeReady) {
|
||||||
|
session2ShortCode = tx.getDecimalCodeRepresentation()
|
||||||
|
tx.userHasVerifiedShortCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
val txId: String = "m.testVerif12"
|
||||||
|
aliceVerificationService2.beginKeyVerification(VerificationMethod.SAS, aliceSession1.myUserId, aliceSession1.sessionParams.credentials.deviceId
|
||||||
|
?: "", txId)
|
||||||
|
|
||||||
|
mTestHelper.waitWithLatch { latch ->
|
||||||
|
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
aliceSession1.cryptoService().getDeviceInfo(aliceSession1.myUserId, aliceSession2.sessionParams.credentials.deviceId ?: "")?.isVerified == true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNotNull(session1ShortCode)
|
||||||
|
Log.d("#TEST", "session1ShortCode: $session1ShortCode")
|
||||||
|
assertNotNull(session2ShortCode)
|
||||||
|
Log.d("#TEST", "session2ShortCode: $session2ShortCode")
|
||||||
|
assertEquals(session1ShortCode, session2ShortCode)
|
||||||
|
|
||||||
|
// SSK and USK private keys should have been shared
|
||||||
|
|
||||||
|
mTestHelper.waitWithLatch(60_000) { latch ->
|
||||||
|
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
Log.d("#TEST", "CAN XS :${ aliceSession2.cryptoService().crossSigningService().getMyCrossSigningKeys()}")
|
||||||
|
aliceSession2.cryptoService().crossSigningService().canCrossSign()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.api.session.content
|
package im.vector.matrix.android.api.session.content
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.exifinterface.media.ExifInterface
|
import androidx.exifinterface.media.ExifInterface
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
@ -29,8 +30,7 @@ data class ContentAttachmentData(
|
||||||
val width: Long? = 0,
|
val width: Long? = 0,
|
||||||
val exifOrientation: Int = ExifInterface.ORIENTATION_UNDEFINED,
|
val exifOrientation: Int = ExifInterface.ORIENTATION_UNDEFINED,
|
||||||
val name: String? = null,
|
val name: String? = null,
|
||||||
val queryUri: String,
|
val queryUri: Uri,
|
||||||
val path: String,
|
|
||||||
private val mimeType: String?,
|
private val mimeType: String?,
|
||||||
val type: Type
|
val type: Type
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
|
@ -60,6 +60,8 @@ interface VerificationService {
|
||||||
roomId: String,
|
roomId: String,
|
||||||
localId: String? = LocalEcho.createLocalEchoId()): PendingVerificationRequest
|
localId: String? = LocalEcho.createLocalEchoId()): PendingVerificationRequest
|
||||||
|
|
||||||
|
fun cancelVerificationRequest(request: PendingVerificationRequest)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request a key verification from another user using toDevice events.
|
* Request a key verification from another user using toDevice events.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -140,7 +140,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
|
|
||||||
private val crossSigningService: DefaultCrossSigningService,
|
private val crossSigningService: DefaultCrossSigningService,
|
||||||
//
|
//
|
||||||
private val incomingRoomKeyRequestManager: IncomingRoomKeyRequestManager,
|
private val incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
||||||
//
|
//
|
||||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||||
// Actions
|
// Actions
|
||||||
|
@ -239,7 +239,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
override fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) {
|
override fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) {
|
||||||
getDevicesTask
|
getDevicesTask
|
||||||
.configureWith {
|
.configureWith {
|
||||||
this.executionThread = TaskThread.CRYPTO
|
// this.executionThread = TaskThread.CRYPTO
|
||||||
this.callback = callback
|
this.callback = callback
|
||||||
}
|
}
|
||||||
.executeBy(taskExecutor)
|
.executeBy(taskExecutor)
|
||||||
|
@ -317,7 +317,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
deviceListManager.invalidateAllDeviceLists()
|
deviceListManager.invalidateAllDeviceLists()
|
||||||
deviceListManager.refreshOutdatedDeviceLists()
|
deviceListManager.refreshOutdatedDeviceLists()
|
||||||
} else {
|
} else {
|
||||||
incomingRoomKeyRequestManager.processReceivedGossipingRequests()
|
incomingGossipingRequestManager.processReceivedGossipingRequests()
|
||||||
}
|
}
|
||||||
}.fold(
|
}.fold(
|
||||||
{
|
{
|
||||||
|
@ -376,7 +376,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
// Make sure we process to-device messages before generating new one-time-keys #2782
|
// Make sure we process to-device messages before generating new one-time-keys #2782
|
||||||
deviceListManager.refreshOutdatedDeviceLists()
|
deviceListManager.refreshOutdatedDeviceLists()
|
||||||
oneTimeKeysUploader.maybeUploadOneTimeKeys()
|
oneTimeKeysUploader.maybeUploadOneTimeKeys()
|
||||||
incomingRoomKeyRequestManager.processReceivedGossipingRequests()
|
incomingGossipingRequestManager.processReceivedGossipingRequests()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -709,7 +709,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
// save audit trail
|
// save audit trail
|
||||||
cryptoStore.saveGossipingEvent(event)
|
cryptoStore.saveGossipingEvent(event)
|
||||||
// Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete)
|
// Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete)
|
||||||
incomingRoomKeyRequestManager.onGossipingRequestEvent(event)
|
incomingGossipingRequestManager.onGossipingRequestEvent(event)
|
||||||
}
|
}
|
||||||
EventType.SEND_SECRET -> {
|
EventType.SEND_SECRET -> {
|
||||||
cryptoStore.saveGossipingEvent(event)
|
cryptoStore.saveGossipingEvent(event)
|
||||||
|
@ -729,30 +729,30 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
*/
|
*/
|
||||||
private fun onRoomKeyEvent(event: Event) {
|
private fun onRoomKeyEvent(event: Event) {
|
||||||
val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return
|
val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return
|
||||||
Timber.v("## onRoomKeyEvent() : type<${event.type}> , sessionId<${roomKeyContent.sessionId}>")
|
Timber.v("## GOSSIP onRoomKeyEvent() : type<${event.type}> , sessionId<${roomKeyContent.sessionId}>")
|
||||||
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) {
|
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) {
|
||||||
Timber.e("## onRoomKeyEvent() : missing fields")
|
Timber.e("## GOSSIP onRoomKeyEvent() : missing fields")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(roomKeyContent.roomId, roomKeyContent.algorithm)
|
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(roomKeyContent.roomId, roomKeyContent.algorithm)
|
||||||
if (alg == null) {
|
if (alg == null) {
|
||||||
Timber.e("## onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}")
|
Timber.e("## GOSSIP onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
alg.onRoomKeyEvent(event, keysBackupService)
|
alg.onRoomKeyEvent(event, keysBackupService)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onSecretSendReceived(event: Event) {
|
private fun onSecretSendReceived(event: Event) {
|
||||||
Timber.i("## onSecretSend() : onSecretSendReceived ${event.content?.get("sender_key")}")
|
Timber.i("## GOSSIP onSecretSend() : onSecretSendReceived ${event.content?.get("sender_key")}")
|
||||||
if (!event.isEncrypted()) {
|
if (!event.isEncrypted()) {
|
||||||
// secret send messages must be encrypted
|
// secret send messages must be encrypted
|
||||||
Timber.e("## onSecretSend() :Received unencrypted secret send event")
|
Timber.e("## GOSSIP onSecretSend() :Received unencrypted secret send event")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Was that sent by us?
|
// Was that sent by us?
|
||||||
if (event.senderId != credentials.userId) {
|
if (event.senderId != credentials.userId) {
|
||||||
Timber.e("## onSecretSend() : Ignore secret from other user ${event.senderId}")
|
Timber.e("## GOSSIP onSecretSend() : Ignore secret from other user ${event.senderId}")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -762,7 +762,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
.getOutgoingSecretKeyRequests().firstOrNull { it.requestId == secretContent.requestId }
|
.getOutgoingSecretKeyRequests().firstOrNull { it.requestId == secretContent.requestId }
|
||||||
|
|
||||||
if (existingRequest == null) {
|
if (existingRequest == null) {
|
||||||
Timber.i("## onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}")
|
Timber.i("## GOSSIP onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1111,7 +1111,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
* @param listener listener
|
* @param listener listener
|
||||||
*/
|
*/
|
||||||
override fun addRoomKeysRequestListener(listener: GossipingRequestListener) {
|
override fun addRoomKeysRequestListener(listener: GossipingRequestListener) {
|
||||||
incomingRoomKeyRequestManager.addRoomKeysRequestListener(listener)
|
incomingGossipingRequestManager.addRoomKeysRequestListener(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1120,7 +1120,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
* @param listener listener
|
* @param listener listener
|
||||||
*/
|
*/
|
||||||
override fun removeRoomKeysRequestListener(listener: GossipingRequestListener) {
|
override fun removeRoomKeysRequestListener(listener: GossipingRequestListener) {
|
||||||
incomingRoomKeyRequestManager.removeRoomKeysRequestListener(listener)
|
incomingGossipingRequestManager.removeRoomKeysRequestListener(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -25,7 +25,9 @@ enum class GossipingRequestState {
|
||||||
NONE,
|
NONE,
|
||||||
PENDING,
|
PENDING,
|
||||||
REJECTED,
|
REJECTED,
|
||||||
|
ACCEPTING,
|
||||||
ACCEPTED,
|
ACCEPTED,
|
||||||
|
FAILED_TO_ACCEPTED,
|
||||||
// USER_REJECTED,
|
// USER_REJECTED,
|
||||||
UNABLE_TO_PROCESS,
|
UNABLE_TO_PROCESS,
|
||||||
CANCELLED_BY_REQUESTER,
|
CANCELLED_BY_REQUESTER,
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
import androidx.work.BackoffPolicy
|
||||||
|
import androidx.work.Data
|
||||||
|
import androidx.work.ExistingWorkPolicy
|
||||||
|
import androidx.work.ListenableWorker
|
||||||
|
import androidx.work.OneTimeWorkRequest
|
||||||
|
import im.vector.matrix.android.api.util.Cancelable
|
||||||
|
import im.vector.matrix.android.internal.di.WorkManagerProvider
|
||||||
|
import im.vector.matrix.android.internal.session.SessionScope
|
||||||
|
import im.vector.matrix.android.internal.util.CancelableWork
|
||||||
|
import im.vector.matrix.android.internal.worker.startChain
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@SessionScope
|
||||||
|
internal class GossipingWorkManager @Inject constructor(
|
||||||
|
private val workManagerProvider: WorkManagerProvider
|
||||||
|
) {
|
||||||
|
|
||||||
|
inline fun <reified W : ListenableWorker> createWork(data: Data, startChain: Boolean): OneTimeWorkRequest {
|
||||||
|
return workManagerProvider.matrixOneTimeWorkRequestBuilder<W>()
|
||||||
|
.setConstraints(WorkManagerProvider.workConstraints)
|
||||||
|
.startChain(startChain)
|
||||||
|
.setInputData(data)
|
||||||
|
.setBackoffCriteria(BackoffPolicy.LINEAR, 10_000L, TimeUnit.MILLISECONDS)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent sending queue to stay broken after app restart
|
||||||
|
// The unique queue id will stay the same as long as this object is instanciated
|
||||||
|
val queueSuffixApp = System.currentTimeMillis()
|
||||||
|
|
||||||
|
fun postWork(workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND): Cancelable {
|
||||||
|
workManagerProvider.workManager
|
||||||
|
.beginUniqueWork(this::class.java.name + "_$queueSuffixApp", policy, workRequest)
|
||||||
|
.enqueue()
|
||||||
|
|
||||||
|
return CancelableWork(workManagerProvider.workManager, workRequest.id)
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,7 @@
|
||||||
package im.vector.matrix.android.internal.crypto
|
package im.vector.matrix.android.internal.crypto
|
||||||
|
|
||||||
import im.vector.matrix.android.api.auth.data.Credentials
|
import im.vector.matrix.android.api.auth.data.Credentials
|
||||||
|
import im.vector.matrix.android.api.auth.data.sessionId
|
||||||
import im.vector.matrix.android.api.crypto.MXCryptoConfig
|
import im.vector.matrix.android.api.crypto.MXCryptoConfig
|
||||||
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
||||||
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
||||||
|
@ -27,16 +28,19 @@ import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.GossipingDefaultContent
|
import im.vector.matrix.android.internal.crypto.model.rest.GossipingDefaultContent
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.GossipingToDeviceObject
|
import im.vector.matrix.android.internal.crypto.model.rest.GossipingToDeviceObject
|
||||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||||
|
import im.vector.matrix.android.internal.di.SessionId
|
||||||
import im.vector.matrix.android.internal.session.SessionScope
|
import im.vector.matrix.android.internal.session.SessionScope
|
||||||
|
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@SessionScope
|
@SessionScope
|
||||||
internal class IncomingRoomKeyRequestManager @Inject constructor(
|
internal class IncomingGossipingRequestManager @Inject constructor(
|
||||||
|
@SessionId private val sessionId: String,
|
||||||
private val credentials: Credentials,
|
private val credentials: Credentials,
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
private val cryptoConfig: MXCryptoConfig,
|
private val cryptoConfig: MXCryptoConfig,
|
||||||
private val secretSecretCryptoProvider: ShareSecretCryptoProvider,
|
private val gossipingWorkManager: GossipingWorkManager,
|
||||||
private val roomDecryptorProvider: RoomDecryptorProvider) {
|
private val roomDecryptorProvider: RoomDecryptorProvider) {
|
||||||
|
|
||||||
// list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations
|
// list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations
|
||||||
|
@ -51,6 +55,32 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||||
receivedGossipingRequests.addAll(cryptoStore.getPendingIncomingGossipingRequests())
|
receivedGossipingRequests.addAll(cryptoStore.getPendingIncomingGossipingRequests())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recently verified devices (map of deviceId and timestamp)
|
||||||
|
private val recentlyVerifiedDevices = HashMap<String, Long>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a session has been verified.
|
||||||
|
* This information can be used by the manager to decide whether or not to fullfil gossiping requests
|
||||||
|
*/
|
||||||
|
fun onVerificationCompleteForDevice(deviceId: String) {
|
||||||
|
// For now we just keep an in memory cache
|
||||||
|
synchronized(recentlyVerifiedDevices) {
|
||||||
|
recentlyVerifiedDevices[deviceId] = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId: String): Boolean {
|
||||||
|
val verifTimestamp: Long?
|
||||||
|
synchronized(recentlyVerifiedDevices) {
|
||||||
|
verifTimestamp = recentlyVerifiedDevices[deviceId]
|
||||||
|
}
|
||||||
|
if (verifTimestamp == null) return false
|
||||||
|
|
||||||
|
val age = System.currentTimeMillis() - verifTimestamp
|
||||||
|
|
||||||
|
return age < FIVE_MINUTES_IN_MILLIS
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when we get an m.room_key_request event
|
* Called when we get an m.room_key_request event
|
||||||
* It must be called on CryptoThread
|
* It must be called on CryptoThread
|
||||||
|
@ -58,7 +88,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||||
* @param event the announcement event.
|
* @param event the announcement event.
|
||||||
*/
|
*/
|
||||||
fun onGossipingRequestEvent(event: Event) {
|
fun onGossipingRequestEvent(event: Event) {
|
||||||
Timber.v("## onGossipingRequestEvent type ${event.type} from user ${event.senderId}")
|
Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} from user ${event.senderId}")
|
||||||
val roomKeyShare = event.getClearContent().toModel<GossipingDefaultContent>()
|
val roomKeyShare = event.getClearContent().toModel<GossipingDefaultContent>()
|
||||||
val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
|
val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
|
||||||
when (roomKeyShare?.action) {
|
when (roomKeyShare?.action) {
|
||||||
|
@ -67,7 +97,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||||
IncomingSecretShareRequest.fromEvent(event)?.let {
|
IncomingSecretShareRequest.fromEvent(event)?.let {
|
||||||
if (event.senderId == credentials.userId && it.deviceId == credentials.deviceId) {
|
if (event.senderId == credentials.userId && it.deviceId == credentials.deviceId) {
|
||||||
// ignore, it was sent by me as *
|
// ignore, it was sent by me as *
|
||||||
Timber.v("## onGossipingRequestEvent type ${event.type} ignore remote echo")
|
Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo")
|
||||||
} else {
|
} else {
|
||||||
// save in DB
|
// save in DB
|
||||||
cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
|
cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
|
||||||
|
@ -78,7 +108,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||||
IncomingRoomKeyRequest.fromEvent(event)?.let {
|
IncomingRoomKeyRequest.fromEvent(event)?.let {
|
||||||
if (event.senderId == credentials.userId && it.deviceId == credentials.deviceId) {
|
if (event.senderId == credentials.userId && it.deviceId == credentials.deviceId) {
|
||||||
// ignore, it was sent by me as *
|
// ignore, it was sent by me as *
|
||||||
Timber.v("## onGossipingRequestEvent type ${event.type} ignore remote echo")
|
Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo")
|
||||||
} else {
|
} else {
|
||||||
cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
|
cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
|
||||||
receivedGossipingRequests.add(it)
|
receivedGossipingRequests.add(it)
|
||||||
|
@ -92,7 +122,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
Timber.e("## onGossipingRequestEvent() : unsupported action ${roomKeyShare?.action}")
|
Timber.e("## GOSSIP onGossipingRequestEvent() : unsupported action ${roomKeyShare?.action}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -103,8 +133,6 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||||
* It must be called on CryptoThread
|
* It must be called on CryptoThread
|
||||||
*/
|
*/
|
||||||
fun processReceivedGossipingRequests() {
|
fun processReceivedGossipingRequests() {
|
||||||
Timber.v("## processReceivedGossipingRequests()")
|
|
||||||
|
|
||||||
val roomKeyRequestsToProcess = receivedGossipingRequests.toList()
|
val roomKeyRequestsToProcess = receivedGossipingRequests.toList()
|
||||||
receivedGossipingRequests.clear()
|
receivedGossipingRequests.clear()
|
||||||
for (request in roomKeyRequestsToProcess) {
|
for (request in roomKeyRequestsToProcess) {
|
||||||
|
@ -125,7 +153,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
receivedRequestCancellations?.forEach { request ->
|
receivedRequestCancellations?.forEach { request ->
|
||||||
Timber.v("## processReceivedGossipingRequests() : m.room_key_request cancellation $request")
|
Timber.v("## GOSSIP processReceivedGossipingRequests() : m.room_key_request cancellation $request")
|
||||||
// we should probably only notify the app of cancellations we told it
|
// we should probably only notify the app of cancellations we told it
|
||||||
// about, but we don't currently have a record of that, so we just pass
|
// about, but we don't currently have a record of that, so we just pass
|
||||||
// everything through.
|
// everything through.
|
||||||
|
@ -154,10 +182,10 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||||
val roomId = body!!.roomId
|
val roomId = body!!.roomId
|
||||||
val alg = body.algorithm
|
val alg = body.algorithm
|
||||||
|
|
||||||
Timber.v("## processIncomingRoomKeyRequest from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}")
|
Timber.v("## GOSSIP processIncomingRoomKeyRequest from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}")
|
||||||
if (userId == null || credentials.userId != userId) {
|
if (userId == null || credentials.userId != userId) {
|
||||||
// TODO: determine if we sent this device the keys already: in
|
// TODO: determine if we sent this device the keys already: in
|
||||||
Timber.w("## processReceivedGossipingRequests() : Ignoring room key request from other user for now")
|
Timber.w("## GOSSIP processReceivedGossipingRequests() : Ignoring room key request from other user for now")
|
||||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -166,18 +194,18 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||||
// the keys for the requested events, and can drop the requests.
|
// the keys for the requested events, and can drop the requests.
|
||||||
val decryptor = roomDecryptorProvider.getRoomDecryptor(roomId, alg)
|
val decryptor = roomDecryptorProvider.getRoomDecryptor(roomId, alg)
|
||||||
if (null == decryptor) {
|
if (null == decryptor) {
|
||||||
Timber.w("## processReceivedGossipingRequests() : room key request for unknown $alg in room $roomId")
|
Timber.w("## GOSSIP processReceivedGossipingRequests() : room key request for unknown $alg in room $roomId")
|
||||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!decryptor.hasKeysForKeyRequest(request)) {
|
if (!decryptor.hasKeysForKeyRequest(request)) {
|
||||||
Timber.w("## processReceivedGossipingRequests() : room key request for unknown session ${body.sessionId!!}")
|
Timber.w("## GOSSIP processReceivedGossipingRequests() : room key request for unknown session ${body.sessionId!!}")
|
||||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (credentials.deviceId == deviceId && credentials.userId == userId) {
|
if (credentials.deviceId == deviceId && credentials.userId == userId) {
|
||||||
Timber.v("## processReceivedGossipingRequests() : oneself device - ignored")
|
Timber.v("## GOSSIP processReceivedGossipingRequests() : oneself device - ignored")
|
||||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -192,13 +220,13 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||||
val device = cryptoStore.getUserDevice(userId, deviceId!!)
|
val device = cryptoStore.getUserDevice(userId, deviceId!!)
|
||||||
if (device != null) {
|
if (device != null) {
|
||||||
if (device.isVerified) {
|
if (device.isVerified) {
|
||||||
Timber.v("## processReceivedGossipingRequests() : device is already verified: sharing keys")
|
Timber.v("## GOSSIP processReceivedGossipingRequests() : device is already verified: sharing keys")
|
||||||
request.share?.run()
|
request.share?.run()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (device.isBlocked) {
|
if (device.isBlocked) {
|
||||||
Timber.v("## processReceivedGossipingRequests() : device is blocked -> ignored")
|
Timber.v("## GOSSIP processReceivedGossipingRequests() : device is blocked -> ignored")
|
||||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -219,30 +247,30 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||||
private fun processIncomingSecretShareRequest(request: IncomingSecretShareRequest) {
|
private fun processIncomingSecretShareRequest(request: IncomingSecretShareRequest) {
|
||||||
val secretName = request.secretName ?: return Unit.also {
|
val secretName = request.secretName ?: return Unit.also {
|
||||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||||
Timber.v("## processIncomingSecretShareRequest() : Missing secret name")
|
Timber.v("## GOSSIP processIncomingSecretShareRequest() : Missing secret name")
|
||||||
}
|
}
|
||||||
|
|
||||||
val userId = request.userId
|
val userId = request.userId
|
||||||
if (userId == null || credentials.userId != userId) {
|
if (userId == null || credentials.userId != userId) {
|
||||||
Timber.e("## processIncomingSecretShareRequest() : Ignoring secret share request from other users")
|
Timber.e("## GOSSIP processIncomingSecretShareRequest() : Ignoring secret share request from other users")
|
||||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val deviceId = request.deviceId
|
val deviceId = request.deviceId
|
||||||
?: return Unit.also {
|
?: return Unit.also {
|
||||||
Timber.e("## processIncomingSecretShareRequest() : Malformed request, no ")
|
Timber.e("## GOSSIP processIncomingSecretShareRequest() : Malformed request, no ")
|
||||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||||
}
|
}
|
||||||
|
|
||||||
val device = cryptoStore.getUserDevice(userId, deviceId)
|
val device = cryptoStore.getUserDevice(userId, deviceId)
|
||||||
?: return Unit.also {
|
?: return Unit.also {
|
||||||
Timber.e("## processIncomingSecretShareRequest() : Received secret share request from unknown device ${request.deviceId}")
|
Timber.e("## GOSSIP processIncomingSecretShareRequest() : Received secret share request from unknown device ${request.deviceId}")
|
||||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!device.isVerified || device.isBlocked) {
|
if (!device.isVerified || device.isBlocked) {
|
||||||
Timber.v("## processIncomingSecretShareRequest() : Ignoring secret share request from untrusted/blocked session $device")
|
Timber.v("## GOSSIP processIncomingSecretShareRequest() : Ignoring secret share request from untrusted/blocked session $device")
|
||||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -255,11 +283,20 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||||
USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user
|
USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user
|
||||||
else -> null
|
else -> null
|
||||||
}?.let { secretValue ->
|
}?.let { secretValue ->
|
||||||
// TODO check if locally trusted and not outdated
|
Timber.i("## GOSSIP processIncomingSecretShareRequest() : Sharing secret $secretName with $device locally trusted")
|
||||||
Timber.i("## processIncomingSecretShareRequest() : Sharing secret $secretName with $device locally trusted")
|
if (isDeviceLocallyVerified == true && hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId)) {
|
||||||
if (isDeviceLocallyVerified == true) {
|
val params = SendGossipWorker.Params(
|
||||||
secretSecretCryptoProvider.shareSecretWithDevice(request, secretValue)
|
sessionId = sessionId,
|
||||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED)
|
secretValue = secretValue,
|
||||||
|
request = request
|
||||||
|
)
|
||||||
|
|
||||||
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTING)
|
||||||
|
val workRequest = gossipingWorkManager.createWork<SendGossipWorker>(WorkerParamsFactory.toData(params), true)
|
||||||
|
gossipingWorkManager.postWork(workRequest)
|
||||||
|
} else {
|
||||||
|
Timber.v("## GOSSIP processIncomingSecretShareRequest() : Can't share secret $secretName with $device, verification too old")
|
||||||
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -269,7 +306,16 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
request.share = { secretValue ->
|
request.share = { secretValue ->
|
||||||
secretSecretCryptoProvider.shareSecretWithDevice(request, secretValue)
|
|
||||||
|
val params = SendGossipWorker.Params(
|
||||||
|
sessionId = userId,
|
||||||
|
secretValue = secretValue,
|
||||||
|
request = request
|
||||||
|
)
|
||||||
|
|
||||||
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTING)
|
||||||
|
val workRequest = gossipingWorkManager.createWork<SendGossipWorker>(WorkerParamsFactory.toData(params), true)
|
||||||
|
gossipingWorkManager.postWork(workRequest)
|
||||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED)
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -304,7 +350,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## onRoomKeyRequest() failed")
|
Timber.e(e, "## GOSSIP onRoomKeyRequest() failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -323,7 +369,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||||
try {
|
try {
|
||||||
listener.onRoomKeyRequestCancellation(request)
|
listener.onRoomKeyRequestCancellation(request)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## onRoomKeyRequestCancellation() failed")
|
Timber.e(e, "## GOSSIP onRoomKeyRequestCancellation() failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -340,4 +386,8 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||||
gossipingRequestListeners.remove(listener)
|
gossipingRequestListeners.remove(listener)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val FIVE_MINUTES_IN_MILLIS = 5 * 60 * 1000
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -59,9 +59,6 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
var deviceEd25519Key: String? = null
|
var deviceEd25519Key: String? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
// The OLM lib account instance.
|
|
||||||
private var olmAccount: OlmAccount? = null
|
|
||||||
|
|
||||||
// The OLM lib utility instance.
|
// The OLM lib utility instance.
|
||||||
private var olmUtility: OlmUtility? = null
|
private var olmUtility: OlmUtility? = null
|
||||||
|
|
||||||
|
@ -86,19 +83,10 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Retrieve the account from the store
|
// Retrieve the account from the store
|
||||||
olmAccount = store.getAccount()
|
try {
|
||||||
|
store.getOrCreateOlmAccount()
|
||||||
if (null == olmAccount) {
|
} catch (e: Exception) {
|
||||||
Timber.v("MXOlmDevice : create a new olm account")
|
Timber.e(e, "MXOlmDevice : cannot initialize olmAccount")
|
||||||
// Else, create it
|
|
||||||
try {
|
|
||||||
olmAccount = OlmAccount()
|
|
||||||
store.storeAccount(olmAccount!!)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "MXOlmDevice : cannot initialize olmAccount")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Timber.v("MXOlmDevice : use an existing account")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -109,13 +97,13 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
deviceCurve25519Key = olmAccount!!.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY]
|
deviceCurve25519Key = store.getOlmAccount().identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY]
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_IDENTITY_KEY} with error")
|
Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_IDENTITY_KEY} with error")
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
deviceEd25519Key = olmAccount!!.identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY]
|
deviceEd25519Key = store.getOlmAccount().identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY]
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_FINGER_PRINT_KEY} with error")
|
Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_FINGER_PRINT_KEY} with error")
|
||||||
}
|
}
|
||||||
|
@ -126,7 +114,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
*/
|
*/
|
||||||
fun getOneTimeKeys(): Map<String, Map<String, String>>? {
|
fun getOneTimeKeys(): Map<String, Map<String, String>>? {
|
||||||
try {
|
try {
|
||||||
return olmAccount!!.oneTimeKeys()
|
return store.getOlmAccount().oneTimeKeys()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## getOneTimeKeys() : failed")
|
Timber.e(e, "## getOneTimeKeys() : failed")
|
||||||
}
|
}
|
||||||
|
@ -138,14 +126,13 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
* @return The maximum number of one-time keys the olm account can store.
|
* @return The maximum number of one-time keys the olm account can store.
|
||||||
*/
|
*/
|
||||||
fun getMaxNumberOfOneTimeKeys(): Long {
|
fun getMaxNumberOfOneTimeKeys(): Long {
|
||||||
return olmAccount?.maxOneTimeKeys() ?: -1
|
return store.getOlmAccount().maxOneTimeKeys()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Release the instance
|
* Release the instance
|
||||||
*/
|
*/
|
||||||
fun release() {
|
fun release() {
|
||||||
olmAccount?.releaseAccount()
|
|
||||||
olmUtility?.releaseUtility()
|
olmUtility?.releaseUtility()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,7 +144,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
*/
|
*/
|
||||||
fun signMessage(message: String): String? {
|
fun signMessage(message: String): String? {
|
||||||
try {
|
try {
|
||||||
return olmAccount!!.signMessage(message)
|
return store.getOlmAccount().signMessage(message)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## signMessage() : failed")
|
Timber.e(e, "## signMessage() : failed")
|
||||||
}
|
}
|
||||||
|
@ -170,8 +157,8 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
*/
|
*/
|
||||||
fun markKeysAsPublished() {
|
fun markKeysAsPublished() {
|
||||||
try {
|
try {
|
||||||
olmAccount!!.markOneTimeKeysAsPublished()
|
store.getOlmAccount().markOneTimeKeysAsPublished()
|
||||||
store.storeAccount(olmAccount!!)
|
store.saveOlmAccount()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## markKeysAsPublished() : failed")
|
Timber.e(e, "## markKeysAsPublished() : failed")
|
||||||
}
|
}
|
||||||
|
@ -184,8 +171,8 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
*/
|
*/
|
||||||
fun generateOneTimeKeys(numKeys: Int) {
|
fun generateOneTimeKeys(numKeys: Int) {
|
||||||
try {
|
try {
|
||||||
olmAccount!!.generateOneTimeKeys(numKeys)
|
store.getOlmAccount().generateOneTimeKeys(numKeys)
|
||||||
store.storeAccount(olmAccount!!)
|
store.saveOlmAccount()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## generateOneTimeKeys() : failed")
|
Timber.e(e, "## generateOneTimeKeys() : failed")
|
||||||
}
|
}
|
||||||
|
@ -205,7 +192,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
|
|
||||||
try {
|
try {
|
||||||
olmSession = OlmSession()
|
olmSession = OlmSession()
|
||||||
olmSession.initOutboundSession(olmAccount!!, theirIdentityKey, theirOneTimeKey)
|
olmSession.initOutboundSession(store.getOlmAccount(), theirIdentityKey, theirOneTimeKey)
|
||||||
|
|
||||||
val olmSessionWrapper = OlmSessionWrapper(olmSession, 0)
|
val olmSessionWrapper = OlmSessionWrapper(olmSession, 0)
|
||||||
|
|
||||||
|
@ -245,7 +232,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
olmSession = OlmSession()
|
olmSession = OlmSession()
|
||||||
olmSession.initInboundSessionFrom(olmAccount!!, theirDeviceIdentityKey, ciphertext)
|
olmSession.initInboundSessionFrom(store.getOlmAccount(), theirDeviceIdentityKey, ciphertext)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## createInboundSession() : the session creation failed")
|
Timber.e(e, "## createInboundSession() : the session creation failed")
|
||||||
return null
|
return null
|
||||||
|
@ -254,8 +241,8 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
Timber.v("## createInboundSession() : sessionId: ${olmSession.sessionIdentifier()}")
|
Timber.v("## createInboundSession() : sessionId: ${olmSession.sessionIdentifier()}")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
olmAccount!!.removeOneTimeKeys(olmSession)
|
store.getOlmAccount().removeOneTimeKeys(olmSession)
|
||||||
store.storeAccount(olmAccount!!)
|
store.saveOlmAccount()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## createInboundSession() : removeOneTimeKeys failed")
|
Timber.e(e, "## createInboundSession() : removeOneTimeKeys failed")
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,27 +17,17 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.crypto
|
package im.vector.matrix.android.internal.crypto
|
||||||
|
|
||||||
import androidx.work.BackoffPolicy
|
|
||||||
import androidx.work.Data
|
|
||||||
import androidx.work.ExistingWorkPolicy
|
|
||||||
import androidx.work.ListenableWorker
|
|
||||||
import androidx.work.OneTimeWorkRequest
|
|
||||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||||
import im.vector.matrix.android.api.util.Cancelable
|
|
||||||
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
|
||||||
import im.vector.matrix.android.internal.di.SessionId
|
import im.vector.matrix.android.internal.di.SessionId
|
||||||
import im.vector.matrix.android.internal.di.WorkManagerProvider
|
|
||||||
import im.vector.matrix.android.internal.session.SessionScope
|
import im.vector.matrix.android.internal.session.SessionScope
|
||||||
import im.vector.matrix.android.internal.util.CancelableWork
|
|
||||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||||
import im.vector.matrix.android.internal.worker.startChain
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@SessionScope
|
@SessionScope
|
||||||
|
@ -46,7 +36,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
private val cryptoCoroutineScope: CoroutineScope,
|
private val cryptoCoroutineScope: CoroutineScope,
|
||||||
private val workManagerProvider: WorkManagerProvider) {
|
private val gossipingWorkManager: GossipingWorkManager) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send off a room key request, if we haven't already done so.
|
* Send off a room key request, if we haven't already done so.
|
||||||
|
@ -65,7 +55,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
|
||||||
cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients)?.let {
|
cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients)?.let {
|
||||||
// Don't resend if it's already done, you need to cancel first (reRequest)
|
// Don't resend if it's already done, you need to cancel first (reRequest)
|
||||||
if (it.state == OutgoingGossipingRequestState.SENDING || it.state == OutgoingGossipingRequestState.SENT) {
|
if (it.state == OutgoingGossipingRequestState.SENDING || it.state == OutgoingGossipingRequestState.SENT) {
|
||||||
Timber.v("## sendOutgoingRoomKeyRequest() : we already request for that session: $it")
|
Timber.v("## GOSSIP sendOutgoingRoomKeyRequest() : we already request for that session: $it")
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,7 +72,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
|
||||||
cryptoStore.getOrAddOutgoingSecretShareRequest(secretName, recipients)?.let {
|
cryptoStore.getOrAddOutgoingSecretShareRequest(secretName, recipients)?.let {
|
||||||
// TODO check if there is already one that is being sent?
|
// TODO check if there is already one that is being sent?
|
||||||
if (it.state == OutgoingGossipingRequestState.SENDING || it.state == OutgoingGossipingRequestState.SENT) {
|
if (it.state == OutgoingGossipingRequestState.SENDING || it.state == OutgoingGossipingRequestState.SENT) {
|
||||||
Timber.v("## sendOutgoingRoomKeyRequest() : we already request for that session: $it")
|
Timber.v("## GOSSIP sendSecretShareRequest() : we already request for that session: $it")
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,7 +113,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
|
||||||
val req = cryptoStore.getOutgoingRoomKeyRequest(requestBody)
|
val req = cryptoStore.getOutgoingRoomKeyRequest(requestBody)
|
||||||
?: // no request was made for this key
|
?: // no request was made for this key
|
||||||
return Unit.also {
|
return Unit.also {
|
||||||
Timber.v("## cancelRoomKeyRequest() Unknown request")
|
Timber.v("## GOSSIP cancelRoomKeyRequest() Unknown request")
|
||||||
}
|
}
|
||||||
|
|
||||||
sendOutgoingRoomKeyRequestCancellation(req, andResend)
|
sendOutgoingRoomKeyRequestCancellation(req, andResend)
|
||||||
|
@ -135,7 +125,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
|
||||||
* @param request the request
|
* @param request the request
|
||||||
*/
|
*/
|
||||||
private fun sendOutgoingGossipingRequest(request: OutgoingGossipingRequest) {
|
private fun sendOutgoingGossipingRequest(request: OutgoingGossipingRequest) {
|
||||||
Timber.v("## sendOutgoingRoomKeyRequest() : Requesting keys $request")
|
Timber.v("## GOSSIP sendOutgoingRoomKeyRequest() : Requesting keys $request")
|
||||||
|
|
||||||
val params = SendGossipRequestWorker.Params(
|
val params = SendGossipRequestWorker.Params(
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
|
@ -143,8 +133,8 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
|
||||||
secretShareRequest = request as? OutgoingSecretRequest
|
secretShareRequest = request as? OutgoingSecretRequest
|
||||||
)
|
)
|
||||||
cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.SENDING)
|
cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.SENDING)
|
||||||
val workRequest = createWork<SendGossipRequestWorker>(WorkerParamsFactory.toData(params), true)
|
val workRequest = gossipingWorkManager.createWork<SendGossipRequestWorker>(WorkerParamsFactory.toData(params), true)
|
||||||
postWork(workRequest)
|
gossipingWorkManager.postWork(workRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -157,33 +147,16 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
|
||||||
val params = CancelGossipRequestWorker.Params.fromRequest(sessionId, request)
|
val params = CancelGossipRequestWorker.Params.fromRequest(sessionId, request)
|
||||||
cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.CANCELLING)
|
cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.CANCELLING)
|
||||||
|
|
||||||
val workRequest = createWork<CancelGossipRequestWorker>(WorkerParamsFactory.toData(params), true)
|
val workRequest = gossipingWorkManager.createWork<CancelGossipRequestWorker>(WorkerParamsFactory.toData(params), true)
|
||||||
postWork(workRequest)
|
gossipingWorkManager.postWork(workRequest)
|
||||||
|
|
||||||
if (resend) {
|
if (resend) {
|
||||||
val reSendParams = SendGossipRequestWorker.Params(
|
val reSendParams = SendGossipRequestWorker.Params(
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
keyShareRequest = request.copy(requestId = LocalEcho.createLocalEchoId())
|
keyShareRequest = request.copy(requestId = LocalEcho.createLocalEchoId())
|
||||||
)
|
)
|
||||||
val reSendWorkRequest = createWork<SendGossipRequestWorker>(WorkerParamsFactory.toData(reSendParams), true)
|
val reSendWorkRequest = gossipingWorkManager.createWork<SendGossipRequestWorker>(WorkerParamsFactory.toData(reSendParams), true)
|
||||||
postWork(reSendWorkRequest)
|
gossipingWorkManager.postWork(reSendWorkRequest)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inline fun <reified W : ListenableWorker> createWork(data: Data, startChain: Boolean): OneTimeWorkRequest {
|
|
||||||
return workManagerProvider.matrixOneTimeWorkRequestBuilder<W>()
|
|
||||||
.setConstraints(WorkManagerProvider.workConstraints)
|
|
||||||
.startChain(startChain)
|
|
||||||
.setInputData(data)
|
|
||||||
.setBackoffCriteria(BackoffPolicy.LINEAR, 10_000L, TimeUnit.MILLISECONDS)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun postWork(workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND): Cancelable {
|
|
||||||
workManagerProvider.workManager
|
|
||||||
.beginUniqueWork(this::class.java.name, policy, workRequest)
|
|
||||||
.enqueue()
|
|
||||||
|
|
||||||
return CancelableWork(workManagerProvider.workManager, workRequest.id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,141 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.work.CoroutineWorker
|
||||||
|
import androidx.work.Data
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.api.auth.data.Credentials
|
||||||
|
import im.vector.matrix.android.api.failure.shouldBeRetried
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
|
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toContent
|
||||||
|
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.model.MXUsersDevicesMap
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.event.SecretSendEventContent
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||||
|
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||||
|
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||||
|
import im.vector.matrix.android.internal.worker.getSessionComponent
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class SendGossipWorker(context: Context,
|
||||||
|
params: WorkerParameters)
|
||||||
|
: CoroutineWorker(context, params) {
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class Params(
|
||||||
|
val sessionId: String,
|
||||||
|
val secretValue: String,
|
||||||
|
val request: IncomingSecretShareRequest
|
||||||
|
)
|
||||||
|
|
||||||
|
@Inject lateinit var sendToDeviceTask: SendToDeviceTask
|
||||||
|
@Inject lateinit var cryptoStore: IMXCryptoStore
|
||||||
|
@Inject lateinit var eventBus: EventBus
|
||||||
|
@Inject lateinit var credentials: Credentials
|
||||||
|
@Inject lateinit var messageEncrypter: MessageEncrypter
|
||||||
|
@Inject lateinit var ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction
|
||||||
|
|
||||||
|
override suspend fun doWork(): Result {
|
||||||
|
val errorOutputData = Data.Builder().putBoolean("failed", true).build()
|
||||||
|
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||||
|
?: return Result.success(errorOutputData)
|
||||||
|
|
||||||
|
val sessionComponent = getSessionComponent(params.sessionId)
|
||||||
|
?: return Result.success(errorOutputData).also {
|
||||||
|
// TODO, can this happen? should I update local echo?
|
||||||
|
Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}")
|
||||||
|
}
|
||||||
|
sessionComponent.inject(this)
|
||||||
|
|
||||||
|
val localId = LocalEcho.createLocalEchoId()
|
||||||
|
val eventType: String = EventType.SEND_SECRET
|
||||||
|
|
||||||
|
val toDeviceContent = SecretSendEventContent(
|
||||||
|
requestId = params.request.requestId ?: "",
|
||||||
|
secretValue = params.secretValue
|
||||||
|
)
|
||||||
|
|
||||||
|
val requestingUserId = params.request.userId ?: ""
|
||||||
|
val requestingDeviceId = params.request.deviceId ?: ""
|
||||||
|
val deviceInfo = cryptoStore.getUserDevice(requestingUserId, requestingDeviceId)
|
||||||
|
?: return Result.success(errorOutputData).also {
|
||||||
|
cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED)
|
||||||
|
Timber.e("Unknown deviceInfo, cannot send message, sessionId: ${params.request.deviceId}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
||||||
|
|
||||||
|
val devicesByUser = mapOf(requestingUserId to listOf(deviceInfo))
|
||||||
|
val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
||||||
|
val olmSessionResult = usersDeviceMap.getObject(requestingUserId, requestingDeviceId)
|
||||||
|
if (olmSessionResult?.sessionId == null) {
|
||||||
|
// no session with this device, probably because there
|
||||||
|
// were no one-time keys.
|
||||||
|
return Result.success(errorOutputData).also {
|
||||||
|
cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED)
|
||||||
|
Timber.e("no session with this device, probably because there were no one-time keys.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val payloadJson = mapOf(
|
||||||
|
"type" to EventType.SEND_SECRET,
|
||||||
|
"content" to toDeviceContent.toContent()
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
||||||
|
sendToDeviceMap.setObject(requestingUserId, requestingDeviceId, encodedPayload)
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
Timber.e("## Fail to encrypt gossip + ${failure.localizedMessage}")
|
||||||
|
}
|
||||||
|
|
||||||
|
cryptoStore.saveGossipingEvent(Event(
|
||||||
|
type = eventType,
|
||||||
|
content = toDeviceContent.toContent(),
|
||||||
|
senderId = credentials.userId
|
||||||
|
).also {
|
||||||
|
it.ageLocalTs = System.currentTimeMillis()
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
sendToDeviceTask.execute(
|
||||||
|
SendToDeviceTask.Params(
|
||||||
|
eventType = EventType.ENCRYPTED,
|
||||||
|
contentMap = sendToDeviceMap,
|
||||||
|
transactionId = localId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.ACCEPTED)
|
||||||
|
return Result.success()
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
return if (exception.shouldBeRetried()) {
|
||||||
|
Result.retry()
|
||||||
|
} else {
|
||||||
|
cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED)
|
||||||
|
Result.success(errorOutputData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,74 +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.crypto
|
|
||||||
|
|
||||||
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.internal.crypto.actions.MessageEncrypter
|
|
||||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmDecryptionFactory
|
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
|
||||||
import im.vector.matrix.android.internal.crypto.model.event.SecretSendEventContent
|
|
||||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
|
||||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal class ShareSecretCryptoProvider @Inject constructor(
|
|
||||||
val messageEncrypter: MessageEncrypter,
|
|
||||||
val sendToDeviceTask: SendToDeviceTask,
|
|
||||||
val deviceListManager: DeviceListManager,
|
|
||||||
private val olmDecryptionFactory: MXOlmDecryptionFactory,
|
|
||||||
val cryptoCoroutineScope: CoroutineScope,
|
|
||||||
val cryptoStore: IMXCryptoStore,
|
|
||||||
val coroutineDispatchers: MatrixCoroutineDispatchers
|
|
||||||
) {
|
|
||||||
fun shareSecretWithDevice(request: IncomingSecretShareRequest, secretValue: String) {
|
|
||||||
val userId = request.userId ?: return
|
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
|
||||||
runCatching { deviceListManager.downloadKeys(listOf(userId), false) }
|
|
||||||
.mapCatching {
|
|
||||||
val deviceId = request.deviceId
|
|
||||||
val deviceInfo = cryptoStore.getUserDevice(userId, deviceId ?: "") ?: throw RuntimeException()
|
|
||||||
|
|
||||||
Timber.i("## shareSecretWithDevice() : sharing secret ${request.secretName} with device $userId:$deviceId")
|
|
||||||
|
|
||||||
val payloadJson = mutableMapOf<String, Any>("type" to EventType.SEND_SECRET)
|
|
||||||
payloadJson["content"] = SecretSendEventContent(
|
|
||||||
requestId = request.requestId ?: "",
|
|
||||||
secretValue = secretValue
|
|
||||||
)
|
|
||||||
|
|
||||||
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
|
||||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
|
||||||
sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
|
|
||||||
Timber.i("## shareSecretWithDevice() : sending to $userId:$deviceId")
|
|
||||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
|
||||||
sendToDeviceTask.execute(sendToDeviceParams)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun decryptEvent(event: Event): MXEventDecryptionResult {
|
|
||||||
return runBlocking(coroutineDispatchers.crypto) {
|
|
||||||
olmDecryptionFactory.create().decryptEvent(event, ShareSecretCryptoProvider::class.java.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -149,7 +149,7 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||||
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) {
|
||||||
mapOf(
|
mapOf(
|
||||||
userId to listOf("*")
|
userId to listOf("*")
|
||||||
)
|
)
|
||||||
|
|
|
@ -25,7 +25,6 @@ import im.vector.matrix.android.api.util.Optional
|
||||||
import im.vector.matrix.android.internal.crypto.DeviceListManager
|
import im.vector.matrix.android.internal.crypto.DeviceListManager
|
||||||
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
||||||
import im.vector.matrix.android.internal.crypto.MyDeviceInfoHolder
|
import im.vector.matrix.android.internal.crypto.MyDeviceInfoHolder
|
||||||
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
|
|
||||||
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.KeyUsage
|
import im.vector.matrix.android.internal.crypto.model.KeyUsage
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.UploadSignatureQueryBuilder
|
import im.vector.matrix.android.internal.crypto.model.rest.UploadSignatureQueryBuilder
|
||||||
|
@ -62,7 +61,6 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
private val cryptoCoroutineScope: CoroutineScope,
|
private val cryptoCoroutineScope: CoroutineScope,
|
||||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
|
||||||
private val eventBus: EventBus) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener {
|
private val eventBus: EventBus) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener {
|
||||||
|
|
||||||
private var olmUtility: OlmUtility? = null
|
private var olmUtility: OlmUtility? = null
|
||||||
|
@ -599,6 +597,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
|
|
||||||
override fun canCrossSign(): Boolean {
|
override fun canCrossSign(): Boolean {
|
||||||
return checkSelfTrust().isVerified() && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null
|
return checkSelfTrust().isVerified() && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null
|
||||||
|
&& cryptoStore.getCrossSigningPrivateKeys()?.user != null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun trustUser(otherUserId: String, callback: MatrixCallback<Unit>) {
|
override fun trustUser(otherUserId: String, callback: MatrixCallback<Unit>) {
|
||||||
|
@ -770,7 +769,12 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
Timber.v("## CrossSigning - update trust for $otherUserId , verified=${it.isVerified()}")
|
Timber.v("## CrossSigning - update trust for $otherUserId , verified=${it.isVerified()}")
|
||||||
setUserKeysAsTrusted(otherUserId, it.isVerified())
|
setUserKeysAsTrusted(otherUserId, it.isVerified())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// now check device trust
|
||||||
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
|
userIds.forEach { otherUserId ->
|
||||||
// TODO if my keys have changes, i should recheck all devices of all users?
|
// TODO if my keys have changes, i should recheck all devices of all users?
|
||||||
val devices = cryptoStore.getUserDeviceList(otherUserId)
|
val devices = cryptoStore.getUserDeviceList(otherUserId)
|
||||||
devices?.forEach { device ->
|
devices?.forEach { device ->
|
||||||
|
@ -791,24 +795,22 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) {
|
private fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) {
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
val currentTrust = cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted()
|
||||||
val currentTrust = cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted()
|
cryptoStore.setUserKeysAsTrusted(otherUserId, trusted)
|
||||||
cryptoStore.setUserKeysAsTrusted(otherUserId, trusted)
|
// If it's me, recheck trust of all users and devices?
|
||||||
// If it's me, recheck trust of all users and devices?
|
val users = ArrayList<String>()
|
||||||
val users = ArrayList<String>()
|
if (otherUserId == userId && currentTrust != trusted) {
|
||||||
if (otherUserId == userId && currentTrust != trusted) {
|
|
||||||
// reRequestAllPendingRoomKeyRequest()
|
// reRequestAllPendingRoomKeyRequest()
|
||||||
cryptoStore.updateUsersTrust {
|
cryptoStore.updateUsersTrust {
|
||||||
users.add(it)
|
users.add(it)
|
||||||
checkUserTrust(it).isVerified()
|
checkUserTrust(it).isVerified()
|
||||||
}
|
}
|
||||||
|
|
||||||
users.forEach {
|
users.forEach {
|
||||||
cryptoStore.getUserDeviceList(it)?.forEach { device ->
|
cryptoStore.getUserDeviceList(it)?.forEach { device ->
|
||||||
val updatedTrust = checkDeviceTrust(it, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false)
|
val updatedTrust = checkDeviceTrust(it, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false)
|
||||||
Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust")
|
Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust")
|
||||||
cryptoStore.setDeviceTrust(it, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified())
|
cryptoStore.setDeviceTrust(it, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified())
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -272,7 +272,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
||||||
val ivParameterSpec = IvParameterSpec(iv)
|
val ivParameterSpec = IvParameterSpec(iv)
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
|
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
|
||||||
// secret are not that big, just do Final
|
// secret are not that big, just do Final
|
||||||
val cipherBytes = cipher.doFinal(clearDataBase64.fromBase64())
|
val cipherBytes = cipher.doFinal(clearDataBase64.toByteArray())
|
||||||
require(cipherBytes.isNotEmpty())
|
require(cipherBytes.isNotEmpty())
|
||||||
|
|
||||||
val macKeySpec = SecretKeySpec(macKey, "HmacSHA256")
|
val macKeySpec = SecretKeySpec(macKey, "HmacSHA256")
|
||||||
|
@ -303,6 +303,15 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
||||||
|
|
||||||
val cipherRawBytes = cipherContent.ciphertext?.fromBase64() ?: throw SharedSecretStorageError.BadCipherText
|
val cipherRawBytes = cipherContent.ciphertext?.fromBase64() ?: throw SharedSecretStorageError.BadCipherText
|
||||||
|
|
||||||
|
// Check Signature
|
||||||
|
val macKeySpec = SecretKeySpec(macKey, "HmacSHA256")
|
||||||
|
val mac = Mac.getInstance("HmacSHA256").apply { init(macKeySpec) }
|
||||||
|
val digest = mac.doFinal(cipherRawBytes)
|
||||||
|
|
||||||
|
if (!cipherContent.mac?.fromBase64()?.contentEquals(digest).orFalse()) {
|
||||||
|
throw SharedSecretStorageError.BadMac
|
||||||
|
}
|
||||||
|
|
||||||
val cipher = Cipher.getInstance("AES/CTR/NoPadding")
|
val cipher = Cipher.getInstance("AES/CTR/NoPadding")
|
||||||
|
|
||||||
val secretKeySpec = SecretKeySpec(aesKey, "AES")
|
val secretKeySpec = SecretKeySpec(aesKey, "AES")
|
||||||
|
@ -313,17 +322,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
||||||
|
|
||||||
require(decryptedSecret.isNotEmpty())
|
require(decryptedSecret.isNotEmpty())
|
||||||
|
|
||||||
// Check Signature
|
return String(decryptedSecret, Charsets.UTF_8)
|
||||||
val macKeySpec = SecretKeySpec(macKey, "HmacSHA256")
|
|
||||||
val mac = Mac.getInstance("HmacSHA256").apply { init(macKeySpec) }
|
|
||||||
val digest = mac.doFinal(cipherRawBytes)
|
|
||||||
|
|
||||||
if (!cipherContent.mac?.fromBase64()?.contentEquals(digest).orFalse()) {
|
|
||||||
throw SharedSecretStorageError.BadMac
|
|
||||||
} else {
|
|
||||||
// we are good
|
|
||||||
return decryptedSecret.toBase64NoPadding()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAlgorithmsForSecret(name: String): List<KeyInfoResult> {
|
override fun getAlgorithmsForSecret(name: String): List<KeyInfoResult> {
|
||||||
|
|
|
@ -49,7 +49,9 @@ internal interface IMXCryptoStore {
|
||||||
/**
|
/**
|
||||||
* @return the olm account
|
* @return the olm account
|
||||||
*/
|
*/
|
||||||
fun getAccount(): OlmAccount?
|
fun getOlmAccount(): OlmAccount
|
||||||
|
|
||||||
|
fun getOrCreateOlmAccount(): OlmAccount
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the known inbound group sessions.
|
* Retrieve the known inbound group sessions.
|
||||||
|
@ -159,7 +161,7 @@ internal interface IMXCryptoStore {
|
||||||
*
|
*
|
||||||
* @param account the account to save
|
* @param account the account to save
|
||||||
*/
|
*/
|
||||||
fun storeAccount(account: OlmAccount)
|
fun saveOlmAccount()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store a device for a user.
|
* Store a device for a user.
|
||||||
|
|
|
@ -122,27 +122,7 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
.setRealmConfiguration(realmConfiguration)
|
.setRealmConfiguration(realmConfiguration)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
/* ==========================================================================================
|
init {
|
||||||
* Other data
|
|
||||||
* ========================================================================================== */
|
|
||||||
|
|
||||||
override fun hasData(): Boolean {
|
|
||||||
return doWithRealm(realmConfiguration) {
|
|
||||||
!it.isEmpty
|
|
||||||
// Check if there is a MetaData object
|
|
||||||
&& it.where<CryptoMetadataEntity>().count() > 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun deleteStore() {
|
|
||||||
doRealmTransaction(realmConfiguration) {
|
|
||||||
it.deleteAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun open() {
|
|
||||||
realmLocker = Realm.getInstance(realmConfiguration)
|
|
||||||
|
|
||||||
// Ensure CryptoMetadataEntity is inserted in DB
|
// Ensure CryptoMetadataEntity is inserted in DB
|
||||||
doRealmTransaction(realmConfiguration) { realm ->
|
doRealmTransaction(realmConfiguration) { realm ->
|
||||||
var currentMetadata = realm.where<CryptoMetadataEntity>().findFirst()
|
var currentMetadata = realm.where<CryptoMetadataEntity>().findFirst()
|
||||||
|
@ -173,6 +153,27 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* ==========================================================================================
|
||||||
|
* Other data
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
|
override fun hasData(): Boolean {
|
||||||
|
return doWithRealm(realmConfiguration) {
|
||||||
|
!it.isEmpty
|
||||||
|
// Check if there is a MetaData object
|
||||||
|
&& it.where<CryptoMetadataEntity>().count() > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteStore() {
|
||||||
|
doRealmTransaction(realmConfiguration) {
|
||||||
|
it.deleteAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun open() {
|
||||||
|
realmLocker = Realm.getInstance(realmConfiguration)
|
||||||
|
}
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
olmSessionsToRelease.forEach {
|
olmSessionsToRelease.forEach {
|
||||||
|
@ -203,20 +204,31 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
}?.deviceId ?: ""
|
}?.deviceId ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun storeAccount(account: OlmAccount) {
|
override fun saveOlmAccount() {
|
||||||
olmAccount = account
|
|
||||||
|
|
||||||
doRealmTransaction(realmConfiguration) {
|
doRealmTransaction(realmConfiguration) {
|
||||||
it.where<CryptoMetadataEntity>().findFirst()?.putOlmAccount(account)
|
it.where<CryptoMetadataEntity>().findFirst()?.putOlmAccount(olmAccount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAccount(): OlmAccount? {
|
override fun getOlmAccount(): OlmAccount {
|
||||||
if (olmAccount == null) {
|
return olmAccount!!
|
||||||
olmAccount = doRealmQueryAndCopy(realmConfiguration) { it.where<CryptoMetadataEntity>().findFirst() }?.getOlmAccount()
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return olmAccount
|
override fun getOrCreateOlmAccount(): OlmAccount {
|
||||||
|
doRealmTransaction(realmConfiguration) {
|
||||||
|
val metaData = it.where<CryptoMetadataEntity>().findFirst()
|
||||||
|
val existing = metaData!!.getOlmAccount()
|
||||||
|
if (existing == null) {
|
||||||
|
Timber.d("## Crypto Creating olm account")
|
||||||
|
val created = OlmAccount()
|
||||||
|
metaData.putOlmAccount(created)
|
||||||
|
olmAccount = created
|
||||||
|
} else {
|
||||||
|
Timber.d("## Crypto Access existing account")
|
||||||
|
olmAccount = existing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return olmAccount!!
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun storeUserDevice(userId: String?, deviceInfo: CryptoDeviceInfo?) {
|
override fun storeUserDevice(userId: String?, deviceInfo: CryptoDeviceInfo?) {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import im.vector.matrix.android.internal.network.executeRequest
|
||||||
import im.vector.matrix.android.internal.task.Task
|
import im.vector.matrix.android.internal.task.Task
|
||||||
import im.vector.matrix.android.internal.util.convertToUTF8
|
import im.vector.matrix.android.internal.util.convertToUTF8
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal interface UploadKeysTask : Task<UploadKeysTask.Params, KeysUploadResponse> {
|
internal interface UploadKeysTask : Task<UploadKeysTask.Params, KeysUploadResponse> {
|
||||||
|
@ -50,6 +51,8 @@ internal class DefaultUploadKeysTask @Inject constructor(
|
||||||
oneTimeKeys = params.oneTimeKeys
|
oneTimeKeys = params.oneTimeKeys
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Timber.i("## Uploading device keys -> $body")
|
||||||
|
|
||||||
return executeRequest(eventBus) {
|
return executeRequest(eventBus) {
|
||||||
apiCall = if (encodedDeviceId.isBlank()) {
|
apiCall = if (encodedDeviceId.isBlank()) {
|
||||||
cryptoApi.uploadKeys(body)
|
cryptoApi.uploadKeys(body)
|
||||||
|
|
|
@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerif
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.SasMode
|
import im.vector.matrix.android.api.session.crypto.verification.SasMode
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
||||||
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.internal.crypto.IncomingGossipingRequestManager
|
||||||
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
|
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
|
||||||
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.store.IMXCryptoStore
|
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||||
|
@ -35,6 +36,7 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
crossSigningService: CrossSigningService,
|
crossSigningService: CrossSigningService,
|
||||||
outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||||
|
incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
||||||
deviceFingerprint: String,
|
deviceFingerprint: String,
|
||||||
transactionId: String,
|
transactionId: String,
|
||||||
otherUserID: String,
|
otherUserID: String,
|
||||||
|
@ -46,6 +48,7 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
|
||||||
cryptoStore,
|
cryptoStore,
|
||||||
crossSigningService,
|
crossSigningService,
|
||||||
outgoingGossipingRequestManager,
|
outgoingGossipingRequestManager,
|
||||||
|
incomingGossipingRequestManager,
|
||||||
deviceFingerprint,
|
deviceFingerprint,
|
||||||
transactionId,
|
transactionId,
|
||||||
otherUserID,
|
otherUserID,
|
||||||
|
|
|
@ -20,6 +20,7 @@ import im.vector.matrix.android.api.session.crypto.verification.CancelCode
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.OutgoingSasVerificationTransaction
|
import im.vector.matrix.android.api.session.crypto.verification.OutgoingSasVerificationTransaction
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
||||||
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.internal.crypto.IncomingGossipingRequestManager
|
||||||
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
|
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
|
||||||
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.store.IMXCryptoStore
|
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||||
|
@ -32,6 +33,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
|
||||||
cryptoStore: IMXCryptoStore,
|
cryptoStore: IMXCryptoStore,
|
||||||
crossSigningService: CrossSigningService,
|
crossSigningService: CrossSigningService,
|
||||||
outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||||
|
incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
||||||
deviceFingerprint: String,
|
deviceFingerprint: String,
|
||||||
transactionId: String,
|
transactionId: String,
|
||||||
otherUserId: String,
|
otherUserId: String,
|
||||||
|
@ -43,6 +45,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
|
||||||
cryptoStore,
|
cryptoStore,
|
||||||
crossSigningService,
|
crossSigningService,
|
||||||
outgoingGossipingRequestManager,
|
outgoingGossipingRequestManager,
|
||||||
|
incomingGossipingRequestManager,
|
||||||
deviceFingerprint,
|
deviceFingerprint,
|
||||||
transactionId,
|
transactionId,
|
||||||
otherUserId,
|
otherUserId,
|
||||||
|
|
|
@ -50,6 +50,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageVerificati
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationStartContent
|
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationStartContent
|
||||||
import im.vector.matrix.android.api.session.room.model.message.ValidVerificationDone
|
import im.vector.matrix.android.api.session.room.model.message.ValidVerificationDone
|
||||||
import im.vector.matrix.android.internal.crypto.DeviceListManager
|
import im.vector.matrix.android.internal.crypto.DeviceListManager
|
||||||
|
import im.vector.matrix.android.internal.crypto.IncomingGossipingRequestManager
|
||||||
import im.vector.matrix.android.internal.crypto.MyDeviceInfoHolder
|
import im.vector.matrix.android.internal.crypto.MyDeviceInfoHolder
|
||||||
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
|
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
|
||||||
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
|
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
|
||||||
|
@ -90,6 +91,7 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
@DeviceId private val deviceId: String?,
|
@DeviceId private val deviceId: String?,
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||||
|
private val incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
||||||
private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
|
private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
|
||||||
private val deviceListManager: DeviceListManager,
|
private val deviceListManager: DeviceListManager,
|
||||||
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
||||||
|
@ -454,7 +456,7 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
private suspend fun handleStart(otherUserId: String?,
|
private suspend fun handleStart(otherUserId: String?,
|
||||||
startReq: ValidVerificationInfoStart,
|
startReq: ValidVerificationInfoStart,
|
||||||
txConfigure: (DefaultVerificationTransaction) -> Unit): CancelCode? {
|
txConfigure: (DefaultVerificationTransaction) -> Unit): CancelCode? {
|
||||||
Timber.d("## SAS onStartRequestReceived ${startReq.transactionId}")
|
Timber.d("## SAS onStartRequestReceived $startReq")
|
||||||
if (otherUserId?.let { checkKeysAreDownloaded(it, startReq.fromDevice) } != null) {
|
if (otherUserId?.let { checkKeysAreDownloaded(it, startReq.fromDevice) } != null) {
|
||||||
val tid = startReq.transactionId
|
val tid = startReq.transactionId
|
||||||
var existing = getExistingTransaction(otherUserId, tid)
|
var existing = getExistingTransaction(otherUserId, tid)
|
||||||
|
@ -466,15 +468,17 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
// smallest is used, and the other m.key.verification.start event is ignored.
|
// smallest is used, and the other m.key.verification.start event is ignored.
|
||||||
// In the case of a single user verifying two of their devices, the device ID is
|
// In the case of a single user verifying two of their devices, the device ID is
|
||||||
// compared instead .
|
// compared instead .
|
||||||
if (existing != null && !existing.isIncoming) {
|
if (existing is DefaultOutgoingSASDefaultVerificationTransaction) {
|
||||||
val readyRequest = getExistingVerificationRequest(otherUserId, tid)
|
val readyRequest = getExistingVerificationRequest(otherUserId, tid)
|
||||||
if (readyRequest?.isReady == true) {
|
if (readyRequest?.isReady == true) {
|
||||||
if (isOtherPrioritary(otherUserId, existing.otherDeviceId ?: "")) {
|
if (isOtherPrioritary(otherUserId, existing.otherDeviceId ?: "")) {
|
||||||
|
Timber.d("## SAS concurrent start isOtherPrioritary, clear")
|
||||||
// The other is prioritary!
|
// The other is prioritary!
|
||||||
// I should replace my outgoing with an incoming
|
// I should replace my outgoing with an incoming
|
||||||
removeTransaction(otherUserId, tid)
|
removeTransaction(otherUserId, tid)
|
||||||
existing = null
|
existing = null
|
||||||
} else {
|
} else {
|
||||||
|
Timber.d("## SAS concurrent start i am prioritary, ignore")
|
||||||
// i am prioritary, ignore this start event!
|
// i am prioritary, ignore this start event!
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -530,6 +534,7 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
cryptoStore,
|
cryptoStore,
|
||||||
crossSigningService,
|
crossSigningService,
|
||||||
outgoingGossipingRequestManager,
|
outgoingGossipingRequestManager,
|
||||||
|
incomingGossipingRequestManager,
|
||||||
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
||||||
startReq.transactionId,
|
startReq.transactionId,
|
||||||
otherUserId,
|
otherUserId,
|
||||||
|
@ -544,7 +549,7 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
existing.onStartReceived(startReq)
|
existing.onStartReceived(startReq)
|
||||||
return null
|
return null
|
||||||
} else {
|
} else {
|
||||||
Timber.w("## SAS onStartRequestReceived - unexpected message ${startReq.transactionId}")
|
Timber.w("## SAS onStartRequestReceived - unexpected message ${startReq.transactionId} / $existing")
|
||||||
return CancelCode.UnexpectedMessage
|
return CancelCode.UnexpectedMessage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -754,6 +759,7 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
|
|
||||||
private suspend fun onReadyReceived(event: Event) {
|
private suspend fun onReadyReceived(event: Event) {
|
||||||
val readyReq = event.getClearContent().toModel<KeyVerificationReady>()?.asValidObject()
|
val readyReq = event.getClearContent().toModel<KeyVerificationReady>()?.asValidObject()
|
||||||
|
Timber.v("## SAS onReadyReceived $readyReq")
|
||||||
|
|
||||||
if (readyReq == null || event.senderId == null) {
|
if (readyReq == null || event.senderId == null) {
|
||||||
// ignore
|
// ignore
|
||||||
|
@ -834,17 +840,18 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
if (readyReq.methods.contains(VERIFICATION_METHOD_RECIPROCATE)) {
|
if (readyReq.methods.contains(VERIFICATION_METHOD_RECIPROCATE)) {
|
||||||
// Create the pending transaction
|
// Create the pending transaction
|
||||||
val tx = DefaultQrCodeVerificationTransaction(
|
val tx = DefaultQrCodeVerificationTransaction(
|
||||||
setDeviceVerificationAction,
|
setDeviceVerificationAction = setDeviceVerificationAction,
|
||||||
readyReq.transactionId,
|
transactionId = readyReq.transactionId,
|
||||||
senderId,
|
otherUserId = senderId,
|
||||||
readyReq.fromDevice,
|
otherDeviceId = readyReq.fromDevice,
|
||||||
crossSigningService,
|
crossSigningService = crossSigningService,
|
||||||
outgoingGossipingRequestManager,
|
outgoingGossipingRequestManager = outgoingGossipingRequestManager,
|
||||||
cryptoStore,
|
incomingGossipingRequestManager = incomingGossipingRequestManager,
|
||||||
qrCodeData,
|
cryptoStore = cryptoStore,
|
||||||
userId,
|
qrCodeData = qrCodeData,
|
||||||
deviceId ?: "",
|
userId = userId,
|
||||||
false)
|
deviceId = deviceId ?: "",
|
||||||
|
isIncoming = false)
|
||||||
|
|
||||||
tx.transport = transportCreator.invoke(tx)
|
tx.transport = transportCreator.invoke(tx)
|
||||||
|
|
||||||
|
@ -1001,13 +1008,11 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addTransaction(tx: DefaultVerificationTransaction) {
|
private fun addTransaction(tx: DefaultVerificationTransaction) {
|
||||||
tx.otherUserId.let { otherUserId ->
|
synchronized(txMap) {
|
||||||
synchronized(txMap) {
|
val txInnerMap = txMap.getOrPut(tx.otherUserId) { HashMap() }
|
||||||
val txInnerMap = txMap.getOrPut(otherUserId) { HashMap() }
|
txInnerMap[tx.transactionId] = tx
|
||||||
txInnerMap[tx.transactionId] = tx
|
dispatchTxAdded(tx)
|
||||||
dispatchTxAdded(tx)
|
tx.addListener(this)
|
||||||
tx.addListener(this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1022,6 +1027,7 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
cryptoStore,
|
cryptoStore,
|
||||||
crossSigningService,
|
crossSigningService,
|
||||||
outgoingGossipingRequestManager,
|
outgoingGossipingRequestManager,
|
||||||
|
incomingGossipingRequestManager,
|
||||||
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
||||||
txID,
|
txID,
|
||||||
otherUserId,
|
otherUserId,
|
||||||
|
@ -1096,6 +1102,18 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
return verificationRequest
|
return verificationRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun cancelVerificationRequest(request: PendingVerificationRequest) {
|
||||||
|
if (request.roomId != null) {
|
||||||
|
val transport = verificationTransportRoomMessageFactory.createTransport(request.roomId, null)
|
||||||
|
transport.cancelTransaction(request.transactionId ?: "", request.otherUserId, null, CancelCode.User)
|
||||||
|
} else {
|
||||||
|
val transport = verificationTransportToDeviceFactory.createTransport(null)
|
||||||
|
request.targetDevices?.forEach { deviceId ->
|
||||||
|
transport.cancelTransaction(request.transactionId ?: "", request.otherUserId, deviceId, CancelCode.User)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun requestKeyVerification(methods: List<VerificationMethod>, otherUserId: String, otherDevices: List<String>?): PendingVerificationRequest {
|
override fun requestKeyVerification(methods: List<VerificationMethod>, otherUserId: String, otherDevices: List<String>?): PendingVerificationRequest {
|
||||||
// TODO refactor this with the DM one
|
// TODO refactor this with the DM one
|
||||||
Timber.i("## Requesting verification to user: $otherUserId with device list $otherDevices")
|
Timber.i("## Requesting verification to user: $otherUserId with device list $otherDevices")
|
||||||
|
@ -1198,6 +1216,7 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
cryptoStore,
|
cryptoStore,
|
||||||
crossSigningService,
|
crossSigningService,
|
||||||
outgoingGossipingRequestManager,
|
outgoingGossipingRequestManager,
|
||||||
|
incomingGossipingRequestManager,
|
||||||
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
||||||
transactionId,
|
transactionId,
|
||||||
otherUserId,
|
otherUserId,
|
||||||
|
@ -1329,17 +1348,18 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
if (VERIFICATION_METHOD_RECIPROCATE in result) {
|
if (VERIFICATION_METHOD_RECIPROCATE in result) {
|
||||||
// Create the pending transaction
|
// Create the pending transaction
|
||||||
val tx = DefaultQrCodeVerificationTransaction(
|
val tx = DefaultQrCodeVerificationTransaction(
|
||||||
setDeviceVerificationAction,
|
setDeviceVerificationAction = setDeviceVerificationAction,
|
||||||
transactionId,
|
transactionId = transactionId,
|
||||||
otherUserId,
|
otherUserId = otherUserId,
|
||||||
otherDeviceId,
|
otherDeviceId = otherDeviceId,
|
||||||
crossSigningService,
|
crossSigningService = crossSigningService,
|
||||||
outgoingGossipingRequestManager,
|
outgoingGossipingRequestManager = outgoingGossipingRequestManager,
|
||||||
cryptoStore,
|
incomingGossipingRequestManager = incomingGossipingRequestManager,
|
||||||
qrCodeData,
|
cryptoStore = cryptoStore,
|
||||||
userId,
|
qrCodeData = qrCodeData,
|
||||||
deviceId ?: "",
|
userId = userId,
|
||||||
false)
|
deviceId = deviceId ?: "",
|
||||||
|
isIncoming = false)
|
||||||
|
|
||||||
tx.transport = transportCreator.invoke(tx)
|
tx.transport = transportCreator.invoke(tx)
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY
|
||||||
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
||||||
|
import im.vector.matrix.android.internal.crypto.IncomingGossipingRequestManager
|
||||||
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
|
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
|
||||||
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.crosssigning.DeviceTrustLevel
|
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||||
|
@ -33,6 +34,7 @@ internal abstract class DefaultVerificationTransaction(
|
||||||
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
||||||
private val crossSigningService: CrossSigningService,
|
private val crossSigningService: CrossSigningService,
|
||||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||||
|
private val incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
||||||
private val userId: String,
|
private val userId: String,
|
||||||
override val transactionId: String,
|
override val transactionId: String,
|
||||||
override val otherUserId: String,
|
override val otherUserId: String,
|
||||||
|
@ -86,6 +88,8 @@ internal abstract class DefaultVerificationTransaction(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (otherUserId == userId) {
|
if (otherUserId == userId) {
|
||||||
|
incomingGossipingRequestManager.onVerificationCompleteForDevice(otherDeviceId!!)
|
||||||
|
|
||||||
// If me it's reasonable to sign and upload the device signature
|
// If me it's reasonable to sign and upload the device signature
|
||||||
// Notice that i might not have the private keys, so may not be able to do it
|
// Notice that i might not have the private keys, so may not be able to do it
|
||||||
crossSigningService.trustDevice(otherDeviceId!!, object : MatrixCallback<Unit> {
|
crossSigningService.trustDevice(otherDeviceId!!, object : MatrixCallback<Unit> {
|
||||||
|
@ -96,7 +100,7 @@ internal abstract class DefaultVerificationTransaction(
|
||||||
}
|
}
|
||||||
|
|
||||||
transport.done(transactionId) {
|
transport.done(transactionId) {
|
||||||
if (otherUserId == userId) {
|
if (otherUserId == userId && !crossSigningService.canCrossSign()) {
|
||||||
outgoingGossipingRequestManager.sendSecretShareRequest(SELF_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*")))
|
outgoingGossipingRequestManager.sendSecretShareRequest(SELF_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*")))
|
||||||
outgoingGossipingRequestManager.sendSecretShareRequest(USER_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*")))
|
outgoingGossipingRequestManager.sendSecretShareRequest(USER_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*")))
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.crypto.verification.SasMode
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.SasVerificationTransaction
|
import im.vector.matrix.android.api.session.crypto.verification.SasVerificationTransaction
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
||||||
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.internal.crypto.IncomingGossipingRequestManager
|
||||||
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
|
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
|
||||||
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.model.MXKey
|
import im.vector.matrix.android.internal.crypto.model.MXKey
|
||||||
|
@ -44,6 +45,7 @@ internal abstract class SASDefaultVerificationTransaction(
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
crossSigningService: CrossSigningService,
|
crossSigningService: CrossSigningService,
|
||||||
outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||||
|
incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
||||||
private val deviceFingerprint: String,
|
private val deviceFingerprint: String,
|
||||||
transactionId: String,
|
transactionId: String,
|
||||||
otherUserId: String,
|
otherUserId: String,
|
||||||
|
@ -53,6 +55,7 @@ internal abstract class SASDefaultVerificationTransaction(
|
||||||
setDeviceVerificationAction,
|
setDeviceVerificationAction,
|
||||||
crossSigningService,
|
crossSigningService,
|
||||||
outgoingGossipingRequestManager,
|
outgoingGossipingRequestManager,
|
||||||
|
incomingGossipingRequestManager,
|
||||||
userId,
|
userId,
|
||||||
transactionId,
|
transactionId,
|
||||||
otherUserId,
|
otherUserId,
|
||||||
|
|
|
@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.crypto.verification.CancelCode
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.QrCodeVerificationTransaction
|
import im.vector.matrix.android.api.session.crypto.verification.QrCodeVerificationTransaction
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
||||||
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.internal.crypto.IncomingGossipingRequestManager
|
||||||
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
|
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
|
||||||
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.crosssigning.fromBase64
|
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64
|
||||||
|
@ -38,6 +39,7 @@ internal class DefaultQrCodeVerificationTransaction(
|
||||||
override var otherDeviceId: String?,
|
override var otherDeviceId: String?,
|
||||||
private val crossSigningService: CrossSigningService,
|
private val crossSigningService: CrossSigningService,
|
||||||
outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||||
|
incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
// Not null only if other user is able to scan QR code
|
// Not null only if other user is able to scan QR code
|
||||||
private val qrCodeData: QrCodeData?,
|
private val qrCodeData: QrCodeData?,
|
||||||
|
@ -48,6 +50,7 @@ internal class DefaultQrCodeVerificationTransaction(
|
||||||
setDeviceVerificationAction,
|
setDeviceVerificationAction,
|
||||||
crossSigningService,
|
crossSigningService,
|
||||||
outgoingGossipingRequestManager,
|
outgoingGossipingRequestManager,
|
||||||
|
incomingGossipingRequestManager,
|
||||||
userId,
|
userId,
|
||||||
transactionId,
|
transactionId,
|
||||||
otherUserId,
|
otherUserId,
|
||||||
|
|
|
@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.android.internal.crypto.CancelGossipRequestWorker
|
import im.vector.matrix.android.internal.crypto.CancelGossipRequestWorker
|
||||||
import im.vector.matrix.android.internal.crypto.CryptoModule
|
import im.vector.matrix.android.internal.crypto.CryptoModule
|
||||||
import im.vector.matrix.android.internal.crypto.SendGossipRequestWorker
|
import im.vector.matrix.android.internal.crypto.SendGossipRequestWorker
|
||||||
|
import im.vector.matrix.android.internal.crypto.SendGossipWorker
|
||||||
import im.vector.matrix.android.internal.crypto.verification.SendVerificationMessageWorker
|
import im.vector.matrix.android.internal.crypto.verification.SendVerificationMessageWorker
|
||||||
import im.vector.matrix.android.internal.di.MatrixComponent
|
import im.vector.matrix.android.internal.di.MatrixComponent
|
||||||
import im.vector.matrix.android.internal.di.SessionAssistedInjectModule
|
import im.vector.matrix.android.internal.di.SessionAssistedInjectModule
|
||||||
|
@ -109,8 +110,11 @@ internal interface SessionComponent {
|
||||||
fun inject(worker: SendVerificationMessageWorker)
|
fun inject(worker: SendVerificationMessageWorker)
|
||||||
|
|
||||||
fun inject(worker: SendGossipRequestWorker)
|
fun inject(worker: SendGossipRequestWorker)
|
||||||
|
|
||||||
fun inject(worker: CancelGossipRequestWorker)
|
fun inject(worker: CancelGossipRequestWorker)
|
||||||
|
|
||||||
|
fun inject(worker: SendGossipWorker)
|
||||||
|
|
||||||
@Component.Factory
|
@Component.Factory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(
|
fun create(
|
||||||
|
|
|
@ -53,9 +53,9 @@ internal class FileUploader @Inject constructor(@Authenticated
|
||||||
|
|
||||||
suspend fun uploadByteArray(byteArray: ByteArray,
|
suspend fun uploadByteArray(byteArray: ByteArray,
|
||||||
filename: String?,
|
filename: String?,
|
||||||
mimeType: String,
|
mimeType: String?,
|
||||||
progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
|
progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
|
||||||
val uploadBody = byteArray.toRequestBody(mimeType.toMediaTypeOrNull())
|
val uploadBody = byteArray.toRequestBody(mimeType?.toMediaTypeOrNull())
|
||||||
return upload(uploadBody, filename, progressListener)
|
return upload(uploadBody, filename, progressListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,12 +16,12 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.session.content
|
package im.vector.matrix.android.internal.session.content
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.media.ThumbnailUtils
|
import android.media.MediaMetadataRetriever
|
||||||
import android.provider.MediaStore
|
|
||||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
|
import timber.log.Timber
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
internal object ThumbnailExtractor {
|
internal object ThumbnailExtractor {
|
||||||
|
|
||||||
|
@ -33,34 +33,40 @@ internal object ThumbnailExtractor {
|
||||||
val mimeType: String
|
val mimeType: String
|
||||||
)
|
)
|
||||||
|
|
||||||
fun extractThumbnail(attachment: ContentAttachmentData): ThumbnailData? {
|
fun extractThumbnail(context: Context, attachment: ContentAttachmentData): ThumbnailData? {
|
||||||
val file = File(attachment.path)
|
|
||||||
if (!file.exists() || !file.isFile) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return if (attachment.type == ContentAttachmentData.Type.VIDEO) {
|
return if (attachment.type == ContentAttachmentData.Type.VIDEO) {
|
||||||
extractVideoThumbnail(attachment)
|
extractVideoThumbnail(context, attachment)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun extractVideoThumbnail(attachment: ContentAttachmentData): ThumbnailData? {
|
private fun extractVideoThumbnail(context: Context, attachment: ContentAttachmentData): ThumbnailData? {
|
||||||
val thumbnail = ThumbnailUtils.createVideoThumbnail(attachment.path, MediaStore.Video.Thumbnails.MINI_KIND) ?: return null
|
var thumbnailData: ThumbnailData? = null
|
||||||
val outputStream = ByteArrayOutputStream()
|
val mediaMetadataRetriever = MediaMetadataRetriever()
|
||||||
thumbnail.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
|
try {
|
||||||
val thumbnailWidth = thumbnail.width
|
mediaMetadataRetriever.setDataSource(context, attachment.queryUri)
|
||||||
val thumbnailHeight = thumbnail.height
|
val thumbnail = mediaMetadataRetriever.frameAtTime
|
||||||
val thumbnailSize = outputStream.size()
|
|
||||||
val thumbnailData = ThumbnailData(
|
val outputStream = ByteArrayOutputStream()
|
||||||
width = thumbnailWidth,
|
thumbnail.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
|
||||||
height = thumbnailHeight,
|
val thumbnailWidth = thumbnail.width
|
||||||
size = thumbnailSize.toLong(),
|
val thumbnailHeight = thumbnail.height
|
||||||
bytes = outputStream.toByteArray(),
|
val thumbnailSize = outputStream.size()
|
||||||
mimeType = "image/jpeg"
|
thumbnailData = ThumbnailData(
|
||||||
)
|
width = thumbnailWidth,
|
||||||
thumbnail.recycle()
|
height = thumbnailHeight,
|
||||||
outputStream.reset()
|
size = thumbnailSize.toLong(),
|
||||||
|
bytes = outputStream.toByteArray(),
|
||||||
|
mimeType = "image/jpeg"
|
||||||
|
)
|
||||||
|
thumbnail.recycle()
|
||||||
|
outputStream.reset()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Cannot extract video thumbnail")
|
||||||
|
} finally {
|
||||||
|
mediaMetadataRetriever.release()
|
||||||
|
}
|
||||||
return thumbnailData
|
return thumbnailData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,12 +17,9 @@
|
||||||
package im.vector.matrix.android.internal.session.content
|
package im.vector.matrix.android.internal.session.content
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import id.zelory.compressor.Compressor
|
|
||||||
import id.zelory.compressor.constraint.default
|
|
||||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
import im.vector.matrix.android.api.session.events.model.toContent
|
import im.vector.matrix.android.api.session.events.model.toContent
|
||||||
|
@ -41,8 +38,6 @@ import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||||
import im.vector.matrix.android.internal.worker.getSessionComponent
|
import im.vector.matrix.android.internal.worker.getSessionComponent
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.File
|
|
||||||
import java.io.FileInputStream
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private data class NewImageAttributes(
|
private data class NewImageAttributes(
|
||||||
|
@ -94,8 +89,90 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
||||||
|
|
||||||
var newImageAttributes: NewImageAttributes? = null
|
var newImageAttributes: NewImageAttributes? = null
|
||||||
|
|
||||||
val attachmentFile = try {
|
try {
|
||||||
File(attachment.path)
|
val inputStream = context.contentResolver.openInputStream(attachment.queryUri)
|
||||||
|
?: return Result.success(
|
||||||
|
WorkerParamsFactory.toData(
|
||||||
|
params.copy(
|
||||||
|
lastFailureMessage = "Cannot openInputStream for file: " + attachment.queryUri.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
inputStream.use {
|
||||||
|
var uploadedThumbnailUrl: String? = null
|
||||||
|
var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null
|
||||||
|
|
||||||
|
ThumbnailExtractor.extractThumbnail(context, params.attachment)?.let { thumbnailData ->
|
||||||
|
val thumbnailProgressListener = object : ProgressRequestBody.Listener {
|
||||||
|
override fun onProgress(current: Long, total: Long) {
|
||||||
|
notifyTracker(params) { contentUploadStateTracker.setProgressThumbnail(it, current, total) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val contentUploadResponse = if (params.isEncrypted) {
|
||||||
|
Timber.v("Encrypt thumbnail")
|
||||||
|
notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) }
|
||||||
|
val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType)
|
||||||
|
uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo
|
||||||
|
fileUploader.uploadByteArray(encryptionResult.encryptedByteArray,
|
||||||
|
"thumb_${attachment.name}",
|
||||||
|
"application/octet-stream",
|
||||||
|
thumbnailProgressListener)
|
||||||
|
} else {
|
||||||
|
fileUploader.uploadByteArray(thumbnailData.bytes,
|
||||||
|
"thumb_${attachment.name}",
|
||||||
|
thumbnailData.mimeType,
|
||||||
|
thumbnailProgressListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadedThumbnailUrl = contentUploadResponse.contentUri
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Timber.e(t, "Thumbnail update failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val progressListener = object : ProgressRequestBody.Listener {
|
||||||
|
override fun onProgress(current: Long, total: Long) {
|
||||||
|
notifyTracker(params) {
|
||||||
|
if (isStopped) {
|
||||||
|
contentUploadStateTracker.setFailure(it, Throwable("Cancelled"))
|
||||||
|
} else {
|
||||||
|
contentUploadStateTracker.setProgress(it, current, total)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val contentUploadResponse = if (params.isEncrypted) {
|
||||||
|
Timber.v("Encrypt file")
|
||||||
|
notifyTracker(params) { contentUploadStateTracker.setEncrypting(it) }
|
||||||
|
|
||||||
|
val encryptionResult = MXEncryptedAttachments.encryptAttachment(inputStream, attachment.getSafeMimeType())
|
||||||
|
uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo
|
||||||
|
|
||||||
|
fileUploader
|
||||||
|
.uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener)
|
||||||
|
} else {
|
||||||
|
fileUploader
|
||||||
|
.uploadByteArray(inputStream.readBytes(), attachment.name, attachment.getSafeMimeType(), progressListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSuccess(params,
|
||||||
|
contentUploadResponse.contentUri,
|
||||||
|
uploadedFileEncryptedFileInfo,
|
||||||
|
uploadedThumbnailUrl,
|
||||||
|
uploadedThumbnailEncryptedFileInfo,
|
||||||
|
newImageAttributes)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Timber.e(t)
|
||||||
|
handleFailure(params, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
notifyTracker(params) { contentUploadStateTracker.setFailure(it, e) }
|
notifyTracker(params) { contentUploadStateTracker.setFailure(it, e) }
|
||||||
|
@ -106,109 +183,6 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
.let { originalFile ->
|
|
||||||
if (attachment.type == ContentAttachmentData.Type.IMAGE) {
|
|
||||||
if (params.compressBeforeSending) {
|
|
||||||
Compressor.compress(context, originalFile) {
|
|
||||||
default(
|
|
||||||
width = MAX_IMAGE_SIZE,
|
|
||||||
height = MAX_IMAGE_SIZE
|
|
||||||
)
|
|
||||||
}.also { compressedFile ->
|
|
||||||
// Update the params
|
|
||||||
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
|
||||||
BitmapFactory.decodeFile(compressedFile.absolutePath, options)
|
|
||||||
val fileSize = compressedFile.length().toInt()
|
|
||||||
|
|
||||||
newImageAttributes = NewImageAttributes(
|
|
||||||
options.outWidth,
|
|
||||||
options.outHeight,
|
|
||||||
fileSize
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// TODO Fix here the image rotation issue
|
|
||||||
originalFile
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Other type
|
|
||||||
originalFile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var uploadedThumbnailUrl: String? = null
|
|
||||||
var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null
|
|
||||||
|
|
||||||
ThumbnailExtractor.extractThumbnail(params.attachment)?.let { thumbnailData ->
|
|
||||||
val thumbnailProgressListener = object : ProgressRequestBody.Listener {
|
|
||||||
override fun onProgress(current: Long, total: Long) {
|
|
||||||
notifyTracker(params) { contentUploadStateTracker.setProgressThumbnail(it, current, total) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
val contentUploadResponse = if (params.isEncrypted) {
|
|
||||||
Timber.v("Encrypt thumbnail")
|
|
||||||
notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) }
|
|
||||||
val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType)
|
|
||||||
uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo
|
|
||||||
fileUploader.uploadByteArray(encryptionResult.encryptedByteArray,
|
|
||||||
"thumb_${attachment.name}",
|
|
||||||
"application/octet-stream",
|
|
||||||
thumbnailProgressListener)
|
|
||||||
} else {
|
|
||||||
fileUploader.uploadByteArray(thumbnailData.bytes,
|
|
||||||
"thumb_${attachment.name}",
|
|
||||||
thumbnailData.mimeType,
|
|
||||||
thumbnailProgressListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadedThumbnailUrl = contentUploadResponse.contentUri
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
Timber.e(t)
|
|
||||||
return handleFailure(params, t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val progressListener = object : ProgressRequestBody.Listener {
|
|
||||||
override fun onProgress(current: Long, total: Long) {
|
|
||||||
notifyTracker(params) {
|
|
||||||
if (isStopped) {
|
|
||||||
contentUploadStateTracker.setFailure(it, Throwable("Cancelled"))
|
|
||||||
} else {
|
|
||||||
contentUploadStateTracker.setProgress(it, current, total)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null
|
|
||||||
|
|
||||||
return try {
|
|
||||||
val contentUploadResponse = if (params.isEncrypted) {
|
|
||||||
Timber.v("Encrypt file")
|
|
||||||
notifyTracker(params) { contentUploadStateTracker.setEncrypting(it) }
|
|
||||||
|
|
||||||
val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.getSafeMimeType())
|
|
||||||
uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo
|
|
||||||
|
|
||||||
fileUploader
|
|
||||||
.uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener)
|
|
||||||
} else {
|
|
||||||
fileUploader
|
|
||||||
.uploadFile(attachmentFile, attachment.name, attachment.getSafeMimeType(), progressListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSuccess(params,
|
|
||||||
contentUploadResponse.contentUri,
|
|
||||||
uploadedFileEncryptedFileInfo,
|
|
||||||
uploadedThumbnailUrl,
|
|
||||||
uploadedThumbnailEncryptedFileInfo,
|
|
||||||
newImageAttributes)
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
Timber.e(t)
|
|
||||||
handleFailure(params, t)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.session.room.send
|
package im.vector.matrix.android.internal.session.room.send
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.media.MediaMetadataRetriever
|
import android.media.MediaMetadataRetriever
|
||||||
import androidx.exifinterface.media.ExifInterface
|
import androidx.exifinterface.media.ExifInterface
|
||||||
|
@ -74,6 +75,7 @@ import javax.inject.Inject
|
||||||
* The transactionId is used as loc
|
* The transactionId is used as loc
|
||||||
*/
|
*/
|
||||||
internal class LocalEchoEventFactory @Inject constructor(
|
internal class LocalEchoEventFactory @Inject constructor(
|
||||||
|
private val context: Context,
|
||||||
@UserId private val userId: String,
|
@UserId private val userId: String,
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val textPillsUtils: TextPillsUtils,
|
private val textPillsUtils: TextPillsUtils,
|
||||||
|
@ -266,14 +268,14 @@ internal class LocalEchoEventFactory @Inject constructor(
|
||||||
height = height?.toInt() ?: 0,
|
height = height?.toInt() ?: 0,
|
||||||
size = attachment.size.toInt()
|
size = attachment.size.toInt()
|
||||||
),
|
),
|
||||||
url = attachment.path
|
url = attachment.queryUri.toString()
|
||||||
)
|
)
|
||||||
return createEvent(roomId, content)
|
return createEvent(roomId, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createVideoEvent(roomId: String, attachment: ContentAttachmentData): Event {
|
private fun createVideoEvent(roomId: String, attachment: ContentAttachmentData): Event {
|
||||||
val mediaDataRetriever = MediaMetadataRetriever()
|
val mediaDataRetriever = MediaMetadataRetriever()
|
||||||
mediaDataRetriever.setDataSource(attachment.path)
|
mediaDataRetriever.setDataSource(context, attachment.queryUri)
|
||||||
|
|
||||||
// Use frame to calculate height and width as we are sure to get the right ones
|
// Use frame to calculate height and width as we are sure to get the right ones
|
||||||
val firstFrame: Bitmap? = mediaDataRetriever.frameAtTime
|
val firstFrame: Bitmap? = mediaDataRetriever.frameAtTime
|
||||||
|
@ -281,7 +283,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
||||||
val width = firstFrame?.width ?: 0
|
val width = firstFrame?.width ?: 0
|
||||||
mediaDataRetriever.release()
|
mediaDataRetriever.release()
|
||||||
|
|
||||||
val thumbnailInfo = ThumbnailExtractor.extractThumbnail(attachment)?.let {
|
val thumbnailInfo = ThumbnailExtractor.extractThumbnail(context, attachment)?.let {
|
||||||
ThumbnailInfo(
|
ThumbnailInfo(
|
||||||
width = it.width,
|
width = it.width,
|
||||||
height = it.height,
|
height = it.height,
|
||||||
|
@ -299,10 +301,10 @@ internal class LocalEchoEventFactory @Inject constructor(
|
||||||
size = attachment.size,
|
size = attachment.size,
|
||||||
duration = attachment.duration?.toInt() ?: 0,
|
duration = attachment.duration?.toInt() ?: 0,
|
||||||
// Glide will be able to use the local path and extract a thumbnail.
|
// Glide will be able to use the local path and extract a thumbnail.
|
||||||
thumbnailUrl = attachment.path,
|
thumbnailUrl = attachment.queryUri.toString(),
|
||||||
thumbnailInfo = thumbnailInfo
|
thumbnailInfo = thumbnailInfo
|
||||||
),
|
),
|
||||||
url = attachment.path
|
url = attachment.queryUri.toString()
|
||||||
)
|
)
|
||||||
return createEvent(roomId, content)
|
return createEvent(roomId, content)
|
||||||
}
|
}
|
||||||
|
@ -315,7 +317,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
||||||
mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() } ?: "audio/mpeg",
|
mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() } ?: "audio/mpeg",
|
||||||
size = attachment.size
|
size = attachment.size
|
||||||
),
|
),
|
||||||
url = attachment.path
|
url = attachment.queryUri.toString()
|
||||||
)
|
)
|
||||||
return createEvent(roomId, content)
|
return createEvent(roomId, content)
|
||||||
}
|
}
|
||||||
|
@ -329,7 +331,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
||||||
?: "application/octet-stream",
|
?: "application/octet-stream",
|
||||||
size = attachment.size
|
size = attachment.size
|
||||||
),
|
),
|
||||||
url = attachment.path
|
url = attachment.queryUri.toString()
|
||||||
)
|
)
|
||||||
return createEvent(roomId, content)
|
return createEvent(roomId, content)
|
||||||
}
|
}
|
||||||
|
|
|
@ -128,7 +128,7 @@ internal class TimelineEventDecryptor @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
Timber.e(t, "Failed to decrypt event $eventId")
|
Timber.e("Failed to decrypt event $eventId, ${t.localizedMessage}")
|
||||||
} finally {
|
} finally {
|
||||||
synchronized(existingRequests) {
|
synchronized(existingRequests) {
|
||||||
existingRequests.remove(request)
|
existingRequests.remove(request)
|
||||||
|
|
1
multipicker/.gitignore
vendored
Normal file
1
multipicker/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/build
|
56
multipicker/build.gradle
Normal file
56
multipicker/build.gradle
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
apply plugin: 'com.android.library'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'kotlin-android-extensions'
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion 29
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion 19
|
||||||
|
targetSdkVersion 29
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0"
|
||||||
|
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
consumerProguardFiles 'consumer-rules.pro'
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
|
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||||
|
implementation 'androidx.core:core-ktx:1.2.0'
|
||||||
|
testImplementation 'junit:junit:4.12'
|
||||||
|
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||||
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||||
|
|
||||||
|
implementation 'androidx.exifinterface:exifinterface:1.3.0-alpha01'
|
||||||
|
|
||||||
|
// Log
|
||||||
|
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||||
|
}
|
0
multipicker/consumer-rules.pro
Normal file
0
multipicker/consumer-rules.pro
Normal file
21
multipicker/proguard-rules.pro
vendored
Normal file
21
multipicker/proguard-rules.pro
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
16
multipicker/src/main/AndroidManifest.xml
Normal file
16
multipicker/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="im.vector.riotx.multipicker">
|
||||||
|
|
||||||
|
<application>
|
||||||
|
<provider
|
||||||
|
android:name=".provider.MultiPickerFileProvider"
|
||||||
|
android:authorities="${applicationId}.multipicker.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/multipicker_provider_paths" />
|
||||||
|
</provider>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.multipicker
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.media.MediaMetadataRetriever
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import im.vector.riotx.multipicker.entity.MultiPickerAudioType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Audio file picker implementation
|
||||||
|
*/
|
||||||
|
class AudioPicker(override val requestCode: Int) : Picker<MultiPickerAudioType>(requestCode) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call this function from onActivityResult(int, int, Intent).
|
||||||
|
* Returns selected audio files or empty list if request code is wrong
|
||||||
|
* or result code is not Activity.RESULT_OK
|
||||||
|
* or user did not select any files.
|
||||||
|
*/
|
||||||
|
override fun getSelectedFiles(context: Context, requestCode: Int, resultCode: Int, data: Intent?): List<MultiPickerAudioType> {
|
||||||
|
if (requestCode != this.requestCode && resultCode != Activity.RESULT_OK) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val audioList = mutableListOf<MultiPickerAudioType>()
|
||||||
|
|
||||||
|
getSelectedUriList(data).forEach { selectedUri ->
|
||||||
|
val projection = arrayOf(
|
||||||
|
MediaStore.Audio.Media.DISPLAY_NAME,
|
||||||
|
MediaStore.Audio.Media.SIZE
|
||||||
|
)
|
||||||
|
|
||||||
|
context.contentResolver.query(
|
||||||
|
selectedUri,
|
||||||
|
projection,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)?.use { cursor ->
|
||||||
|
val nameColumn = cursor.getColumnIndex(MediaStore.Audio.Media.DISPLAY_NAME)
|
||||||
|
val sizeColumn = cursor.getColumnIndex(MediaStore.Audio.Media.SIZE)
|
||||||
|
|
||||||
|
if (cursor.moveToNext()) {
|
||||||
|
val name = cursor.getString(nameColumn)
|
||||||
|
val size = cursor.getLong(sizeColumn)
|
||||||
|
var duration = 0L
|
||||||
|
|
||||||
|
context.contentResolver.openFileDescriptor(selectedUri, "r")?.use { pfd ->
|
||||||
|
val mediaMetadataRetriever = MediaMetadataRetriever()
|
||||||
|
mediaMetadataRetriever.setDataSource(pfd.fileDescriptor)
|
||||||
|
duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION).toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
audioList.add(
|
||||||
|
MultiPickerAudioType(
|
||||||
|
name,
|
||||||
|
size,
|
||||||
|
context.contentResolver.getType(selectedUri),
|
||||||
|
selectedUri,
|
||||||
|
duration
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return audioList
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createIntent(): Intent {
|
||||||
|
return Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !single)
|
||||||
|
type = "audio/*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.multipicker
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import im.vector.riotx.multipicker.entity.MultiPickerImageType
|
||||||
|
import im.vector.riotx.multipicker.utils.ImageUtils
|
||||||
|
import java.io.File
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of taking a photo with Camera
|
||||||
|
*/
|
||||||
|
class CameraPicker(val requestCode: Int) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start camera by using an Activity
|
||||||
|
* @param activity Activity to handle onActivityResult().
|
||||||
|
* @return Uri of taken photo or null if the operation is cancelled.
|
||||||
|
*/
|
||||||
|
fun startWithExpectingFile(activity: Activity): Uri? {
|
||||||
|
val photoUri = createPhotoUri(activity)
|
||||||
|
val intent = createIntent().apply {
|
||||||
|
putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
|
||||||
|
}
|
||||||
|
activity.startActivityForResult(intent, requestCode)
|
||||||
|
return photoUri
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start camera by using a Fragment
|
||||||
|
* @param fragment Fragment to handle onActivityResult().
|
||||||
|
* @return Uri of taken photo or null if the operation is cancelled.
|
||||||
|
*/
|
||||||
|
fun startWithExpectingFile(fragment: Fragment): Uri? {
|
||||||
|
val photoUri = createPhotoUri(fragment.requireContext())
|
||||||
|
val intent = createIntent().apply {
|
||||||
|
putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
|
||||||
|
}
|
||||||
|
fragment.startActivityForResult(intent, requestCode)
|
||||||
|
return photoUri
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call this function from onActivityResult(int, int, Intent).
|
||||||
|
* @return Taken photo or null if request code is wrong
|
||||||
|
* or result code is not Activity.RESULT_OK
|
||||||
|
* or user cancelled the operation.
|
||||||
|
*/
|
||||||
|
fun getTakenPhoto(context: Context, requestCode: Int, resultCode: Int, photoUri: Uri): MultiPickerImageType? {
|
||||||
|
if (requestCode == this.requestCode && resultCode == Activity.RESULT_OK) {
|
||||||
|
val projection = arrayOf(
|
||||||
|
MediaStore.Images.Media.DISPLAY_NAME,
|
||||||
|
MediaStore.Images.Media.SIZE
|
||||||
|
)
|
||||||
|
|
||||||
|
context.contentResolver.query(
|
||||||
|
photoUri,
|
||||||
|
projection,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)?.use { cursor ->
|
||||||
|
val nameColumn = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)
|
||||||
|
val sizeColumn = cursor.getColumnIndex(MediaStore.Images.Media.SIZE)
|
||||||
|
|
||||||
|
if (cursor.moveToNext()) {
|
||||||
|
val name = cursor.getString(nameColumn)
|
||||||
|
val size = cursor.getLong(sizeColumn)
|
||||||
|
|
||||||
|
val bitmap = ImageUtils.getBitmap(context, photoUri)
|
||||||
|
val orientation = ImageUtils.getOrientation(context, photoUri)
|
||||||
|
|
||||||
|
return MultiPickerImageType(
|
||||||
|
name,
|
||||||
|
size,
|
||||||
|
context.contentResolver.getType(photoUri),
|
||||||
|
photoUri,
|
||||||
|
bitmap?.width ?: 0,
|
||||||
|
bitmap?.height ?: 0,
|
||||||
|
orientation
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createIntent(): Intent {
|
||||||
|
return Intent(MediaStore.ACTION_IMAGE_CAPTURE)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createPhotoUri(context: Context): Uri {
|
||||||
|
val file = createImageFile(context)
|
||||||
|
val authority = context.packageName + ".multipicker.fileprovider"
|
||||||
|
return FileProvider.getUriForFile(context, authority, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createImageFile(context: Context): File {
|
||||||
|
val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
|
||||||
|
val storageDir: File = context.filesDir
|
||||||
|
return File.createTempFile(
|
||||||
|
"${timeStamp}_", /* prefix */
|
||||||
|
".jpg", /* suffix */
|
||||||
|
storageDir /* directory */
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,135 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.multipicker
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.provider.ContactsContract
|
||||||
|
import im.vector.riotx.multipicker.entity.MultiPickerContactType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contact Picker implementation
|
||||||
|
*/
|
||||||
|
class ContactPicker(override val requestCode: Int) : Picker<MultiPickerContactType>(requestCode) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call this function from onActivityResult(int, int, Intent).
|
||||||
|
* Returns selected contact or empty list if request code is wrong
|
||||||
|
* or result code is not Activity.RESULT_OK
|
||||||
|
* or user did not select any files.
|
||||||
|
*/
|
||||||
|
override fun getSelectedFiles(context: Context, requestCode: Int, resultCode: Int, data: Intent?): List<MultiPickerContactType> {
|
||||||
|
if (requestCode != this.requestCode && resultCode != Activity.RESULT_OK) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val contactList = mutableListOf<MultiPickerContactType>()
|
||||||
|
|
||||||
|
data?.data?.let { selectedUri ->
|
||||||
|
val projection = arrayOf(
|
||||||
|
ContactsContract.Contacts.DISPLAY_NAME,
|
||||||
|
ContactsContract.Contacts.PHOTO_URI,
|
||||||
|
ContactsContract.Contacts._ID
|
||||||
|
)
|
||||||
|
|
||||||
|
context.contentResolver.query(
|
||||||
|
selectedUri,
|
||||||
|
projection,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)?.use { cursor ->
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
val idColumn = cursor.getColumnIndex(ContactsContract.Contacts._ID)
|
||||||
|
val nameColumn = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)
|
||||||
|
val photoUriColumn = cursor.getColumnIndex(ContactsContract.Contacts.PHOTO_URI)
|
||||||
|
|
||||||
|
val contactId = cursor.getInt(idColumn)
|
||||||
|
var name = cursor.getString(nameColumn)
|
||||||
|
var photoUri = cursor.getString(photoUriColumn)
|
||||||
|
var phoneNumberList = mutableListOf<String>()
|
||||||
|
var emailList = mutableListOf<String>()
|
||||||
|
|
||||||
|
getRawContactId(context.contentResolver, contactId)?.let { rawContactId ->
|
||||||
|
val selection = ContactsContract.Data.RAW_CONTACT_ID + " = ?"
|
||||||
|
val selectionArgs = arrayOf(rawContactId.toString())
|
||||||
|
|
||||||
|
context.contentResolver.query(
|
||||||
|
ContactsContract.Data.CONTENT_URI,
|
||||||
|
arrayOf(
|
||||||
|
ContactsContract.Data.MIMETYPE,
|
||||||
|
ContactsContract.Data.DATA1
|
||||||
|
),
|
||||||
|
selection,
|
||||||
|
selectionArgs,
|
||||||
|
null
|
||||||
|
)?.use { cursor ->
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
val mimeType = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.MIMETYPE))
|
||||||
|
val contactData = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.DATA1))
|
||||||
|
|
||||||
|
if (mimeType == ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) {
|
||||||
|
name = contactData
|
||||||
|
}
|
||||||
|
if (mimeType == ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE) {
|
||||||
|
phoneNumberList.add(contactData)
|
||||||
|
}
|
||||||
|
if (mimeType == ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE) {
|
||||||
|
emailList.add(contactData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contactList.add(
|
||||||
|
MultiPickerContactType(
|
||||||
|
name,
|
||||||
|
photoUri,
|
||||||
|
phoneNumberList,
|
||||||
|
emailList
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return contactList
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getRawContactId(contentResolver: ContentResolver, contactId: Int): Int? {
|
||||||
|
val projection = arrayOf(ContactsContract.RawContacts._ID)
|
||||||
|
val selection = ContactsContract.RawContacts.CONTACT_ID + " = ?"
|
||||||
|
val selectionArgs = arrayOf(contactId.toString() + "")
|
||||||
|
return contentResolver.query(
|
||||||
|
ContactsContract.RawContacts.CONTENT_URI,
|
||||||
|
projection,
|
||||||
|
selection,
|
||||||
|
selectionArgs,
|
||||||
|
null
|
||||||
|
)?.use { cursor ->
|
||||||
|
return if (cursor.moveToFirst()) cursor.getInt(cursor.getColumnIndex(ContactsContract.RawContacts._ID)) else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createIntent(): Intent {
|
||||||
|
return Intent(Intent.ACTION_PICK).apply {
|
||||||
|
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !single)
|
||||||
|
type = ContactsContract.Contacts.CONTENT_TYPE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.multipicker
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.provider.OpenableColumns
|
||||||
|
import im.vector.riotx.multipicker.entity.MultiPickerFileType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of selecting any type of files
|
||||||
|
*/
|
||||||
|
class FilePicker(override val requestCode: Int) : Picker<MultiPickerFileType>(requestCode) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call this function from onActivityResult(int, int, Intent).
|
||||||
|
* Returns selected files or empty list if request code is wrong
|
||||||
|
* or result code is not Activity.RESULT_OK
|
||||||
|
* or user did not select any files.
|
||||||
|
*/
|
||||||
|
override fun getSelectedFiles(context: Context, requestCode: Int, resultCode: Int, data: Intent?): List<MultiPickerFileType> {
|
||||||
|
if (requestCode != this.requestCode && resultCode != Activity.RESULT_OK) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val fileList = mutableListOf<MultiPickerFileType>()
|
||||||
|
|
||||||
|
getSelectedUriList(data).forEach { selectedUri ->
|
||||||
|
context.contentResolver.query(selectedUri, null, null, null, null)
|
||||||
|
?.use { cursor ->
|
||||||
|
val nameColumn = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||||
|
val sizeColumn = cursor.getColumnIndex(OpenableColumns.SIZE)
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
val name = cursor.getString(nameColumn)
|
||||||
|
val size = cursor.getLong(sizeColumn)
|
||||||
|
|
||||||
|
fileList.add(
|
||||||
|
MultiPickerFileType(
|
||||||
|
name,
|
||||||
|
size,
|
||||||
|
context.contentResolver.getType(selectedUri),
|
||||||
|
selectedUri
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fileList
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createIntent(): Intent {
|
||||||
|
return Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !single)
|
||||||
|
type = "*/*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.multipicker
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import im.vector.riotx.multipicker.entity.MultiPickerImageType
|
||||||
|
import im.vector.riotx.multipicker.utils.ImageUtils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Image Picker implementation
|
||||||
|
*/
|
||||||
|
class ImagePicker(override val requestCode: Int) : Picker<MultiPickerImageType>(requestCode) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call this function from onActivityResult(int, int, Intent).
|
||||||
|
* Returns selected image files or empty list if request code is wrong
|
||||||
|
* or result code is not Activity.RESULT_OK
|
||||||
|
* or user did not select any files.
|
||||||
|
*/
|
||||||
|
override fun getSelectedFiles(context: Context, requestCode: Int, resultCode: Int, data: Intent?): List<MultiPickerImageType> {
|
||||||
|
if (requestCode != this.requestCode && resultCode != Activity.RESULT_OK) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val imageList = mutableListOf<MultiPickerImageType>()
|
||||||
|
|
||||||
|
getSelectedUriList(data).forEach { selectedUri ->
|
||||||
|
val projection = arrayOf(
|
||||||
|
MediaStore.Images.Media.DISPLAY_NAME,
|
||||||
|
MediaStore.Images.Media.SIZE
|
||||||
|
)
|
||||||
|
|
||||||
|
context.contentResolver.query(
|
||||||
|
selectedUri,
|
||||||
|
projection,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)?.use { cursor ->
|
||||||
|
val nameColumn = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)
|
||||||
|
val sizeColumn = cursor.getColumnIndex(MediaStore.Images.Media.SIZE)
|
||||||
|
|
||||||
|
if (cursor.moveToNext()) {
|
||||||
|
val name = cursor.getString(nameColumn)
|
||||||
|
val size = cursor.getLong(sizeColumn)
|
||||||
|
|
||||||
|
val bitmap = ImageUtils.getBitmap(context, selectedUri)
|
||||||
|
val orientation = ImageUtils.getOrientation(context, selectedUri)
|
||||||
|
|
||||||
|
imageList.add(
|
||||||
|
MultiPickerImageType(
|
||||||
|
name,
|
||||||
|
size,
|
||||||
|
context.contentResolver.getType(selectedUri),
|
||||||
|
selectedUri,
|
||||||
|
bitmap?.width ?: 0,
|
||||||
|
bitmap?.height ?: 0,
|
||||||
|
orientation
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return imageList
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createIntent(): Intent {
|
||||||
|
return Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !single)
|
||||||
|
type = "image/*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.riotx.multipicker
|
||||||
|
|
||||||
|
class MultiPicker<T> {
|
||||||
|
|
||||||
|
companion object Type {
|
||||||
|
val IMAGE by lazy { MultiPicker<ImagePicker>() }
|
||||||
|
val FILE by lazy { MultiPicker<FilePicker>() }
|
||||||
|
val VIDEO by lazy { MultiPicker<VideoPicker>() }
|
||||||
|
val AUDIO by lazy { MultiPicker<AudioPicker>() }
|
||||||
|
val CONTACT by lazy { MultiPicker<ContactPicker>() }
|
||||||
|
val CAMERA by lazy { MultiPicker<CameraPicker>() }
|
||||||
|
|
||||||
|
const val REQUEST_CODE_PICK_IMAGE = 5000
|
||||||
|
const val REQUEST_CODE_PICK_VIDEO = 5001
|
||||||
|
const val REQUEST_CODE_PICK_FILE = 5002
|
||||||
|
const val REQUEST_CODE_PICK_AUDIO = 5003
|
||||||
|
const val REQUEST_CODE_PICK_CONTACT = 5004
|
||||||
|
const val REQUEST_CODE_TAKE_PHOTO = 5005
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun <T> get(type: MultiPicker<T>): T {
|
||||||
|
return when (type) {
|
||||||
|
IMAGE -> ImagePicker(REQUEST_CODE_PICK_IMAGE) as T
|
||||||
|
VIDEO -> VideoPicker(REQUEST_CODE_PICK_VIDEO) as T
|
||||||
|
FILE -> FilePicker(REQUEST_CODE_PICK_FILE) as T
|
||||||
|
AUDIO -> AudioPicker(REQUEST_CODE_PICK_AUDIO) as T
|
||||||
|
CONTACT -> ContactPicker(REQUEST_CODE_PICK_CONTACT) as T
|
||||||
|
CAMERA -> CameraPicker(REQUEST_CODE_TAKE_PHOTO) as T
|
||||||
|
else -> throw IllegalArgumentException("Unsupported type $type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
116
multipicker/src/main/java/im/vector/riotx/multipicker/Picker.kt
Normal file
116
multipicker/src/main/java/im/vector/riotx/multipicker/Picker.kt
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.multipicker
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.pm.ResolveInfo
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class to provide all types of Pickers
|
||||||
|
*/
|
||||||
|
abstract class Picker<T>(open val requestCode: Int) {
|
||||||
|
|
||||||
|
protected var single = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call this function from onActivityResult(int, int, Intent).
|
||||||
|
* @return selected files or empty list if request code is wrong
|
||||||
|
* or result code is not Activity.RESULT_OK
|
||||||
|
* or user did not select any files.
|
||||||
|
*/
|
||||||
|
abstract fun getSelectedFiles(context: Context, requestCode: Int, resultCode: Int, data: Intent?): List<T>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this function to retrieve files which are shared from another application or internally
|
||||||
|
* by using android.intent.action.SEND or android.intent.action.SEND_MULTIPLE actions.
|
||||||
|
*/
|
||||||
|
fun getIncomingFiles(context: Context, data: Intent?): List<T> {
|
||||||
|
if (data == null) return emptyList()
|
||||||
|
|
||||||
|
val uriList = mutableListOf<Uri>()
|
||||||
|
if (data.action == Intent.ACTION_SEND) {
|
||||||
|
(data.getParcelableExtra(Intent.EXTRA_STREAM) as? Uri)?.let { uriList.add(it) }
|
||||||
|
} else if (data.action == Intent.ACTION_SEND_MULTIPLE) {
|
||||||
|
val extraUriList: List<Uri>? = data.getParcelableArrayListExtra(Intent.EXTRA_STREAM)
|
||||||
|
extraUriList?.let { uriList.addAll(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val resInfoList: List<ResolveInfo> = context.packageManager.queryIntentActivities(data, PackageManager.MATCH_DEFAULT_ONLY)
|
||||||
|
uriList.forEach {
|
||||||
|
for (resolveInfo in resInfoList) {
|
||||||
|
val packageName: String = resolveInfo.activityInfo.packageName
|
||||||
|
context.grantUriPermission(packageName, it, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getSelectedFiles(context, requestCode, Activity.RESULT_OK, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call this function to disable multiple selection of files.
|
||||||
|
*/
|
||||||
|
fun single(): Picker<T> {
|
||||||
|
single = true
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun createIntent(): Intent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start Storage Access Framework UI by using an Activity.
|
||||||
|
* @param activity Activity to handle onActivityResult().
|
||||||
|
*/
|
||||||
|
fun startWith(activity: Activity) {
|
||||||
|
activity.startActivityForResult(createIntent().apply { addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) }, requestCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start Storage Access Framework UI by using a Fragment.
|
||||||
|
* @param fragment Fragment to handle onActivityResult().
|
||||||
|
*/
|
||||||
|
fun startWith(fragment: Fragment) {
|
||||||
|
fragment.startActivityForResult(createIntent().apply { addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) }, requestCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun getSelectedUriList(data: Intent?): List<Uri> {
|
||||||
|
val selectedUriList = mutableListOf<Uri>()
|
||||||
|
val dataUri = data?.data
|
||||||
|
val clipData = data?.clipData
|
||||||
|
|
||||||
|
if (clipData != null) {
|
||||||
|
for (i in 0 until clipData.itemCount) {
|
||||||
|
selectedUriList.add(clipData.getItemAt(i).uri)
|
||||||
|
}
|
||||||
|
} else if (dataUri != null) {
|
||||||
|
selectedUriList.add(dataUri)
|
||||||
|
} else {
|
||||||
|
data?.extras?.get(Intent.EXTRA_STREAM)?.let {
|
||||||
|
(it as? List<*>)?.filterIsInstance<Uri>()?.let { uriList ->
|
||||||
|
selectedUriList.addAll(uriList)
|
||||||
|
}
|
||||||
|
if (it is Uri) {
|
||||||
|
selectedUriList.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selectedUriList
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.multipicker
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.media.MediaMetadataRetriever
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import im.vector.riotx.multipicker.entity.MultiPickerVideoType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Video Picker implementation
|
||||||
|
*/
|
||||||
|
class VideoPicker(override val requestCode: Int) : Picker<MultiPickerVideoType>(requestCode) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call this function from onActivityResult(int, int, Intent).
|
||||||
|
* Returns selected video files or empty list if request code is wrong
|
||||||
|
* or result code is not Activity.RESULT_OK
|
||||||
|
* or user did not select any files.
|
||||||
|
*/
|
||||||
|
override fun getSelectedFiles(context: Context, requestCode: Int, resultCode: Int, data: Intent?): List<MultiPickerVideoType> {
|
||||||
|
if (requestCode != this.requestCode && resultCode != Activity.RESULT_OK) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val videoList = mutableListOf<MultiPickerVideoType>()
|
||||||
|
|
||||||
|
getSelectedUriList(data).forEach { selectedUri ->
|
||||||
|
val projection = arrayOf(
|
||||||
|
MediaStore.Video.Media.DISPLAY_NAME,
|
||||||
|
MediaStore.Video.Media.SIZE
|
||||||
|
)
|
||||||
|
|
||||||
|
context.contentResolver.query(
|
||||||
|
selectedUri,
|
||||||
|
projection,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)?.use { cursor ->
|
||||||
|
val nameColumn = cursor.getColumnIndex(MediaStore.Video.Media.DISPLAY_NAME)
|
||||||
|
val sizeColumn = cursor.getColumnIndex(MediaStore.Video.Media.SIZE)
|
||||||
|
|
||||||
|
if (cursor.moveToNext()) {
|
||||||
|
val name = cursor.getString(nameColumn)
|
||||||
|
val size = cursor.getLong(sizeColumn)
|
||||||
|
var duration = 0L
|
||||||
|
var width = 0
|
||||||
|
var height = 0
|
||||||
|
var orientation = 0
|
||||||
|
|
||||||
|
context.contentResolver.openFileDescriptor(selectedUri, "r")?.use { pfd ->
|
||||||
|
val mediaMetadataRetriever = MediaMetadataRetriever()
|
||||||
|
mediaMetadataRetriever.setDataSource(pfd.fileDescriptor)
|
||||||
|
duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION).toLong()
|
||||||
|
width = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH).toInt()
|
||||||
|
height = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT).toInt()
|
||||||
|
orientation = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
videoList.add(
|
||||||
|
MultiPickerVideoType(
|
||||||
|
name,
|
||||||
|
size,
|
||||||
|
context.contentResolver.getType(selectedUri),
|
||||||
|
selectedUri,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
orientation,
|
||||||
|
duration
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return videoList
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createIntent(): Intent {
|
||||||
|
return Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !single)
|
||||||
|
type = "video/*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.riotx.multipicker.entity
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
|
||||||
|
data class MultiPickerAudioType(
|
||||||
|
override val displayName: String?,
|
||||||
|
override val size: Long,
|
||||||
|
override val mimeType: String?,
|
||||||
|
override val contentUri: Uri,
|
||||||
|
val duration: Long
|
||||||
|
) : MultiPickerBaseType
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.multipicker.entity
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
|
||||||
|
interface MultiPickerBaseType {
|
||||||
|
val displayName: String?
|
||||||
|
val size: Long
|
||||||
|
val mimeType: String?
|
||||||
|
val contentUri: Uri
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.multipicker.entity
|
||||||
|
|
||||||
|
data class MultiPickerContactType(
|
||||||
|
val displayName: String,
|
||||||
|
val photoUri: String?,
|
||||||
|
val phoneNumberList: List<String>,
|
||||||
|
val emailList: List<String>
|
||||||
|
)
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.multipicker.entity
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
|
||||||
|
data class MultiPickerFileType(
|
||||||
|
override val displayName: String?,
|
||||||
|
override val size: Long,
|
||||||
|
override val mimeType: String?,
|
||||||
|
override val contentUri: Uri
|
||||||
|
) : MultiPickerBaseType
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.multipicker.entity
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
|
||||||
|
data class MultiPickerImageType(
|
||||||
|
override val displayName: String?,
|
||||||
|
override val size: Long,
|
||||||
|
override val mimeType: String?,
|
||||||
|
override val contentUri: Uri,
|
||||||
|
val width: Int,
|
||||||
|
val height: Int,
|
||||||
|
val orientation: Int
|
||||||
|
) : MultiPickerBaseType
|
|
@ -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.riotx.multipicker.entity
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
|
||||||
|
data class MultiPickerVideoType(
|
||||||
|
override val displayName: String?,
|
||||||
|
override val size: Long,
|
||||||
|
override val mimeType: String?,
|
||||||
|
override val contentUri: Uri,
|
||||||
|
val width: Int,
|
||||||
|
val height: Int,
|
||||||
|
val orientation: Int,
|
||||||
|
val duration: Long
|
||||||
|
) : MultiPickerBaseType
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.multipicker.provider
|
||||||
|
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
|
||||||
|
class MultiPickerFileProvider : FileProvider()
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.multipicker.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.ImageDecoder
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.exifinterface.media.ExifInterface
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
object ImageUtils {
|
||||||
|
|
||||||
|
fun getBitmap(context: Context, uri: Uri): Bitmap? {
|
||||||
|
return try {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
ImageDecoder.decodeBitmap(ImageDecoder.createSource(context.contentResolver, uri))
|
||||||
|
} else {
|
||||||
|
context.contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||||
|
BitmapFactory.decodeStream(inputStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Cannot decode Bitmap: %s", uri.toString())
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getOrientation(context: Context, uri: Uri): Int {
|
||||||
|
var orientation = 0
|
||||||
|
context.contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||||
|
try {
|
||||||
|
ExifInterface(inputStream).let {
|
||||||
|
orientation = it.rotationDegrees
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Cannot read orientation: %s", uri.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return orientation
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths>
|
||||||
|
<files-path
|
||||||
|
name="external_files"
|
||||||
|
path="." />
|
||||||
|
</paths>
|
|
@ -1 +1,2 @@
|
||||||
include ':vector', ':matrix-sdk-android', ':matrix-sdk-android-rx', ':diff-match-patch'
|
include ':vector', ':matrix-sdk-android', ':matrix-sdk-android-rx', ':diff-match-patch'
|
||||||
|
include ':multipicker'
|
||||||
|
|
|
@ -254,6 +254,7 @@ dependencies {
|
||||||
implementation project(":matrix-sdk-android")
|
implementation project(":matrix-sdk-android")
|
||||||
implementation project(":matrix-sdk-android-rx")
|
implementation project(":matrix-sdk-android-rx")
|
||||||
implementation project(":diff-match-patch")
|
implementation project(":diff-match-patch")
|
||||||
|
implementation project(":multipicker")
|
||||||
implementation 'com.android.support:multidex:1.0.3'
|
implementation 'com.android.support:multidex:1.0.3'
|
||||||
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
|
@ -323,7 +324,7 @@ dependencies {
|
||||||
implementation 'com.nulab-inc:zxcvbn:1.2.7'
|
implementation 'com.nulab-inc:zxcvbn:1.2.7'
|
||||||
|
|
||||||
//Alerter
|
//Alerter
|
||||||
implementation 'com.tapadoo.android:alerter:4.0.3'
|
implementation 'com.tapadoo.android:alerter:5.1.2'
|
||||||
|
|
||||||
implementation 'com.otaliastudios:autocomplete:1.1.0'
|
implementation 'com.otaliastudios:autocomplete:1.1.0'
|
||||||
|
|
||||||
|
@ -347,9 +348,6 @@ dependencies {
|
||||||
// Badge for compatibility
|
// Badge for compatibility
|
||||||
implementation 'me.leolin:ShortcutBadger:1.1.22@aar'
|
implementation 'me.leolin:ShortcutBadger:1.1.22@aar'
|
||||||
|
|
||||||
// File picker
|
|
||||||
implementation 'com.kbeanie:multipicker:1.6@aar'
|
|
||||||
|
|
||||||
// DI
|
// DI
|
||||||
implementation "com.google.dagger:dagger:$daggerVersion"
|
implementation "com.google.dagger:dagger:$daggerVersion"
|
||||||
kapt "com.google.dagger:dagger-compiler:$daggerVersion"
|
kapt "com.google.dagger:dagger-compiler:$daggerVersion"
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
<!-- Adding CAMERA permission prevents Chromebooks to see the application on the PlayStore -->
|
<!-- Adding CAMERA permission prevents Chromebooks to see the application on the PlayStore -->
|
||||||
<!-- Tell that the Camera is not mandatory to install the application -->
|
<!-- Tell that the Camera is not mandatory to install the application -->
|
||||||
|
|
|
@ -48,6 +48,7 @@ import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks
|
||||||
import im.vector.riotx.features.notifications.NotificationDrawerManager
|
import im.vector.riotx.features.notifications.NotificationDrawerManager
|
||||||
import im.vector.riotx.features.notifications.NotificationUtils
|
import im.vector.riotx.features.notifications.NotificationUtils
|
||||||
import im.vector.riotx.features.notifications.PushRuleTriggerListener
|
import im.vector.riotx.features.notifications.PushRuleTriggerListener
|
||||||
|
import im.vector.riotx.features.popup.PopupAlertManager
|
||||||
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
|
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
|
||||||
import im.vector.riotx.features.session.SessionListener
|
import im.vector.riotx.features.session.SessionListener
|
||||||
import im.vector.riotx.features.settings.VectorPreferences
|
import im.vector.riotx.features.settings.VectorPreferences
|
||||||
|
@ -77,6 +78,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
|
||||||
@Inject lateinit var notificationUtils: NotificationUtils
|
@Inject lateinit var notificationUtils: NotificationUtils
|
||||||
@Inject lateinit var appStateHandler: AppStateHandler
|
@Inject lateinit var appStateHandler: AppStateHandler
|
||||||
@Inject lateinit var rxConfig: RxConfig
|
@Inject lateinit var rxConfig: RxConfig
|
||||||
|
@Inject lateinit var popupAlertManager: PopupAlertManager
|
||||||
lateinit var vectorComponent: VectorComponent
|
lateinit var vectorComponent: VectorComponent
|
||||||
private var fontThreadHandler: Handler? = null
|
private var fontThreadHandler: Handler? = null
|
||||||
|
|
||||||
|
@ -102,7 +104,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
|
||||||
BigImageViewer.initialize(GlideImageLoader.with(applicationContext))
|
BigImageViewer.initialize(GlideImageLoader.with(applicationContext))
|
||||||
EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
|
EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
|
||||||
EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
|
EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
|
||||||
registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks())
|
registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks(popupAlertManager))
|
||||||
val fontRequest = FontRequest(
|
val fontRequest = FontRequest(
|
||||||
"com.google.android.gms.fonts",
|
"com.google.android.gms.fonts",
|
||||||
"com.google.android.gms",
|
"com.google.android.gms",
|
||||||
|
|
|
@ -26,6 +26,8 @@ import im.vector.riotx.features.attachments.preview.AttachmentsPreviewFragment
|
||||||
import im.vector.riotx.features.createdirect.CreateDirectRoomDirectoryUsersFragment
|
import im.vector.riotx.features.createdirect.CreateDirectRoomDirectoryUsersFragment
|
||||||
import im.vector.riotx.features.createdirect.CreateDirectRoomKnownUsersFragment
|
import im.vector.riotx.features.createdirect.CreateDirectRoomKnownUsersFragment
|
||||||
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment
|
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment
|
||||||
|
import im.vector.riotx.features.crypto.verification.cancel.VerificationCancelFragment
|
||||||
|
import im.vector.riotx.features.crypto.verification.cancel.VerificationNotMeFragment
|
||||||
import im.vector.riotx.features.crypto.verification.choose.VerificationChooseMethodFragment
|
import im.vector.riotx.features.crypto.verification.choose.VerificationChooseMethodFragment
|
||||||
import im.vector.riotx.features.crypto.verification.conclusion.VerificationConclusionFragment
|
import im.vector.riotx.features.crypto.verification.conclusion.VerificationConclusionFragment
|
||||||
import im.vector.riotx.features.crypto.verification.emoji.VerificationEmojiCodeFragment
|
import im.vector.riotx.features.crypto.verification.emoji.VerificationEmojiCodeFragment
|
||||||
|
@ -336,6 +338,16 @@ interface FragmentModule {
|
||||||
@FragmentKey(VerificationConclusionFragment::class)
|
@FragmentKey(VerificationConclusionFragment::class)
|
||||||
fun bindVerificationConclusionFragment(fragment: VerificationConclusionFragment): Fragment
|
fun bindVerificationConclusionFragment(fragment: VerificationConclusionFragment): Fragment
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FragmentKey(VerificationCancelFragment::class)
|
||||||
|
fun bindVerificationCancelFragment(fragment: VerificationCancelFragment): Fragment
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FragmentKey(VerificationNotMeFragment::class)
|
||||||
|
fun bindVerificationNotMeFragment(fragment: VerificationNotMeFragment): Fragment
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@FragmentKey(QrCodeScannerFragment::class)
|
@FragmentKey(QrCodeScannerFragment::class)
|
||||||
|
|
|
@ -45,6 +45,7 @@ import im.vector.riotx.features.notifications.NotificationBroadcastReceiver
|
||||||
import im.vector.riotx.features.notifications.NotificationDrawerManager
|
import im.vector.riotx.features.notifications.NotificationDrawerManager
|
||||||
import im.vector.riotx.features.notifications.NotificationUtils
|
import im.vector.riotx.features.notifications.NotificationUtils
|
||||||
import im.vector.riotx.features.notifications.PushRuleTriggerListener
|
import im.vector.riotx.features.notifications.PushRuleTriggerListener
|
||||||
|
import im.vector.riotx.features.popup.PopupAlertManager
|
||||||
import im.vector.riotx.features.rageshake.BugReporter
|
import im.vector.riotx.features.rageshake.BugReporter
|
||||||
import im.vector.riotx.features.rageshake.VectorFileLogger
|
import im.vector.riotx.features.rageshake.VectorFileLogger
|
||||||
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
|
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
|
||||||
|
@ -128,6 +129,8 @@ interface VectorComponent {
|
||||||
|
|
||||||
fun emojiDataSource(): EmojiDataSource
|
fun emojiDataSource(): EmojiDataSource
|
||||||
|
|
||||||
|
fun alertManager() : PopupAlertManager
|
||||||
|
|
||||||
@Component.Factory
|
@Component.Factory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(@BindsInstance context: Context): VectorComponent
|
fun create(@BindsInstance context: Context): VectorComponent
|
||||||
|
|
|
@ -16,10 +16,13 @@
|
||||||
|
|
||||||
package im.vector.riotx.core.extensions
|
package im.vector.riotx.core.extensions
|
||||||
|
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.airbnb.epoxy.EpoxyController
|
import com.airbnb.epoxy.EpoxyController
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.features.themes.ThemeUtils
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply a Vertical LinearLayout Manager to the recyclerView and set the adapter from the epoxy controller
|
* Apply a Vertical LinearLayout Manager to the recyclerView and set the adapter from the epoxy controller
|
||||||
|
@ -40,7 +43,13 @@ fun RecyclerView.configureWith(epoxyController: EpoxyController,
|
||||||
itemAnimator?.let { this.itemAnimator = it }
|
itemAnimator?.let { this.itemAnimator = it }
|
||||||
}
|
}
|
||||||
if (showDivider) {
|
if (showDivider) {
|
||||||
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
|
addItemDecoration(
|
||||||
|
DividerItemDecoration(context, DividerItemDecoration.VERTICAL).apply {
|
||||||
|
ContextCompat.getDrawable(context, ThemeUtils.getResourceId(context, R.drawable.divider_horizontal_light))?.let {
|
||||||
|
setDrawable(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
setHasFixedSize(hasFixedSize)
|
setHasFixedSize(hasFixedSize)
|
||||||
adapter = epoxyController.adapter
|
adapter = epoxyController.adapter
|
||||||
|
|
|
@ -18,20 +18,13 @@ package im.vector.riotx.features.attachments
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.kbeanie.multipicker.api.Picker.PICK_AUDIO
|
|
||||||
import com.kbeanie.multipicker.api.Picker.PICK_CONTACT
|
|
||||||
import com.kbeanie.multipicker.api.Picker.PICK_FILE
|
|
||||||
import com.kbeanie.multipicker.api.Picker.PICK_IMAGE_CAMERA
|
|
||||||
import com.kbeanie.multipicker.api.Picker.PICK_IMAGE_DEVICE
|
|
||||||
import com.kbeanie.multipicker.core.ImagePickerImpl
|
|
||||||
import com.kbeanie.multipicker.core.PickerManager
|
|
||||||
import com.kbeanie.multipicker.utils.IntentUtils
|
|
||||||
import im.vector.matrix.android.BuildConfig
|
import im.vector.matrix.android.BuildConfig
|
||||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
import im.vector.riotx.core.platform.Restorable
|
import im.vector.riotx.core.platform.Restorable
|
||||||
import im.vector.riotx.features.attachments.AttachmentsHelper.Callback
|
import im.vector.riotx.multipicker.MultiPicker
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
private const val CAPTURE_PATH_KEY = "CAPTURE_PATH_KEY"
|
private const val CAPTURE_PATH_KEY = "CAPTURE_PATH_KEY"
|
||||||
|
@ -39,20 +32,8 @@ private const val PENDING_TYPE_KEY = "PENDING_TYPE_KEY"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class helps to handle attachments by providing simple methods.
|
* This class helps to handle attachments by providing simple methods.
|
||||||
* The process is asynchronous and you must implement [Callback] methods to get the data or a failure.
|
|
||||||
*/
|
*/
|
||||||
class AttachmentsHelper private constructor(private val context: Context,
|
class AttachmentsHelper(val context: Context, val callback: Callback) : Restorable {
|
||||||
private val pickerManagerFactory: PickerManagerFactory) : Restorable {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun create(fragment: Fragment, callback: Callback): AttachmentsHelper {
|
|
||||||
return AttachmentsHelper(fragment.requireContext(), FragmentPickerManagerFactory(fragment, callback))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun create(activity: Activity, callback: Callback): AttachmentsHelper {
|
|
||||||
return AttachmentsHelper(activity, ActivityPickerManagerFactory(activity, callback))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun onContactAttachmentReady(contactAttachment: ContactAttachment) {
|
fun onContactAttachmentReady(contactAttachment: ContactAttachment) {
|
||||||
|
@ -66,39 +47,15 @@ class AttachmentsHelper private constructor(private val context: Context,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture path allows to handle camera image picking. It must be restored if the activity gets killed.
|
// Capture path allows to handle camera image picking. It must be restored if the activity gets killed.
|
||||||
private var capturePath: String? = null
|
private var captureUri: Uri? = null
|
||||||
// The pending type is set if we have to handle permission request. It must be restored if the activity gets killed.
|
// The pending type is set if we have to handle permission request. It must be restored if the activity gets killed.
|
||||||
var pendingType: AttachmentTypeSelectorView.Type? = null
|
var pendingType: AttachmentTypeSelectorView.Type? = null
|
||||||
|
|
||||||
private val imagePicker by lazy {
|
|
||||||
pickerManagerFactory.createImagePicker()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val videoPicker by lazy {
|
|
||||||
pickerManagerFactory.createVideoPicker()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val cameraImagePicker by lazy {
|
|
||||||
pickerManagerFactory.createCameraImagePicker()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val filePicker by lazy {
|
|
||||||
pickerManagerFactory.createFilePicker()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val audioPicker by lazy {
|
|
||||||
pickerManagerFactory.createAudioPicker()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val contactPicker by lazy {
|
|
||||||
pickerManagerFactory.createContactPicker()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restorable
|
// Restorable
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
capturePath?.also {
|
captureUri?.also {
|
||||||
outState.putString(CAPTURE_PATH_KEY, it)
|
outState.putParcelable(CAPTURE_PATH_KEY, it)
|
||||||
}
|
}
|
||||||
pendingType?.also {
|
pendingType?.also {
|
||||||
outState.putSerializable(PENDING_TYPE_KEY, it)
|
outState.putSerializable(PENDING_TYPE_KEY, it)
|
||||||
|
@ -106,10 +63,7 @@ class AttachmentsHelper private constructor(private val context: Context,
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
|
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
|
||||||
capturePath = savedInstanceState?.getString(CAPTURE_PATH_KEY)
|
captureUri = savedInstanceState?.getParcelable(CAPTURE_PATH_KEY) as? Uri
|
||||||
if (capturePath != null) {
|
|
||||||
cameraImagePicker.reinitialize(capturePath)
|
|
||||||
}
|
|
||||||
pendingType = savedInstanceState?.getSerializable(PENDING_TYPE_KEY) as? AttachmentTypeSelectorView.Type
|
pendingType = savedInstanceState?.getSerializable(PENDING_TYPE_KEY) as? AttachmentTypeSelectorView.Type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,36 +72,36 @@ class AttachmentsHelper private constructor(private val context: Context,
|
||||||
/**
|
/**
|
||||||
* Starts the process for handling file picking
|
* Starts the process for handling file picking
|
||||||
*/
|
*/
|
||||||
fun selectFile() {
|
fun selectFile(fragment: Fragment) {
|
||||||
filePicker.pickFile()
|
MultiPicker.get(MultiPicker.FILE).startWith(fragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the process for handling image picking
|
* Starts the process for handling image picking
|
||||||
*/
|
*/
|
||||||
fun selectGallery() {
|
fun selectGallery(fragment: Fragment) {
|
||||||
imagePicker.pickImage()
|
MultiPicker.get(MultiPicker.IMAGE).startWith(fragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the process for handling audio picking
|
* Starts the process for handling audio picking
|
||||||
*/
|
*/
|
||||||
fun selectAudio() {
|
fun selectAudio(fragment: Fragment) {
|
||||||
audioPicker.pickAudio()
|
MultiPicker.get(MultiPicker.AUDIO).startWith(fragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the process for handling capture image picking
|
* Starts the process for handling capture image picking
|
||||||
*/
|
*/
|
||||||
fun openCamera() {
|
fun openCamera(fragment: Fragment) {
|
||||||
capturePath = cameraImagePicker.pickImage()
|
captureUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(fragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the process for handling contact picking
|
* Starts the process for handling contact picking
|
||||||
*/
|
*/
|
||||||
fun selectContact() {
|
fun selectContact(fragment: Fragment) {
|
||||||
contactPicker.pickContact()
|
MultiPicker.get(MultiPicker.CONTACT).startWith(fragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -157,14 +111,58 @@ class AttachmentsHelper private constructor(private val context: Context,
|
||||||
*/
|
*/
|
||||||
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
if (resultCode == Activity.RESULT_OK) {
|
||||||
val pickerManager = getPickerManagerForRequestCode(requestCode)
|
when (requestCode) {
|
||||||
if (pickerManager != null) {
|
MultiPicker.REQUEST_CODE_PICK_FILE -> {
|
||||||
if (pickerManager is ImagePickerImpl) {
|
callback.onContentAttachmentsReady(
|
||||||
pickerManager.reinitialize(capturePath)
|
MultiPicker.get(MultiPicker.FILE)
|
||||||
|
.getSelectedFiles(context, requestCode, resultCode, data)
|
||||||
|
.map { it.toContentAttachmentData() }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
pickerManager.submit(data)
|
MultiPicker.REQUEST_CODE_PICK_AUDIO -> {
|
||||||
return true
|
callback.onContentAttachmentsReady(
|
||||||
|
MultiPicker.get(MultiPicker.AUDIO)
|
||||||
|
.getSelectedFiles(context, requestCode, resultCode, data)
|
||||||
|
.map { it.toContentAttachmentData() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
MultiPicker.REQUEST_CODE_PICK_CONTACT -> {
|
||||||
|
MultiPicker.get(MultiPicker.CONTACT)
|
||||||
|
.getSelectedFiles(context, requestCode, resultCode, data)
|
||||||
|
.firstOrNull()
|
||||||
|
?.toContactAttachment()
|
||||||
|
?.let {
|
||||||
|
callback.onContactAttachmentReady(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MultiPicker.REQUEST_CODE_PICK_IMAGE -> {
|
||||||
|
callback.onContentAttachmentsReady(
|
||||||
|
MultiPicker.get(MultiPicker.IMAGE)
|
||||||
|
.getSelectedFiles(context, requestCode, resultCode, data)
|
||||||
|
.map { it.toContentAttachmentData() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
MultiPicker.REQUEST_CODE_TAKE_PHOTO -> {
|
||||||
|
captureUri?.let { captureUri ->
|
||||||
|
MultiPicker.get(MultiPicker.CAMERA)
|
||||||
|
.getTakenPhoto(context, requestCode, resultCode, captureUri)
|
||||||
|
?.let {
|
||||||
|
callback.onContentAttachmentsReady(
|
||||||
|
listOf(it).map { it.toContentAttachmentData() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MultiPicker.REQUEST_CODE_PICK_VIDEO -> {
|
||||||
|
callback.onContentAttachmentsReady(
|
||||||
|
MultiPicker.get(MultiPicker.VIDEO)
|
||||||
|
.getSelectedFiles(context, requestCode, resultCode, data)
|
||||||
|
.map { it.toContentAttachmentData() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> return false
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -174,39 +172,35 @@ class AttachmentsHelper private constructor(private val context: Context,
|
||||||
*
|
*
|
||||||
* @return true if it can handle the intent data, false otherwise
|
* @return true if it can handle the intent data, false otherwise
|
||||||
*/
|
*/
|
||||||
fun handleShareIntent(intent: Intent): Boolean {
|
fun handleShareIntent(context: Context, intent: Intent): Boolean {
|
||||||
val type = intent.resolveType(context) ?: return false
|
val type = intent.resolveType(context) ?: return false
|
||||||
if (type.startsWith("image")) {
|
if (type.startsWith("image")) {
|
||||||
imagePicker.submit(safeShareIntent(intent))
|
callback.onContentAttachmentsReady(
|
||||||
|
MultiPicker.get(MultiPicker.IMAGE).getIncomingFiles(context, intent).map {
|
||||||
|
it.toContentAttachmentData()
|
||||||
|
}
|
||||||
|
)
|
||||||
} else if (type.startsWith("video")) {
|
} else if (type.startsWith("video")) {
|
||||||
videoPicker.submit(safeShareIntent(intent))
|
callback.onContentAttachmentsReady(
|
||||||
|
MultiPicker.get(MultiPicker.VIDEO).getIncomingFiles(context, intent).map {
|
||||||
|
it.toContentAttachmentData()
|
||||||
|
}
|
||||||
|
)
|
||||||
} else if (type.startsWith("audio")) {
|
} else if (type.startsWith("audio")) {
|
||||||
videoPicker.submit(safeShareIntent(intent))
|
callback.onContentAttachmentsReady(
|
||||||
|
MultiPicker.get(MultiPicker.AUDIO).getIncomingFiles(context, intent).map {
|
||||||
|
it.toContentAttachmentData()
|
||||||
|
}
|
||||||
|
)
|
||||||
} else if (type.startsWith("application") || type.startsWith("file") || type.startsWith("*")) {
|
} else if (type.startsWith("application") || type.startsWith("file") || type.startsWith("*")) {
|
||||||
filePicker.submit(safeShareIntent(intent))
|
callback.onContentAttachmentsReady(
|
||||||
|
MultiPicker.get(MultiPicker.FILE).getIncomingFiles(context, intent).map {
|
||||||
|
it.toContentAttachmentData()
|
||||||
|
}
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun safeShareIntent(intent: Intent): Intent {
|
|
||||||
// Work around for getPickerIntentForSharing doing NPE in android 10
|
|
||||||
return try {
|
|
||||||
IntentUtils.getPickerIntentForSharing(intent)
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
intent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getPickerManagerForRequestCode(requestCode: Int): PickerManager? {
|
|
||||||
return when (requestCode) {
|
|
||||||
PICK_IMAGE_DEVICE -> imagePicker
|
|
||||||
PICK_IMAGE_CAMERA -> cameraImagePicker
|
|
||||||
PICK_FILE -> filePicker
|
|
||||||
PICK_CONTACT -> contactPicker
|
|
||||||
PICK_AUDIO -> audioPicker
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,51 +16,48 @@
|
||||||
|
|
||||||
package im.vector.riotx.features.attachments
|
package im.vector.riotx.features.attachments
|
||||||
|
|
||||||
import com.kbeanie.multipicker.api.entity.ChosenAudio
|
|
||||||
import com.kbeanie.multipicker.api.entity.ChosenContact
|
|
||||||
import com.kbeanie.multipicker.api.entity.ChosenFile
|
|
||||||
import com.kbeanie.multipicker.api.entity.ChosenImage
|
|
||||||
import com.kbeanie.multipicker.api.entity.ChosenVideo
|
|
||||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
|
import im.vector.riotx.multipicker.entity.MultiPickerAudioType
|
||||||
|
import im.vector.riotx.multipicker.entity.MultiPickerBaseType
|
||||||
|
import im.vector.riotx.multipicker.entity.MultiPickerContactType
|
||||||
|
import im.vector.riotx.multipicker.entity.MultiPickerFileType
|
||||||
|
import im.vector.riotx.multipicker.entity.MultiPickerImageType
|
||||||
|
import im.vector.riotx.multipicker.entity.MultiPickerVideoType
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
fun ChosenContact.toContactAttachment(): ContactAttachment {
|
fun MultiPickerContactType.toContactAttachment(): ContactAttachment {
|
||||||
return ContactAttachment(
|
return ContactAttachment(
|
||||||
displayName = displayName,
|
displayName = displayName,
|
||||||
photoUri = photoUri,
|
photoUri = photoUri,
|
||||||
emails = emails.toList(),
|
emails = emailList.toList(),
|
||||||
phones = phones.toList()
|
phones = phoneNumberList.toList()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ChosenFile.toContentAttachmentData(): ContentAttachmentData {
|
fun MultiPickerFileType.toContentAttachmentData(): ContentAttachmentData {
|
||||||
if (mimeType == null) Timber.w("No mimeType")
|
if (mimeType == null) Timber.w("No mimeType")
|
||||||
return ContentAttachmentData(
|
return ContentAttachmentData(
|
||||||
path = originalPath,
|
|
||||||
mimeType = mimeType,
|
mimeType = mimeType,
|
||||||
type = mapType(),
|
type = mapType(),
|
||||||
size = size,
|
size = size,
|
||||||
date = createdAt?.time ?: System.currentTimeMillis(),
|
|
||||||
name = displayName,
|
name = displayName,
|
||||||
queryUri = queryUri
|
queryUri = contentUri
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ChosenAudio.toContentAttachmentData(): ContentAttachmentData {
|
fun MultiPickerAudioType.toContentAttachmentData(): ContentAttachmentData {
|
||||||
if (mimeType == null) Timber.w("No mimeType")
|
if (mimeType == null) Timber.w("No mimeType")
|
||||||
return ContentAttachmentData(
|
return ContentAttachmentData(
|
||||||
path = originalPath,
|
|
||||||
mimeType = mimeType,
|
mimeType = mimeType,
|
||||||
type = mapType(),
|
type = mapType(),
|
||||||
size = size,
|
size = size,
|
||||||
date = createdAt?.time ?: System.currentTimeMillis(),
|
|
||||||
name = displayName,
|
name = displayName,
|
||||||
duration = duration,
|
duration = duration,
|
||||||
queryUri = queryUri
|
queryUri = contentUri
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ChosenFile.mapType(): ContentAttachmentData.Type {
|
private fun MultiPickerBaseType.mapType(): ContentAttachmentData.Type {
|
||||||
return when {
|
return when {
|
||||||
mimeType?.startsWith("image/") == true -> ContentAttachmentData.Type.IMAGE
|
mimeType?.startsWith("image/") == true -> ContentAttachmentData.Type.IMAGE
|
||||||
mimeType?.startsWith("video/") == true -> ContentAttachmentData.Type.VIDEO
|
mimeType?.startsWith("video/") == true -> ContentAttachmentData.Type.VIDEO
|
||||||
|
@ -69,10 +66,9 @@ private fun ChosenFile.mapType(): ContentAttachmentData.Type {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ChosenImage.toContentAttachmentData(): ContentAttachmentData {
|
fun MultiPickerImageType.toContentAttachmentData(): ContentAttachmentData {
|
||||||
if (mimeType == null) Timber.w("No mimeType")
|
if (mimeType == null) Timber.w("No mimeType")
|
||||||
return ContentAttachmentData(
|
return ContentAttachmentData(
|
||||||
path = originalPath,
|
|
||||||
mimeType = mimeType,
|
mimeType = mimeType,
|
||||||
type = mapType(),
|
type = mapType(),
|
||||||
name = displayName,
|
name = displayName,
|
||||||
|
@ -80,23 +76,20 @@ fun ChosenImage.toContentAttachmentData(): ContentAttachmentData {
|
||||||
height = height.toLong(),
|
height = height.toLong(),
|
||||||
width = width.toLong(),
|
width = width.toLong(),
|
||||||
exifOrientation = orientation,
|
exifOrientation = orientation,
|
||||||
date = createdAt?.time ?: System.currentTimeMillis(),
|
queryUri = contentUri
|
||||||
queryUri = queryUri
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ChosenVideo.toContentAttachmentData(): ContentAttachmentData {
|
fun MultiPickerVideoType.toContentAttachmentData(): ContentAttachmentData {
|
||||||
if (mimeType == null) Timber.w("No mimeType")
|
if (mimeType == null) Timber.w("No mimeType")
|
||||||
return ContentAttachmentData(
|
return ContentAttachmentData(
|
||||||
path = originalPath,
|
|
||||||
mimeType = mimeType,
|
mimeType = mimeType,
|
||||||
type = ContentAttachmentData.Type.VIDEO,
|
type = ContentAttachmentData.Type.VIDEO,
|
||||||
size = size,
|
size = size,
|
||||||
date = createdAt?.time ?: System.currentTimeMillis(),
|
|
||||||
height = height.toLong(),
|
height = height.toLong(),
|
||||||
width = width.toLong(),
|
width = width.toLong(),
|
||||||
duration = duration,
|
duration = duration,
|
||||||
name = displayName,
|
name = displayName,
|
||||||
queryUri = queryUri
|
queryUri = contentUri
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,96 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2019 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package im.vector.riotx.features.attachments
|
|
||||||
|
|
||||||
import com.kbeanie.multipicker.api.callbacks.AudioPickerCallback
|
|
||||||
import com.kbeanie.multipicker.api.callbacks.ContactPickerCallback
|
|
||||||
import com.kbeanie.multipicker.api.callbacks.FilePickerCallback
|
|
||||||
import com.kbeanie.multipicker.api.callbacks.ImagePickerCallback
|
|
||||||
import com.kbeanie.multipicker.api.callbacks.VideoPickerCallback
|
|
||||||
import com.kbeanie.multipicker.api.entity.ChosenAudio
|
|
||||||
import com.kbeanie.multipicker.api.entity.ChosenContact
|
|
||||||
import com.kbeanie.multipicker.api.entity.ChosenFile
|
|
||||||
import com.kbeanie.multipicker.api.entity.ChosenImage
|
|
||||||
import com.kbeanie.multipicker.api.entity.ChosenVideo
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class delegates the PickerManager callbacks to an [AttachmentsHelper.Callback]
|
|
||||||
*/
|
|
||||||
class AttachmentsPickerCallback(private val callback: AttachmentsHelper.Callback)
|
|
||||||
: ImagePickerCallback,
|
|
||||||
FilePickerCallback,
|
|
||||||
VideoPickerCallback,
|
|
||||||
AudioPickerCallback,
|
|
||||||
ContactPickerCallback {
|
|
||||||
|
|
||||||
override fun onContactChosen(contact: ChosenContact?) {
|
|
||||||
if (contact == null) {
|
|
||||||
callback.onAttachmentsProcessFailed()
|
|
||||||
} else {
|
|
||||||
val contactAttachment = contact.toContactAttachment()
|
|
||||||
callback.onContactAttachmentReady(contactAttachment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAudiosChosen(audios: MutableList<ChosenAudio>?) {
|
|
||||||
if (audios.isNullOrEmpty()) {
|
|
||||||
callback.onAttachmentsProcessFailed()
|
|
||||||
} else {
|
|
||||||
val attachments = audios.map {
|
|
||||||
it.toContentAttachmentData()
|
|
||||||
}
|
|
||||||
callback.onContentAttachmentsReady(attachments)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFilesChosen(files: MutableList<ChosenFile>?) {
|
|
||||||
if (files.isNullOrEmpty()) {
|
|
||||||
callback.onAttachmentsProcessFailed()
|
|
||||||
} else {
|
|
||||||
val attachments = files.map {
|
|
||||||
it.toContentAttachmentData()
|
|
||||||
}
|
|
||||||
callback.onContentAttachmentsReady(attachments)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onImagesChosen(images: MutableList<ChosenImage>?) {
|
|
||||||
if (images.isNullOrEmpty()) {
|
|
||||||
callback.onAttachmentsProcessFailed()
|
|
||||||
} else {
|
|
||||||
val attachments = images.map {
|
|
||||||
it.toContentAttachmentData()
|
|
||||||
}
|
|
||||||
callback.onContentAttachmentsReady(attachments)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onVideosChosen(videos: MutableList<ChosenVideo>?) {
|
|
||||||
if (videos.isNullOrEmpty()) {
|
|
||||||
callback.onAttachmentsProcessFailed()
|
|
||||||
} else {
|
|
||||||
val attachments = videos.map {
|
|
||||||
it.toContentAttachmentData()
|
|
||||||
}
|
|
||||||
callback.onContentAttachmentsReady(attachments)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(error: String?) {
|
|
||||||
callback.onAttachmentsProcessFailed()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,134 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2019 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package im.vector.riotx.features.attachments
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import com.kbeanie.multipicker.api.AudioPicker
|
|
||||||
import com.kbeanie.multipicker.api.CameraImagePicker
|
|
||||||
import com.kbeanie.multipicker.api.ContactPicker
|
|
||||||
import com.kbeanie.multipicker.api.FilePicker
|
|
||||||
import com.kbeanie.multipicker.api.ImagePicker
|
|
||||||
import com.kbeanie.multipicker.api.VideoPicker
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Factory for creating different pickers. It allows to use with fragment or activity builders.
|
|
||||||
*/
|
|
||||||
interface PickerManagerFactory {
|
|
||||||
|
|
||||||
fun createImagePicker(): ImagePicker
|
|
||||||
|
|
||||||
fun createCameraImagePicker(): CameraImagePicker
|
|
||||||
|
|
||||||
fun createVideoPicker(): VideoPicker
|
|
||||||
|
|
||||||
fun createFilePicker(): FilePicker
|
|
||||||
|
|
||||||
fun createAudioPicker(): AudioPicker
|
|
||||||
|
|
||||||
fun createContactPicker(): ContactPicker
|
|
||||||
}
|
|
||||||
|
|
||||||
class ActivityPickerManagerFactory(private val activity: Activity, callback: AttachmentsHelper.Callback) : PickerManagerFactory {
|
|
||||||
|
|
||||||
private val attachmentsPickerCallback = AttachmentsPickerCallback(callback)
|
|
||||||
|
|
||||||
override fun createImagePicker(): ImagePicker {
|
|
||||||
return ImagePicker(activity).also {
|
|
||||||
it.setImagePickerCallback(attachmentsPickerCallback)
|
|
||||||
it.allowMultiple()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createCameraImagePicker(): CameraImagePicker {
|
|
||||||
return CameraImagePicker(activity).also {
|
|
||||||
it.setImagePickerCallback(attachmentsPickerCallback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createVideoPicker(): VideoPicker {
|
|
||||||
return VideoPicker(activity).also {
|
|
||||||
it.setVideoPickerCallback(attachmentsPickerCallback)
|
|
||||||
it.allowMultiple()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createFilePicker(): FilePicker {
|
|
||||||
return FilePicker(activity).also {
|
|
||||||
it.allowMultiple()
|
|
||||||
it.setFilePickerCallback(attachmentsPickerCallback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createAudioPicker(): AudioPicker {
|
|
||||||
return AudioPicker(activity).also {
|
|
||||||
it.allowMultiple()
|
|
||||||
it.setAudioPickerCallback(attachmentsPickerCallback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createContactPicker(): ContactPicker {
|
|
||||||
return ContactPicker(activity).also {
|
|
||||||
it.setContactPickerCallback(attachmentsPickerCallback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FragmentPickerManagerFactory(private val fragment: Fragment, callback: AttachmentsHelper.Callback) : PickerManagerFactory {
|
|
||||||
|
|
||||||
private val attachmentsPickerCallback = AttachmentsPickerCallback(callback)
|
|
||||||
|
|
||||||
override fun createImagePicker(): ImagePicker {
|
|
||||||
return ImagePicker(fragment).also {
|
|
||||||
it.setImagePickerCallback(attachmentsPickerCallback)
|
|
||||||
it.allowMultiple()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createCameraImagePicker(): CameraImagePicker {
|
|
||||||
return CameraImagePicker(fragment).also {
|
|
||||||
it.setImagePickerCallback(attachmentsPickerCallback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createVideoPicker(): VideoPicker {
|
|
||||||
return VideoPicker(fragment).also {
|
|
||||||
it.setVideoPickerCallback(attachmentsPickerCallback)
|
|
||||||
it.allowMultiple()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createFilePicker(): FilePicker {
|
|
||||||
return FilePicker(fragment).also {
|
|
||||||
it.allowMultiple()
|
|
||||||
it.setFilePickerCallback(attachmentsPickerCallback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createAudioPicker(): AudioPicker {
|
|
||||||
return AudioPicker(fragment).also {
|
|
||||||
it.allowMultiple()
|
|
||||||
it.setAudioPickerCallback(attachmentsPickerCallback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createContactPicker(): ContactPicker {
|
|
||||||
return ContactPicker(fragment).also {
|
|
||||||
it.setContactPickerCallback(attachmentsPickerCallback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -25,7 +25,7 @@ class AttachmentBigPreviewController @Inject constructor() : TypedEpoxyControlle
|
||||||
override fun buildModels(data: AttachmentsPreviewViewState) {
|
override fun buildModels(data: AttachmentsPreviewViewState) {
|
||||||
data.attachments.forEach {
|
data.attachments.forEach {
|
||||||
attachmentBigPreviewItem {
|
attachmentBigPreviewItem {
|
||||||
id(it.path)
|
id(it.queryUri.toString())
|
||||||
attachment(it)
|
attachment(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ class AttachmentMiniaturePreviewController @Inject constructor() : TypedEpoxyCon
|
||||||
override fun buildModels(data: AttachmentsPreviewViewState) {
|
override fun buildModels(data: AttachmentsPreviewViewState) {
|
||||||
data.attachments.forEachIndexed { index, contentAttachmentData ->
|
data.attachments.forEachIndexed { index, contentAttachmentData ->
|
||||||
attachmentMiniaturePreviewItem {
|
attachmentMiniaturePreviewItem {
|
||||||
id(contentAttachmentData.path)
|
id(contentAttachmentData.queryUri.toString())
|
||||||
attachment(contentAttachmentData)
|
attachment(contentAttachmentData)
|
||||||
checked(data.currentAttachmentIndex == index)
|
checked(data.currentAttachmentIndex == index)
|
||||||
clickListener { _ ->
|
clickListener { _ ->
|
||||||
|
|
|
@ -33,11 +33,10 @@ abstract class AttachmentPreviewItem<H : AttachmentPreviewItem.Holder> : VectorE
|
||||||
abstract val attachment: ContentAttachmentData
|
abstract val attachment: ContentAttachmentData
|
||||||
|
|
||||||
override fun bind(holder: H) {
|
override fun bind(holder: H) {
|
||||||
val path = attachment.path
|
|
||||||
if (attachment.type == ContentAttachmentData.Type.VIDEO || attachment.type == ContentAttachmentData.Type.IMAGE) {
|
if (attachment.type == ContentAttachmentData.Type.VIDEO || attachment.type == ContentAttachmentData.Type.IMAGE) {
|
||||||
Glide.with(holder.view.context)
|
Glide.with(holder.view.context)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.load(path)
|
.load(attachment.queryUri)
|
||||||
.apply(RequestOptions().frame(0))
|
.apply(RequestOptions().frame(0))
|
||||||
.into(holder.imageView)
|
.into(holder.imageView)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -17,10 +17,11 @@
|
||||||
|
|
||||||
package im.vector.riotx.features.attachments.preview
|
package im.vector.riotx.features.attachments.preview
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import im.vector.riotx.core.platform.VectorViewModelAction
|
import im.vector.riotx.core.platform.VectorViewModelAction
|
||||||
|
|
||||||
sealed class AttachmentsPreviewAction : VectorViewModelAction {
|
sealed class AttachmentsPreviewAction : VectorViewModelAction {
|
||||||
object RemoveCurrentAttachment : AttachmentsPreviewAction()
|
object RemoveCurrentAttachment : AttachmentsPreviewAction()
|
||||||
data class SetCurrentAttachment(val index: Int): AttachmentsPreviewAction()
|
data class SetCurrentAttachment(val index: Int): AttachmentsPreviewAction()
|
||||||
data class UpdatePathOfCurrentAttachment(val newPath: String): AttachmentsPreviewAction()
|
data class UpdatePathOfCurrentAttachment(val newUri: Uri): AttachmentsPreviewAction()
|
||||||
}
|
}
|
||||||
|
|
|
@ -172,9 +172,9 @@ class AttachmentsPreviewFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleCropResult(result: Intent) {
|
private fun handleCropResult(result: Intent) {
|
||||||
val resultPath = UCrop.getOutput(result)?.path
|
val resultUri = UCrop.getOutput(result)
|
||||||
if (resultPath != null) {
|
if (resultUri != null) {
|
||||||
viewModel.handle(AttachmentsPreviewAction.UpdatePathOfCurrentAttachment(resultPath))
|
viewModel.handle(AttachmentsPreviewAction.UpdatePathOfCurrentAttachment(resultUri))
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(requireContext(), "Cannot retrieve cropped value", Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), "Cannot retrieve cropped value", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
@ -202,8 +202,7 @@ class AttachmentsPreviewFragment @Inject constructor(
|
||||||
private fun doHandleEditAction() = withState(viewModel) {
|
private fun doHandleEditAction() = withState(viewModel) {
|
||||||
val currentAttachment = it.attachments.getOrNull(it.currentAttachmentIndex) ?: return@withState
|
val currentAttachment = it.attachments.getOrNull(it.currentAttachmentIndex) ?: return@withState
|
||||||
val destinationFile = File(requireContext().cacheDir, "${currentAttachment.name}_edited_image_${System.currentTimeMillis()}")
|
val destinationFile = File(requireContext().cacheDir, "${currentAttachment.name}_edited_image_${System.currentTimeMillis()}")
|
||||||
// Note: using currentAttachment.queryUri.toUri() make the app crash when sharing from Google Photos
|
val uri = currentAttachment.queryUri
|
||||||
val uri = File(currentAttachment.path).toUri()
|
|
||||||
UCrop.of(uri, destinationFile.toUri())
|
UCrop.of(uri, destinationFile.toUri())
|
||||||
.withOptions(
|
.withOptions(
|
||||||
UCrop.Options()
|
UCrop.Options()
|
||||||
|
|
|
@ -62,7 +62,7 @@ class AttachmentsPreviewViewModel @AssistedInject constructor(@Assisted initialS
|
||||||
private fun handleUpdatePathOfCurrentAttachment(action: AttachmentsPreviewAction.UpdatePathOfCurrentAttachment) = withState {
|
private fun handleUpdatePathOfCurrentAttachment(action: AttachmentsPreviewAction.UpdatePathOfCurrentAttachment) = withState {
|
||||||
val attachments = it.attachments.mapIndexed { index, contentAttachmentData ->
|
val attachments = it.attachments.mapIndexed { index, contentAttachmentData ->
|
||||||
if (index == it.currentAttachmentIndex) {
|
if (index == it.currentAttachmentIndex) {
|
||||||
contentAttachmentData.copy(path = action.newPath)
|
contentAttachmentData.copy(queryUri = action.newUri)
|
||||||
} else {
|
} else {
|
||||||
contentAttachmentData
|
contentAttachmentData
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||||
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.riotx.R
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.features.popup.DefaultVectorAlert
|
||||||
import im.vector.riotx.features.popup.PopupAlertManager
|
import im.vector.riotx.features.popup.PopupAlertManager
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
|
@ -54,7 +55,7 @@ import javax.inject.Singleton
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class KeyRequestHandler @Inject constructor(private val context: Context)
|
class KeyRequestHandler @Inject constructor(private val context: Context, private val popupAlertManager: PopupAlertManager)
|
||||||
: GossipingRequestListener,
|
: GossipingRequestListener,
|
||||||
VerificationService.Listener {
|
VerificationService.Listener {
|
||||||
|
|
||||||
|
@ -118,9 +119,9 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deviceInfo.isUnknown) {
|
if (deviceInfo.isUnknown) {
|
||||||
session?.cryptoService()?.setDeviceVerification(DeviceTrustLevel(false, false), userId, deviceId)
|
session?.cryptoService()?.setDeviceVerification(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false), userId, deviceId)
|
||||||
|
|
||||||
deviceInfo.trustLevel = DeviceTrustLevel(false, false)
|
deviceInfo.trustLevel = DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false)
|
||||||
|
|
||||||
// can we get more info on this device?
|
// can we get more info on this device?
|
||||||
session?.cryptoService()?.getDevicesList(object : MatrixCallback<DevicesListResponse> {
|
session?.cryptoService()?.getDevicesList(object : MatrixCallback<DevicesListResponse> {
|
||||||
|
@ -188,7 +189,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val alert = PopupAlertManager.VectorAlert(
|
val alert = DefaultVectorAlert(
|
||||||
alertManagerId(userId, deviceId),
|
alertManagerId(userId, deviceId),
|
||||||
context.getString(R.string.key_share_request),
|
context.getString(R.string.key_share_request),
|
||||||
dialogText,
|
dialogText,
|
||||||
|
@ -210,7 +211,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
|
||||||
denyAllRequests(mappingKey)
|
denyAllRequests(mappingKey)
|
||||||
})
|
})
|
||||||
|
|
||||||
PopupAlertManager.postVectorAlert(alert)
|
popupAlertManager.postVectorAlert(alert)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun denyAllRequests(mappingKey: String) {
|
private fun denyAllRequests(mappingKey: String) {
|
||||||
|
@ -250,7 +251,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
|
||||||
&& it.requestId == request.requestId
|
&& it.requestId == request.requestId
|
||||||
}
|
}
|
||||||
if (alertsToRequests[alertMgrUniqueKey]?.isEmpty() == true) {
|
if (alertsToRequests[alertMgrUniqueKey]?.isEmpty() == true) {
|
||||||
PopupAlertManager.cancelAlert(alertMgrUniqueKey)
|
popupAlertManager.cancelAlert(alertMgrUniqueKey)
|
||||||
alertsToRequests.remove(keyForMap(userId, deviceId))
|
alertsToRequests.remove(keyForMap(userId, deviceId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -261,7 +262,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
|
||||||
if (state == VerificationTxState.Verified) {
|
if (state == VerificationTxState.Verified) {
|
||||||
// ok it's verified, see if we have key request for that
|
// ok it's verified, see if we have key request for that
|
||||||
shareAllSessions("${tx.otherDeviceId}${tx.otherUserId}")
|
shareAllSessions("${tx.otherDeviceId}${tx.otherUserId}")
|
||||||
PopupAlertManager.cancelAlert("ikr_${tx.otherDeviceId}${tx.otherUserId}")
|
popupAlertManager.cancelAlert("ikr_${tx.otherDeviceId}${tx.otherUserId}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// should do it with QR tx also
|
// should do it with QR tx also
|
||||||
|
@ -271,7 +272,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
|
||||||
override fun markedAsManuallyVerified(userId: String, deviceId: String) {
|
override fun markedAsManuallyVerified(userId: String, deviceId: String) {
|
||||||
// accept related requests
|
// accept related requests
|
||||||
shareAllSessions(keyForMap(userId, deviceId))
|
shareAllSessions(keyForMap(userId, deviceId))
|
||||||
PopupAlertManager.cancelAlert(alertManagerId(userId, deviceId))
|
popupAlertManager.cancelAlert(alertManagerId(userId, deviceId))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun keyForMap(userId: String, deviceId: String) = "$deviceId$userId"
|
private fun keyForMap(userId: String, deviceId: String) = "$deviceId$userId"
|
||||||
|
|
|
@ -17,15 +17,17 @@ package im.vector.riotx.features.crypto.verification
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
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.PendingVerificationRequest
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationService
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationService
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.PendingVerificationRequest
|
import im.vector.matrix.android.api.util.toMatrixItem
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.platform.VectorBaseActivity
|
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||||
import im.vector.riotx.features.home.room.detail.RoomDetailActivity
|
import im.vector.riotx.features.home.room.detail.RoomDetailActivity
|
||||||
import im.vector.riotx.features.home.room.detail.RoomDetailArgs
|
import im.vector.riotx.features.home.room.detail.RoomDetailArgs
|
||||||
import im.vector.riotx.features.popup.PopupAlertManager
|
import im.vector.riotx.features.popup.PopupAlertManager
|
||||||
|
import im.vector.riotx.features.popup.VerificationVectorAlert
|
||||||
import im.vector.riotx.features.themes.ThemeUtils
|
import im.vector.riotx.features.themes.ThemeUtils
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
@ -34,7 +36,9 @@ import javax.inject.Singleton
|
||||||
* Listens to the VerificationManager and add a new notification when an incoming request is detected.
|
* Listens to the VerificationManager and add a new notification when an incoming request is detected.
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
class IncomingVerificationRequestHandler @Inject constructor(private val context: Context) : VerificationService.Listener {
|
class IncomingVerificationRequestHandler @Inject constructor(
|
||||||
|
private val context: Context,
|
||||||
|
private val popupAlertManager: PopupAlertManager) : VerificationService.Listener {
|
||||||
|
|
||||||
private var session: Session? = null
|
private var session: Session? = null
|
||||||
|
|
||||||
|
@ -58,7 +62,7 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context
|
||||||
val name = session?.getUser(tx.otherUserId)?.displayName
|
val name = session?.getUser(tx.otherUserId)?.displayName
|
||||||
?: tx.otherUserId
|
?: tx.otherUserId
|
||||||
|
|
||||||
val alert = PopupAlertManager.VectorAlert(
|
val alert = VerificationVectorAlert(
|
||||||
uid,
|
uid,
|
||||||
context.getString(R.string.sas_incoming_request_notif_title),
|
context.getString(R.string.sas_incoming_request_notif_title),
|
||||||
context.getString(R.string.sas_incoming_request_notif_content, name),
|
context.getString(R.string.sas_incoming_request_notif_content, name),
|
||||||
|
@ -68,12 +72,14 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context
|
||||||
// TODO a bit too hugly :/
|
// TODO a bit too hugly :/
|
||||||
activity.supportFragmentManager.findFragmentByTag(VerificationBottomSheet.WAITING_SELF_VERIF_TAG)?.let {
|
activity.supportFragmentManager.findFragmentByTag(VerificationBottomSheet.WAITING_SELF_VERIF_TAG)?.let {
|
||||||
false.also {
|
false.also {
|
||||||
PopupAlertManager.cancelAlert(uid)
|
popupAlertManager.cancelAlert(uid)
|
||||||
}
|
}
|
||||||
} ?: true
|
} ?: true
|
||||||
} else true
|
} else true
|
||||||
})
|
})
|
||||||
.apply {
|
.apply {
|
||||||
|
matrixItem = session?.getUser(tx.otherUserId)?.toMatrixItem()
|
||||||
|
|
||||||
contentAction = Runnable {
|
contentAction = Runnable {
|
||||||
(weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
|
(weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
|
||||||
it.navigator.performDeviceVerification(it, tx.otherUserId, tx.transactionId)
|
it.navigator.performDeviceVerification(it, tx.otherUserId, tx.transactionId)
|
||||||
|
@ -99,11 +105,11 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context
|
||||||
// 10mn expiration
|
// 10mn expiration
|
||||||
expirationTimestamp = System.currentTimeMillis() + (10 * 60 * 1000L)
|
expirationTimestamp = System.currentTimeMillis() + (10 * 60 * 1000L)
|
||||||
}
|
}
|
||||||
PopupAlertManager.postVectorAlert(alert)
|
popupAlertManager.postVectorAlert(alert)
|
||||||
}
|
}
|
||||||
is VerificationTxState.TerminalTxState -> {
|
is VerificationTxState.TerminalTxState -> {
|
||||||
// cancel related notification
|
// cancel related notification
|
||||||
PopupAlertManager.cancelAlert(uid)
|
popupAlertManager.cancelAlert(uid)
|
||||||
}
|
}
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
|
@ -115,7 +121,7 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context
|
||||||
val name = session?.getUser(pr.otherUserId)?.displayName
|
val name = session?.getUser(pr.otherUserId)?.displayName
|
||||||
?: pr.otherUserId
|
?: pr.otherUserId
|
||||||
|
|
||||||
val alert = PopupAlertManager.VectorAlert(
|
val alert = VerificationVectorAlert(
|
||||||
uniqueIdForVerificationRequest(pr),
|
uniqueIdForVerificationRequest(pr),
|
||||||
context.getString(R.string.sas_incoming_request_notif_title),
|
context.getString(R.string.sas_incoming_request_notif_title),
|
||||||
"$name(${pr.otherUserId})",
|
"$name(${pr.otherUserId})",
|
||||||
|
@ -128,6 +134,8 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context
|
||||||
} else true
|
} else true
|
||||||
})
|
})
|
||||||
.apply {
|
.apply {
|
||||||
|
matrixItem = session?.getUser(pr.otherUserId)?.toMatrixItem()
|
||||||
|
|
||||||
contentAction = Runnable {
|
contentAction = Runnable {
|
||||||
(weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
|
(weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
|
||||||
val roomId = pr.roomId
|
val roomId = pr.roomId
|
||||||
|
@ -148,14 +156,14 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context
|
||||||
// 5mn expiration
|
// 5mn expiration
|
||||||
expirationTimestamp = System.currentTimeMillis() + (5 * 60 * 1000L)
|
expirationTimestamp = System.currentTimeMillis() + (5 * 60 * 1000L)
|
||||||
}
|
}
|
||||||
PopupAlertManager.postVectorAlert(alert)
|
popupAlertManager.postVectorAlert(alert)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun verificationRequestUpdated(pr: PendingVerificationRequest) {
|
override fun verificationRequestUpdated(pr: PendingVerificationRequest) {
|
||||||
// If an incoming request is readied (by another device?) we should discard the alert
|
// If an incoming request is readied (by another device?) we should discard the alert
|
||||||
if (pr.isIncoming && (pr.isReady || pr.handledByOtherSession)) {
|
if (pr.isIncoming && (pr.isReady || pr.handledByOtherSession)) {
|
||||||
PopupAlertManager.cancelAlert(uniqueIdForVerificationRequest(pr))
|
popupAlertManager.cancelAlert(uniqueIdForVerificationRequest(pr))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,9 +16,11 @@
|
||||||
package im.vector.riotx.features.crypto.verification
|
package im.vector.riotx.features.crypto.verification
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.app.Dialog
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import android.view.KeyEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
@ -34,19 +36,24 @@ import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
|
import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
|
||||||
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
||||||
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
||||||
|
import im.vector.matrix.android.api.session.crypto.verification.CancelCode
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.di.ScreenComponent
|
import im.vector.riotx.core.di.ScreenComponent
|
||||||
import im.vector.riotx.core.extensions.commitTransaction
|
import im.vector.riotx.core.extensions.commitTransaction
|
||||||
import im.vector.riotx.core.extensions.exhaustive
|
import im.vector.riotx.core.extensions.exhaustive
|
||||||
|
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||||
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
|
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
|
||||||
import im.vector.riotx.features.crypto.quads.SharedSecureStorageActivity
|
import im.vector.riotx.features.crypto.quads.SharedSecureStorageActivity
|
||||||
|
import im.vector.riotx.features.crypto.verification.cancel.VerificationCancelFragment
|
||||||
|
import im.vector.riotx.features.crypto.verification.cancel.VerificationNotMeFragment
|
||||||
import im.vector.riotx.features.crypto.verification.choose.VerificationChooseMethodFragment
|
import im.vector.riotx.features.crypto.verification.choose.VerificationChooseMethodFragment
|
||||||
import im.vector.riotx.features.crypto.verification.conclusion.VerificationConclusionFragment
|
import im.vector.riotx.features.crypto.verification.conclusion.VerificationConclusionFragment
|
||||||
import im.vector.riotx.features.crypto.verification.emoji.VerificationEmojiCodeFragment
|
import im.vector.riotx.features.crypto.verification.emoji.VerificationEmojiCodeFragment
|
||||||
import im.vector.riotx.features.crypto.verification.qrconfirmation.VerificationQrScannedByOtherFragment
|
import im.vector.riotx.features.crypto.verification.qrconfirmation.VerificationQrScannedByOtherFragment
|
||||||
import im.vector.riotx.features.crypto.verification.request.VerificationRequestFragment
|
import im.vector.riotx.features.crypto.verification.request.VerificationRequestFragment
|
||||||
import im.vector.riotx.features.home.AvatarRenderer
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
|
import im.vector.riotx.features.settings.VectorSettingsActivity
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -58,6 +65,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||||
data class VerificationArgs(
|
data class VerificationArgs(
|
||||||
val otherUserId: String,
|
val otherUserId: String,
|
||||||
val verificationId: String? = null,
|
val verificationId: String? = null,
|
||||||
|
val verificationLocalId: String? = null,
|
||||||
val roomId: String? = null,
|
val roomId: String? = null,
|
||||||
// Special mode where UX should show loading wheel until other session sends a request/tx
|
// Special mode where UX should show loading wheel until other session sends a request/tx
|
||||||
val selfVerificationMode: Boolean = false
|
val selfVerificationMode: Boolean = false
|
||||||
|
@ -80,13 +88,17 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||||
lateinit var otherUserNameText: TextView
|
lateinit var otherUserNameText: TextView
|
||||||
|
|
||||||
@BindView(R.id.verificationRequestShield)
|
@BindView(R.id.verificationRequestShield)
|
||||||
lateinit var otherUserShield: View
|
lateinit var otherUserShield: ImageView
|
||||||
|
|
||||||
@BindView(R.id.verificationRequestAvatar)
|
@BindView(R.id.verificationRequestAvatar)
|
||||||
lateinit var otherUserAvatarImageView: ImageView
|
lateinit var otherUserAvatarImageView: ImageView
|
||||||
|
|
||||||
override fun getLayoutResId() = R.layout.bottom_sheet_verification
|
override fun getLayoutResId() = R.layout.bottom_sheet_verification
|
||||||
|
|
||||||
|
init {
|
||||||
|
isCancelable = false
|
||||||
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
@ -110,10 +122,27 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||||
.show()
|
.show()
|
||||||
Unit
|
Unit
|
||||||
}
|
}
|
||||||
|
VerificationBottomSheetViewEvents.GoToSettings -> {
|
||||||
|
dismiss()
|
||||||
|
(activity as? VectorBaseActivity)?.navigator?.openSettings(requireContext(), VectorSettingsActivity.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY)
|
||||||
|
}
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
return super.onCreateDialog(savedInstanceState).apply {
|
||||||
|
setOnKeyListener { _, keyCode, keyEvent ->
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_BACK && keyEvent.action == KeyEvent.ACTION_UP) {
|
||||||
|
viewModel.queryCancel()
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
if (resultCode == Activity.RESULT_OK && requestCode == SECRET_REQUEST_CODE) {
|
if (resultCode == Activity.RESULT_OK && requestCode == SECRET_REQUEST_CODE) {
|
||||||
data?.getStringExtra(SharedSecureStorageActivity.EXTRA_DATA_RESULT)?.let {
|
data?.getStringExtra(SharedSecureStorageActivity.EXTRA_DATA_RESULT)?.let {
|
||||||
|
@ -127,15 +156,16 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||||
|
|
||||||
state.otherUserMxItem?.let { matrixItem ->
|
state.otherUserMxItem?.let { matrixItem ->
|
||||||
if (state.isMe) {
|
if (state.isMe) {
|
||||||
|
avatarRenderer.render(matrixItem, otherUserAvatarImageView)
|
||||||
if (state.sasTransactionState == VerificationTxState.Verified
|
if (state.sasTransactionState == VerificationTxState.Verified
|
||||||
|| state.qrTransactionState == VerificationTxState.Verified
|
|| state.qrTransactionState == VerificationTxState.Verified
|
||||||
|| state.verifiedFromPrivateKeys) {
|
|| state.verifiedFromPrivateKeys) {
|
||||||
otherUserAvatarImageView.setImageResource(R.drawable.ic_shield_trusted)
|
otherUserShield.setImageResource(R.drawable.ic_shield_trusted)
|
||||||
} else {
|
} else {
|
||||||
otherUserAvatarImageView.setImageResource(R.drawable.ic_shield_warning)
|
otherUserShield.setImageResource(R.drawable.ic_shield_warning)
|
||||||
}
|
}
|
||||||
otherUserNameText.text = getString(R.string.complete_security)
|
otherUserNameText.text = getString(R.string.complete_security)
|
||||||
otherUserShield.isVisible = false
|
otherUserShield.isVisible = true
|
||||||
} else {
|
} else {
|
||||||
avatarRenderer.render(matrixItem, otherUserAvatarImageView)
|
avatarRenderer.render(matrixItem, otherUserAvatarImageView)
|
||||||
|
|
||||||
|
@ -149,6 +179,18 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.userThinkItsNotHim) {
|
||||||
|
otherUserNameText.text = getString(R.string.dialog_title_warning)
|
||||||
|
showFragment(VerificationNotMeFragment::class, Bundle())
|
||||||
|
return@withState
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.userWantsToCancel) {
|
||||||
|
otherUserNameText.text = getString(R.string.are_you_sure)
|
||||||
|
showFragment(VerificationCancelFragment::class, Bundle())
|
||||||
|
return@withState
|
||||||
|
}
|
||||||
|
|
||||||
if (state.selfVerificationMode && state.verifiedFromPrivateKeys) {
|
if (state.selfVerificationMode && state.verifiedFromPrivateKeys) {
|
||||||
showFragment(VerificationConclusionFragment::class, Bundle().apply {
|
showFragment(VerificationConclusionFragment::class, Bundle().apply {
|
||||||
putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(true, null, state.isMe))
|
putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(true, null, state.isMe))
|
||||||
|
@ -222,7 +264,14 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||||
// Transaction has not yet started
|
// Transaction has not yet started
|
||||||
if (state.pendingRequest.invoke()?.cancelConclusion != null) {
|
if (state.pendingRequest.invoke()?.cancelConclusion != null) {
|
||||||
// The request has been declined, we should dismiss
|
// The request has been declined, we should dismiss
|
||||||
dismiss()
|
otherUserNameText.text = getString(R.string.verification_cancelled)
|
||||||
|
showFragment(VerificationConclusionFragment::class, Bundle().apply {
|
||||||
|
putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(
|
||||||
|
false,
|
||||||
|
state.pendingRequest.invoke()?.cancelConclusion?.value ?: CancelCode.User.value,
|
||||||
|
state.isMe))
|
||||||
|
})
|
||||||
|
return@withState
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it's an outgoing
|
// If it's an outgoing
|
||||||
|
@ -267,6 +316,10 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun dismiss() {
|
||||||
|
super.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
const val SECRET_REQUEST_CODE = 101
|
const val SECRET_REQUEST_CODE = 101
|
||||||
|
|
|
@ -24,5 +24,6 @@ import im.vector.riotx.core.platform.VectorViewEvents
|
||||||
sealed class VerificationBottomSheetViewEvents : VectorViewEvents {
|
sealed class VerificationBottomSheetViewEvents : VectorViewEvents {
|
||||||
object Dismiss : VerificationBottomSheetViewEvents()
|
object Dismiss : VerificationBottomSheetViewEvents()
|
||||||
object AccessSecretStore : VerificationBottomSheetViewEvents()
|
object AccessSecretStore : VerificationBottomSheetViewEvents()
|
||||||
|
object GoToSettings : VerificationBottomSheetViewEvents()
|
||||||
data class ModalError(val errorMessage: CharSequence) : VerificationBottomSheetViewEvents()
|
data class ModalError(val errorMessage: CharSequence) : VerificationBottomSheetViewEvents()
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,9 @@ import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
|
import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
|
||||||
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
||||||
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
||||||
|
import im.vector.matrix.android.api.session.crypto.verification.CancelCode
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction
|
import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction
|
||||||
|
import im.vector.matrix.android.api.session.crypto.verification.PendingVerificationRequest
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.QrCodeVerificationTransaction
|
import im.vector.matrix.android.api.session.crypto.verification.QrCodeVerificationTransaction
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.SasVerificationTransaction
|
import im.vector.matrix.android.api.session.crypto.verification.SasVerificationTransaction
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod
|
||||||
|
@ -44,7 +46,6 @@ import im.vector.matrix.android.api.util.MatrixItem
|
||||||
import im.vector.matrix.android.api.util.toMatrixItem
|
import im.vector.matrix.android.api.util.toMatrixItem
|
||||||
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64
|
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64
|
||||||
import im.vector.matrix.android.internal.crypto.crosssigning.isVerified
|
import im.vector.matrix.android.internal.crypto.crosssigning.isVerified
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.PendingVerificationRequest
|
|
||||||
import im.vector.riotx.core.extensions.exhaustive
|
import im.vector.riotx.core.extensions.exhaustive
|
||||||
import im.vector.riotx.core.platform.VectorViewModel
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -60,7 +61,10 @@ data class VerificationBottomSheetViewState(
|
||||||
// true when we display the loading and we wait for the other (incoming request)
|
// true when we display the loading and we wait for the other (incoming request)
|
||||||
val selfVerificationMode: Boolean = false,
|
val selfVerificationMode: Boolean = false,
|
||||||
val verifiedFromPrivateKeys: Boolean = false,
|
val verifiedFromPrivateKeys: Boolean = false,
|
||||||
val isMe: Boolean = false
|
val isMe: Boolean = false,
|
||||||
|
val currentDeviceCanCrossSign: Boolean = false,
|
||||||
|
val userWantsToCancel: Boolean = false,
|
||||||
|
val userThinkItsNotHim: Boolean = false
|
||||||
) : MvRxState
|
) : MvRxState
|
||||||
|
|
||||||
class VerificationBottomSheetViewModel @AssistedInject constructor(
|
class VerificationBottomSheetViewModel @AssistedInject constructor(
|
||||||
|
@ -111,7 +115,8 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
|
||||||
pendingRequest = if (pr != null) Success(pr) else Uninitialized,
|
pendingRequest = if (pr != null) Success(pr) else Uninitialized,
|
||||||
selfVerificationMode = selfVerificationMode,
|
selfVerificationMode = selfVerificationMode,
|
||||||
roomId = args.roomId,
|
roomId = args.roomId,
|
||||||
isMe = args.otherUserId == session.myUserId
|
isMe = args.otherUserId == session.myUserId,
|
||||||
|
currentDeviceCanCrossSign = session.cryptoService().crossSigningService().canCrossSign()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,6 +142,57 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
|
||||||
args: VerificationBottomSheet.VerificationArgs): VerificationBottomSheetViewModel
|
args: VerificationBottomSheet.VerificationArgs): VerificationBottomSheetViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun queryCancel() = withState {
|
||||||
|
if (it.userThinkItsNotHim) {
|
||||||
|
setState {
|
||||||
|
copy(userThinkItsNotHim = false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setState {
|
||||||
|
copy(userWantsToCancel = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun confirmCancel() = withState { state ->
|
||||||
|
cancelAllPendingVerifications(state)
|
||||||
|
_viewEvents.post(VerificationBottomSheetViewEvents.Dismiss)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cancelAllPendingVerifications(state: VerificationBottomSheetViewState) {
|
||||||
|
session.cryptoService()
|
||||||
|
.verificationService().getExistingVerificationRequest(state.otherUserMxItem?.id ?: "", state.transactionId)?.let {
|
||||||
|
session.cryptoService().verificationService().cancelVerificationRequest(it)
|
||||||
|
}
|
||||||
|
session.cryptoService()
|
||||||
|
.verificationService()
|
||||||
|
.getExistingTransaction(state.otherUserMxItem?.id ?: "", state.transactionId ?: "")
|
||||||
|
?.cancel(CancelCode.User)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun continueFromCancel() {
|
||||||
|
setState {
|
||||||
|
copy(userWantsToCancel = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun continueFromWasNotMe() {
|
||||||
|
setState {
|
||||||
|
copy(userThinkItsNotHim = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun itWasNotMe() {
|
||||||
|
setState {
|
||||||
|
copy(userThinkItsNotHim = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun goToSettings() = withState { state ->
|
||||||
|
cancelAllPendingVerifications(state)
|
||||||
|
_viewEvents.post(VerificationBottomSheetViewEvents.GoToSettings)
|
||||||
|
}
|
||||||
|
|
||||||
companion object : MvRxViewModelFactory<VerificationBottomSheetViewModel, VerificationBottomSheetViewState> {
|
companion object : MvRxViewModelFactory<VerificationBottomSheetViewModel, VerificationBottomSheetViewState> {
|
||||||
|
|
||||||
override fun create(viewModelContext: ViewModelContext, state: VerificationBottomSheetViewState): VerificationBottomSheetViewModel? {
|
override fun create(viewModelContext: ViewModelContext, state: VerificationBottomSheetViewState): VerificationBottomSheetViewModel? {
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.features.crypto.verification.cancel
|
||||||
|
|
||||||
|
import com.airbnb.epoxy.EpoxyController
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.epoxy.dividerItem
|
||||||
|
import im.vector.riotx.core.resources.ColorProvider
|
||||||
|
import im.vector.riotx.core.resources.StringProvider
|
||||||
|
import im.vector.riotx.features.crypto.verification.VerificationBottomSheetViewState
|
||||||
|
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
|
||||||
|
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class VerificationCancelController @Inject constructor(
|
||||||
|
private val stringProvider: StringProvider,
|
||||||
|
private val colorProvider: ColorProvider
|
||||||
|
) : EpoxyController() {
|
||||||
|
|
||||||
|
var listener: Listener? = null
|
||||||
|
|
||||||
|
private var viewState: VerificationBottomSheetViewState? = null
|
||||||
|
|
||||||
|
fun update(viewState: VerificationBottomSheetViewState) {
|
||||||
|
this.viewState = viewState
|
||||||
|
requestModelBuild()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun buildModels() {
|
||||||
|
val state = viewState ?: return
|
||||||
|
|
||||||
|
if (state.isMe) {
|
||||||
|
if (state.currentDeviceCanCrossSign) {
|
||||||
|
bottomSheetVerificationNoticeItem {
|
||||||
|
id("notice")
|
||||||
|
notice(stringProvider.getString(R.string.verify_cancel_self_verification_from_trusted))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bottomSheetVerificationNoticeItem {
|
||||||
|
id("notice")
|
||||||
|
notice(stringProvider.getString(R.string.verify_cancel_self_verification_from_untrusted))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bottomSheetVerificationNoticeItem {
|
||||||
|
id("notice")
|
||||||
|
notice(stringProvider.getString(R.string.verify_cancel_self_verification_from_untrusted))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dividerItem {
|
||||||
|
id("sep0")
|
||||||
|
}
|
||||||
|
|
||||||
|
bottomSheetVerificationActionItem {
|
||||||
|
id("cancel")
|
||||||
|
title(stringProvider.getString(R.string.cancel))
|
||||||
|
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
|
||||||
|
iconRes(R.drawable.ic_arrow_right)
|
||||||
|
iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
|
||||||
|
listener { listener?.onTapCancel() }
|
||||||
|
}
|
||||||
|
|
||||||
|
dividerItem {
|
||||||
|
id("sep1")
|
||||||
|
}
|
||||||
|
|
||||||
|
bottomSheetVerificationActionItem {
|
||||||
|
id("continue")
|
||||||
|
title(stringProvider.getString(R.string._continue))
|
||||||
|
titleColor(colorProvider.getColor(R.color.riotx_positive_accent))
|
||||||
|
iconRes(R.drawable.ic_arrow_right)
|
||||||
|
iconColor(colorProvider.getColor(R.color.riotx_positive_accent))
|
||||||
|
listener { listener?.onTapContinue() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
fun onTapCancel()
|
||||||
|
fun onTapContinue()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.features.crypto.verification.cancel
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import com.airbnb.mvrx.parentFragmentViewModel
|
||||||
|
import com.airbnb.mvrx.withState
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.extensions.cleanup
|
||||||
|
import im.vector.riotx.core.extensions.configureWith
|
||||||
|
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
|
import im.vector.riotx.features.crypto.verification.VerificationBottomSheetViewModel
|
||||||
|
import kotlinx.android.synthetic.main.bottom_sheet_verification_child_fragment.*
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class VerificationCancelFragment @Inject constructor(
|
||||||
|
val controller: VerificationCancelController
|
||||||
|
) : VectorBaseFragment(), VerificationCancelController.Listener {
|
||||||
|
|
||||||
|
private val viewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class)
|
||||||
|
|
||||||
|
override fun getLayoutResId() = R.layout.bottom_sheet_verification_child_fragment
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
setupRecyclerView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
bottomSheetVerificationRecyclerView.cleanup()
|
||||||
|
controller.listener = null
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupRecyclerView() {
|
||||||
|
bottomSheetVerificationRecyclerView.configureWith(controller, hasFixedSize = false, disableItemAnimation = true)
|
||||||
|
controller.listener = this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invalidate() = withState(viewModel) { state ->
|
||||||
|
controller.update(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTapCancel() {
|
||||||
|
viewModel.confirmCancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTapContinue() {
|
||||||
|
viewModel.continueFromCancel()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.features.crypto.verification.cancel
|
||||||
|
|
||||||
|
import com.airbnb.epoxy.EpoxyController
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.epoxy.dividerItem
|
||||||
|
import im.vector.riotx.core.resources.ColorProvider
|
||||||
|
import im.vector.riotx.core.resources.StringProvider
|
||||||
|
import im.vector.riotx.features.crypto.verification.VerificationBottomSheetViewState
|
||||||
|
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
|
||||||
|
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem
|
||||||
|
import im.vector.riotx.features.html.EventHtmlRenderer
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class VerificationNotMeController @Inject constructor(
|
||||||
|
private val stringProvider: StringProvider,
|
||||||
|
private val colorProvider: ColorProvider,
|
||||||
|
private val eventHtmlRenderer: EventHtmlRenderer
|
||||||
|
) : EpoxyController() {
|
||||||
|
|
||||||
|
var listener: Listener? = null
|
||||||
|
|
||||||
|
private var viewState: VerificationBottomSheetViewState? = null
|
||||||
|
|
||||||
|
fun update(viewState: VerificationBottomSheetViewState) {
|
||||||
|
this.viewState = viewState
|
||||||
|
requestModelBuild()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun buildModels() {
|
||||||
|
bottomSheetVerificationNoticeItem {
|
||||||
|
id("notice")
|
||||||
|
notice(eventHtmlRenderer.render(stringProvider.getString(R.string.verify_not_me_self_verification)))
|
||||||
|
}
|
||||||
|
|
||||||
|
dividerItem {
|
||||||
|
id("sep0")
|
||||||
|
}
|
||||||
|
|
||||||
|
bottomSheetVerificationActionItem {
|
||||||
|
id("skip")
|
||||||
|
title(stringProvider.getString(R.string.skip))
|
||||||
|
titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
|
||||||
|
iconRes(R.drawable.ic_arrow_right)
|
||||||
|
iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
|
||||||
|
listener { listener?.onTapSkip() }
|
||||||
|
}
|
||||||
|
|
||||||
|
dividerItem {
|
||||||
|
id("sep1")
|
||||||
|
}
|
||||||
|
|
||||||
|
bottomSheetVerificationActionItem {
|
||||||
|
id("settings")
|
||||||
|
title(stringProvider.getString(R.string.settings))
|
||||||
|
titleColor(colorProvider.getColor(R.color.riotx_positive_accent))
|
||||||
|
iconRes(R.drawable.ic_arrow_right)
|
||||||
|
iconColor(colorProvider.getColor(R.color.riotx_positive_accent))
|
||||||
|
listener { listener?.onTapSettings() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
fun onTapSkip()
|
||||||
|
fun onTapSettings()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.features.crypto.verification.cancel
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import com.airbnb.mvrx.parentFragmentViewModel
|
||||||
|
import com.airbnb.mvrx.withState
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.extensions.cleanup
|
||||||
|
import im.vector.riotx.core.extensions.configureWith
|
||||||
|
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
|
import im.vector.riotx.features.crypto.verification.VerificationBottomSheetViewModel
|
||||||
|
import kotlinx.android.synthetic.main.bottom_sheet_verification_child_fragment.*
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class VerificationNotMeFragment @Inject constructor(
|
||||||
|
val controller: VerificationNotMeController
|
||||||
|
) : VectorBaseFragment(), VerificationNotMeController.Listener {
|
||||||
|
|
||||||
|
private val viewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class)
|
||||||
|
|
||||||
|
override fun getLayoutResId() = R.layout.bottom_sheet_verification_child_fragment
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
setupRecyclerView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
bottomSheetVerificationRecyclerView.cleanup()
|
||||||
|
controller.listener = null
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupRecyclerView() {
|
||||||
|
bottomSheetVerificationRecyclerView.configureWith(controller, hasFixedSize = false, disableItemAnimation = true)
|
||||||
|
controller.listener = this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invalidate() = withState(viewModel) { state ->
|
||||||
|
controller.update(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTapSkip() {
|
||||||
|
viewModel.continueFromWasNotMe()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTapSettings() {
|
||||||
|
viewModel.goToSettings()
|
||||||
|
}
|
||||||
|
}
|
|
@ -95,10 +95,27 @@ class VerificationChooseMethodController @Inject constructor(
|
||||||
listener { listener?.doVerifyBySas() }
|
listener { listener?.doVerifyBySas() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.isMe && state.canCrossSign) {
|
||||||
|
dividerItem {
|
||||||
|
id("sep_notMe")
|
||||||
|
}
|
||||||
|
|
||||||
|
bottomSheetVerificationActionItem {
|
||||||
|
id("wasnote")
|
||||||
|
title(stringProvider.getString(R.string.verify_new_session_was_not_me))
|
||||||
|
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
|
||||||
|
subTitle(stringProvider.getString(R.string.verify_new_session_compromized))
|
||||||
|
iconRes(R.drawable.ic_arrow_right)
|
||||||
|
iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
|
||||||
|
listener { listener?.onClickOnWasNotMe() }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Listener {
|
interface Listener {
|
||||||
fun openCamera()
|
fun openCamera()
|
||||||
fun doVerifyBySas()
|
fun doVerifyBySas()
|
||||||
|
fun onClickOnWasNotMe()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,6 +89,10 @@ class VerificationChooseMethodFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onClickOnWasNotMe() {
|
||||||
|
sharedViewModel.itWasNotMe()
|
||||||
|
}
|
||||||
|
|
||||||
private fun doOpenQRCodeScanner() {
|
private fun doOpenQRCodeScanner() {
|
||||||
QrCodeScannerActivity.startForResult(this)
|
QrCodeScannerActivity.startForResult(this)
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,9 @@ data class VerificationChooseMethodViewState(
|
||||||
val otherCanShowQrCode: Boolean = false,
|
val otherCanShowQrCode: Boolean = false,
|
||||||
val otherCanScanQrCode: Boolean = false,
|
val otherCanScanQrCode: Boolean = false,
|
||||||
val qrCodeText: String? = null,
|
val qrCodeText: String? = null,
|
||||||
val SASModeAvailable: Boolean = false
|
val SASModeAvailable: Boolean = false,
|
||||||
|
val isMe: Boolean = false,
|
||||||
|
val canCrossSign: Boolean = false
|
||||||
) : MvRxState
|
) : MvRxState
|
||||||
|
|
||||||
class VerificationChooseMethodViewModel @AssistedInject constructor(
|
class VerificationChooseMethodViewModel @AssistedInject constructor(
|
||||||
|
@ -61,6 +63,10 @@ class VerificationChooseMethodViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun verificationRequestCreated(pr: PendingVerificationRequest) {
|
||||||
|
verificationRequestUpdated(pr)
|
||||||
|
}
|
||||||
|
|
||||||
override fun verificationRequestUpdated(pr: PendingVerificationRequest) = withState { state ->
|
override fun verificationRequestUpdated(pr: PendingVerificationRequest) = withState { state ->
|
||||||
val pvr = session.cryptoService().verificationService().getExistingVerificationRequest(state.otherUserId, state.transactionId)
|
val pvr = session.cryptoService().verificationService().getExistingVerificationRequest(state.otherUserId, state.transactionId)
|
||||||
|
|
||||||
|
@ -103,6 +109,8 @@ class VerificationChooseMethodViewModel @AssistedInject constructor(
|
||||||
val qrCodeVerificationTransaction = verificationService.getExistingTransaction(args.otherUserId, args.verificationId ?: "")
|
val qrCodeVerificationTransaction = verificationService.getExistingTransaction(args.otherUserId, args.verificationId ?: "")
|
||||||
|
|
||||||
return VerificationChooseMethodViewState(otherUserId = args.otherUserId,
|
return VerificationChooseMethodViewState(otherUserId = args.otherUserId,
|
||||||
|
isMe = session.myUserId == pvr?.otherUserId,
|
||||||
|
canCrossSign = session.cryptoService().crossSigningService().canCrossSign(),
|
||||||
transactionId = args.verificationId ?: "",
|
transactionId = args.verificationId ?: "",
|
||||||
otherCanShowQrCode = pvr?.otherCanShowQrCode().orFalse(),
|
otherCanShowQrCode = pvr?.otherCanShowQrCode().orFalse(),
|
||||||
otherCanScanQrCode = pvr?.otherCanScanQrCode().orFalse(),
|
otherCanScanQrCode = pvr?.otherCanScanQrCode().orFalse(),
|
||||||
|
|
|
@ -58,6 +58,8 @@ class VerificationConclusionController @Inject constructor(
|
||||||
id("image")
|
id("image")
|
||||||
imageRes(R.drawable.ic_shield_trusted)
|
imageRes(R.drawable.ic_shield_trusted)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bottomDone()
|
||||||
}
|
}
|
||||||
ConclusionState.WARNING -> {
|
ConclusionState.WARNING -> {
|
||||||
bottomSheetVerificationNoticeItem {
|
bottomSheetVerificationNoticeItem {
|
||||||
|
@ -74,10 +76,32 @@ class VerificationConclusionController @Inject constructor(
|
||||||
id("warning_notice")
|
id("warning_notice")
|
||||||
notice(eventHtmlRenderer.render(stringProvider.getString(R.string.verification_conclusion_compromised)))
|
notice(eventHtmlRenderer.render(stringProvider.getString(R.string.verification_conclusion_compromised)))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
|
|
||||||
|
bottomDone()
|
||||||
|
}
|
||||||
|
ConclusionState.CANCELLED -> {
|
||||||
|
bottomSheetVerificationNoticeItem {
|
||||||
|
id("notice_cancelled")
|
||||||
|
notice(stringProvider.getString(R.string.verify_cancelled_notice))
|
||||||
|
}
|
||||||
|
|
||||||
|
dividerItem {
|
||||||
|
id("sep0")
|
||||||
|
}
|
||||||
|
|
||||||
|
bottomSheetVerificationActionItem {
|
||||||
|
id("got_it")
|
||||||
|
title(stringProvider.getString(R.string.sas_got_it))
|
||||||
|
titleColor(colorProvider.getColor(R.color.riotx_accent))
|
||||||
|
iconRes(R.drawable.ic_arrow_right)
|
||||||
|
iconColor(colorProvider.getColor(R.color.riotx_accent))
|
||||||
|
listener { listener?.onButtonTapped() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bottomDone() {
|
||||||
dividerItem {
|
dividerItem {
|
||||||
id("sep0")
|
id("sep0")
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,12 +66,7 @@ class VerificationConclusionFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun invalidate() = withState(viewModel) { state ->
|
override fun invalidate() = withState(viewModel) { state ->
|
||||||
if (state.conclusionState == ConclusionState.CANCELLED) {
|
controller.update(state)
|
||||||
// Just dismiss in this case
|
|
||||||
sharedViewModel.handle(VerificationAction.GotItConclusion)
|
|
||||||
} else {
|
|
||||||
controller.update(state)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onButtonTapped() {
|
override fun onButtonTapped() {
|
||||||
|
|
|
@ -84,11 +84,16 @@ class VerificationRequestController @Inject constructor(
|
||||||
listener { listener?.onClickDismiss() }
|
listener { listener?.onClickDismiss() }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val styledText = matrixItem.let {
|
val styledText =
|
||||||
stringProvider.getString(R.string.verification_request_notice, it.id)
|
if (state.isMe) {
|
||||||
.toSpannable()
|
stringProvider.getString(R.string.verify_new_session_notice)
|
||||||
.colorizeMatchingText(it.id, colorProvider.getColorFromAttribute(R.attr.vctr_notice_text_color))
|
} else {
|
||||||
}
|
matrixItem.let {
|
||||||
|
stringProvider.getString(R.string.verification_request_notice, it.id)
|
||||||
|
.toSpannable()
|
||||||
|
.colorizeMatchingText(it.id, colorProvider.getColorFromAttribute(R.attr.vctr_notice_text_color))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bottomSheetVerificationNoticeItem {
|
bottomSheetVerificationNoticeItem {
|
||||||
id("notice")
|
id("notice")
|
||||||
|
@ -119,18 +124,42 @@ class VerificationRequestController @Inject constructor(
|
||||||
}
|
}
|
||||||
is Success -> {
|
is Success -> {
|
||||||
if (!pr.invoke().isReady) {
|
if (!pr.invoke().isReady) {
|
||||||
bottomSheetVerificationWaitingItem {
|
if (state.isMe) {
|
||||||
id("waiting")
|
bottomSheetVerificationWaitingItem {
|
||||||
title(stringProvider.getString(R.string.verification_request_waiting_for, matrixItem.getBestName()))
|
id("waiting")
|
||||||
|
title(stringProvider.getString(R.string.verification_request_waiting))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bottomSheetVerificationWaitingItem {
|
||||||
|
id("waiting")
|
||||||
|
title(stringProvider.getString(R.string.verification_request_waiting_for, matrixItem.getBestName()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.isMe && state.currentDeviceCanCrossSign) {
|
||||||
|
dividerItem {
|
||||||
|
id("sep_notMe")
|
||||||
|
}
|
||||||
|
|
||||||
|
bottomSheetVerificationActionItem {
|
||||||
|
id("wasnote")
|
||||||
|
title(stringProvider.getString(R.string.verify_new_session_was_not_me))
|
||||||
|
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
|
||||||
|
subTitle(stringProvider.getString(R.string.verify_new_session_compromized))
|
||||||
|
iconRes(R.drawable.ic_arrow_right)
|
||||||
|
iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
|
||||||
|
listener { listener?.onClickOnWasNotMe() }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Listener {
|
interface Listener {
|
||||||
fun onClickOnVerificationStart()
|
fun onClickOnVerificationStart()
|
||||||
|
fun onClickOnWasNotMe()
|
||||||
fun onClickRecoverFromPassphrase()
|
fun onClickRecoverFromPassphrase()
|
||||||
fun onClickDismiss()
|
fun onClickDismiss()
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,4 +69,8 @@ class VerificationRequestFragment @Inject constructor(
|
||||||
override fun onClickDismiss() {
|
override fun onClickDismiss() {
|
||||||
viewModel.handle(VerificationAction.SkipVerification)
|
viewModel.handle(VerificationAction.SkipVerification)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onClickOnWasNotMe() {
|
||||||
|
viewModel.itWasNotMe()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,8 +96,8 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
|
||||||
// PRIVATE API *********************************************************************************
|
// PRIVATE API *********************************************************************************
|
||||||
|
|
||||||
private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest<Drawable> {
|
private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest<Drawable> {
|
||||||
val resolvedUrl = activeSessionHolder.getActiveSession().contentUrlResolver()
|
val resolvedUrl = activeSessionHolder.getSafeActiveSession()?.contentUrlResolver()
|
||||||
.resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE)
|
?.resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||||
|
|
||||||
return glideRequest
|
return glideRequest
|
||||||
.load(resolvedUrl)
|
.load(resolvedUrl)
|
||||||
|
|
|
@ -41,6 +41,7 @@ import im.vector.riotx.core.platform.VectorBaseActivity
|
||||||
import im.vector.riotx.core.pushers.PushersManager
|
import im.vector.riotx.core.pushers.PushersManager
|
||||||
import im.vector.riotx.features.disclaimer.showDisclaimerDialog
|
import im.vector.riotx.features.disclaimer.showDisclaimerDialog
|
||||||
import im.vector.riotx.features.notifications.NotificationDrawerManager
|
import im.vector.riotx.features.notifications.NotificationDrawerManager
|
||||||
|
import im.vector.riotx.features.popup.DefaultVectorAlert
|
||||||
import im.vector.riotx.features.popup.PopupAlertManager
|
import im.vector.riotx.features.popup.PopupAlertManager
|
||||||
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
|
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
|
||||||
import im.vector.riotx.features.settings.VectorPreferences
|
import im.vector.riotx.features.settings.VectorPreferences
|
||||||
|
@ -60,6 +61,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||||
@Inject lateinit var pushManager: PushersManager
|
@Inject lateinit var pushManager: PushersManager
|
||||||
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
|
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
|
||||||
@Inject lateinit var vectorPreferences: VectorPreferences
|
@Inject lateinit var vectorPreferences: VectorPreferences
|
||||||
|
@Inject lateinit var popupAlertManager: PopupAlertManager
|
||||||
|
|
||||||
private val drawerListener = object : DrawerLayout.SimpleDrawerListener() {
|
private val drawerListener = object : DrawerLayout.SimpleDrawerListener() {
|
||||||
override fun onDrawerStateChanged(newState: Int) {
|
override fun onDrawerStateChanged(newState: Int) {
|
||||||
|
@ -149,8 +151,8 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||||
if (crossSigningEnabledOnAccount && myCrossSigningKeys?.isTrusted() == false) {
|
if (crossSigningEnabledOnAccount && myCrossSigningKeys?.isTrusted() == false) {
|
||||||
// We need to ask
|
// We need to ask
|
||||||
sharedActionViewModel.hasDisplayedCompleteSecurityPrompt = true
|
sharedActionViewModel.hasDisplayedCompleteSecurityPrompt = true
|
||||||
PopupAlertManager.postVectorAlert(
|
popupAlertManager.postVectorAlert(
|
||||||
PopupAlertManager.VectorAlert(
|
DefaultVectorAlert(
|
||||||
uid = "completeSecurity",
|
uid = "completeSecurity",
|
||||||
title = getString(R.string.new_signin),
|
title = getString(R.string.new_signin),
|
||||||
description = getString(R.string.complete_security),
|
description = getString(R.string.complete_security),
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2019 New Vector Ltd
|
* Copyright 2019 New Vector Ltd
|
||||||
*
|
*
|
||||||
|
@ -19,8 +20,10 @@ package im.vector.riotx.features.home
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.forEachIndexed
|
import androidx.core.view.forEachIndexed
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
|
import com.airbnb.mvrx.activityViewModel
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import com.google.android.material.bottomnavigation.BottomNavigationItemView
|
import com.google.android.material.bottomnavigation.BottomNavigationItemView
|
||||||
|
@ -32,11 +35,14 @@ import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.extensions.commitTransactionNow
|
import im.vector.riotx.core.extensions.commitTransactionNow
|
||||||
import im.vector.riotx.core.glide.GlideApp
|
import im.vector.riotx.core.glide.GlideApp
|
||||||
import im.vector.riotx.core.platform.ToolbarConfigurable
|
import im.vector.riotx.core.platform.ToolbarConfigurable
|
||||||
|
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
import im.vector.riotx.core.ui.views.KeysBackupBanner
|
import im.vector.riotx.core.ui.views.KeysBackupBanner
|
||||||
import im.vector.riotx.features.home.room.list.RoomListFragment
|
import im.vector.riotx.features.home.room.list.RoomListFragment
|
||||||
import im.vector.riotx.features.home.room.list.RoomListParams
|
import im.vector.riotx.features.home.room.list.RoomListParams
|
||||||
import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView
|
import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView
|
||||||
|
import im.vector.riotx.features.popup.PopupAlertManager
|
||||||
|
import im.vector.riotx.features.popup.VerificationVectorAlert
|
||||||
import im.vector.riotx.features.workers.signout.SignOutViewModel
|
import im.vector.riotx.features.workers.signout.SignOutViewModel
|
||||||
import kotlinx.android.synthetic.main.fragment_home_detail.*
|
import kotlinx.android.synthetic.main.fragment_home_detail.*
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -48,12 +54,15 @@ private const val INDEX_ROOMS = 2
|
||||||
|
|
||||||
class HomeDetailFragment @Inject constructor(
|
class HomeDetailFragment @Inject constructor(
|
||||||
val homeDetailViewModelFactory: HomeDetailViewModel.Factory,
|
val homeDetailViewModelFactory: HomeDetailViewModel.Factory,
|
||||||
private val avatarRenderer: AvatarRenderer
|
private val avatarRenderer: AvatarRenderer,
|
||||||
|
private val alertManager: PopupAlertManager
|
||||||
) : VectorBaseFragment(), KeysBackupBanner.Delegate {
|
) : VectorBaseFragment(), KeysBackupBanner.Delegate {
|
||||||
|
|
||||||
private val unreadCounterBadgeViews = arrayListOf<UnreadCounterBadgeView>()
|
private val unreadCounterBadgeViews = arrayListOf<UnreadCounterBadgeView>()
|
||||||
|
|
||||||
private val viewModel: HomeDetailViewModel by fragmentViewModel()
|
private val viewModel: HomeDetailViewModel by fragmentViewModel()
|
||||||
|
private val unknownDeviceDetectorSharedViewModel : UnknownDeviceDetectorSharedViewModel by activityViewModel()
|
||||||
|
|
||||||
private lateinit var sharedActionViewModel: HomeSharedActionViewModel
|
private lateinit var sharedActionViewModel: HomeSharedActionViewModel
|
||||||
|
|
||||||
override fun getLayoutResId() = R.layout.fragment_home_detail
|
override fun getLayoutResId() = R.layout.fragment_home_detail
|
||||||
|
@ -77,6 +86,38 @@ class HomeDetailFragment @Inject constructor(
|
||||||
viewModel.selectSubscribe(this, HomeDetailViewState::displayMode) { displayMode ->
|
viewModel.selectSubscribe(this, HomeDetailViewState::displayMode) { displayMode ->
|
||||||
switchDisplayMode(displayMode)
|
switchDisplayMode(displayMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unknownDeviceDetectorSharedViewModel.subscribe {
|
||||||
|
it.unknownSessions.invoke()?.let { unknownDevices ->
|
||||||
|
Timber.v("## Detector - ${unknownDevices.size} Unknown sessions")
|
||||||
|
unknownDevices.forEachIndexed { index, deviceInfo ->
|
||||||
|
Timber.v("## Detector - #$index deviceId:${deviceInfo.second.deviceId} lastSeenTs:${deviceInfo.second.lastSeenTs}")
|
||||||
|
}
|
||||||
|
val uid = "Newest_Device"
|
||||||
|
alertManager.cancelAlert(uid)
|
||||||
|
if (it.canCrossSign && unknownDevices.isNotEmpty()) {
|
||||||
|
val newest = unknownDevices.first().second
|
||||||
|
val user = unknownDevices.first().first
|
||||||
|
alertManager.postVectorAlert(
|
||||||
|
VerificationVectorAlert(
|
||||||
|
uid = uid,
|
||||||
|
title = getString(R.string.new_session),
|
||||||
|
description = getString(R.string.new_session_review),
|
||||||
|
iconId = R.drawable.ic_shield_warning
|
||||||
|
).apply {
|
||||||
|
matrixItem = user
|
||||||
|
colorInt = ContextCompat.getColor(requireActivity(), R.color.riotx_accent)
|
||||||
|
contentAction = Runnable {
|
||||||
|
(weakCurrentActivity?.get() as? VectorBaseActivity)
|
||||||
|
?.navigator
|
||||||
|
?.requestSessionVerification(requireContext(), newest.deviceId ?: "")
|
||||||
|
}
|
||||||
|
dismissedAction = Runnable {}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onGroupChange(groupSummary: GroupSummary?) {
|
private fun onGroupChange(groupSummary: GroupSummary?) {
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.features.home
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.Async
|
||||||
|
import com.airbnb.mvrx.MvRxState
|
||||||
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
|
import com.airbnb.mvrx.Uninitialized
|
||||||
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
|
import im.vector.matrix.android.api.session.Session
|
||||||
|
import im.vector.matrix.android.api.util.MatrixItem
|
||||||
|
import im.vector.matrix.android.api.util.NoOpCancellable
|
||||||
|
import im.vector.matrix.android.api.util.toMatrixItem
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
||||||
|
import im.vector.matrix.rx.rx
|
||||||
|
import im.vector.matrix.rx.singleBuilder
|
||||||
|
import im.vector.riotx.core.di.HasScreenInjector
|
||||||
|
import im.vector.riotx.core.platform.EmptyAction
|
||||||
|
import im.vector.riotx.core.platform.EmptyViewEvents
|
||||||
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
|
|
||||||
|
data class UnknownDevicesState(
|
||||||
|
val unknownSessions: Async<List<Pair<MatrixItem?, DeviceInfo>>> = Uninitialized,
|
||||||
|
val canCrossSign: Boolean = false
|
||||||
|
) : MvRxState
|
||||||
|
|
||||||
|
class UnknownDeviceDetectorSharedViewModel(session: Session, initialState: UnknownDevicesState)
|
||||||
|
: VectorViewModel<UnknownDevicesState, EmptyAction, EmptyViewEvents>(initialState) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
session.rx().liveUserCryptoDevices(session.myUserId)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.switchMap { deviceList ->
|
||||||
|
// Timber.v("## Detector - ============================")
|
||||||
|
// Timber.v("## Detector - Crypto device update ${deviceList.map { "${it.deviceId} : ${it.isVerified}" }}")
|
||||||
|
singleBuilder<DevicesListResponse> {
|
||||||
|
session.cryptoService().getDevicesList(it)
|
||||||
|
NoOpCancellable
|
||||||
|
}.map { resp ->
|
||||||
|
// Timber.v("## Detector - Device Infos ${resp.devices?.map { "${it.deviceId} : lastSeen:${it.lastSeenTs}" }}")
|
||||||
|
resp.devices?.filter { info ->
|
||||||
|
deviceList.firstOrNull { info.deviceId == it.deviceId }?.let {
|
||||||
|
!it.isVerified
|
||||||
|
} ?: false
|
||||||
|
}?.sortedByDescending { it.lastSeenTs }
|
||||||
|
?.map {
|
||||||
|
session.getUser(it.user_id ?: "")?.toMatrixItem() to it
|
||||||
|
} ?: emptyList()
|
||||||
|
}
|
||||||
|
.toObservable()
|
||||||
|
}
|
||||||
|
.execute { async ->
|
||||||
|
copy(unknownSessions = async)
|
||||||
|
}
|
||||||
|
|
||||||
|
session.rx().liveCrossSigningInfo(session.myUserId)
|
||||||
|
.execute {
|
||||||
|
copy(canCrossSign = session.cryptoService().crossSigningService().canCrossSign())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handle(action: EmptyAction) {}
|
||||||
|
|
||||||
|
companion object : MvRxViewModelFactory<UnknownDeviceDetectorSharedViewModel, UnknownDevicesState> {
|
||||||
|
override fun create(viewModelContext: ViewModelContext, state: UnknownDevicesState): UnknownDeviceDetectorSharedViewModel? {
|
||||||
|
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
|
||||||
|
return UnknownDeviceDetectorSharedViewModel(session, state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -250,7 +250,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java)
|
sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java)
|
||||||
attachmentsHelper = AttachmentsHelper.create(this, this).register()
|
attachmentsHelper = AttachmentsHelper(requireContext(), this).register()
|
||||||
keyboardStateUtils = KeyboardStateUtils(requireActivity())
|
keyboardStateUtils = KeyboardStateUtils(requireActivity())
|
||||||
setupToolbar(roomToolbar)
|
setupToolbar(roomToolbar)
|
||||||
setupRecyclerView()
|
setupRecyclerView()
|
||||||
|
@ -290,9 +290,9 @@ class RoomDetailFragment @Inject constructor(
|
||||||
|
|
||||||
roomDetailViewModel.observeViewEvents {
|
roomDetailViewModel.observeViewEvents {
|
||||||
when (it) {
|
when (it) {
|
||||||
is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable)
|
is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable)
|
||||||
is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds)
|
is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds)
|
||||||
is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it)
|
is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it)
|
||||||
is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(it)
|
is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(it)
|
||||||
is RoomDetailViewEvents.ShowMessage -> showSnackWithMessage(it.message, Snackbar.LENGTH_LONG)
|
is RoomDetailViewEvents.ShowMessage -> showSnackWithMessage(it.message, Snackbar.LENGTH_LONG)
|
||||||
is RoomDetailViewEvents.NavigateToEvent -> navigateToEvent(it)
|
is RoomDetailViewEvents.NavigateToEvent -> navigateToEvent(it)
|
||||||
|
@ -665,7 +665,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
private fun sendUri(uri: Uri): Boolean {
|
private fun sendUri(uri: Uri): Boolean {
|
||||||
roomDetailViewModel.preventAttachmentPreview = true
|
roomDetailViewModel.preventAttachmentPreview = true
|
||||||
val shareIntent = Intent(Intent.ACTION_SEND, uri)
|
val shareIntent = Intent(Intent.ACTION_SEND, uri)
|
||||||
val isHandled = attachmentsHelper.handleShareIntent(shareIntent)
|
val isHandled = attachmentsHelper.handleShareIntent(requireContext(), shareIntent)
|
||||||
if (!isHandled) {
|
if (!isHandled) {
|
||||||
roomDetailViewModel.preventAttachmentPreview = false
|
roomDetailViewModel.preventAttachmentPreview = false
|
||||||
Toast.makeText(requireContext(), R.string.error_handling_incoming_share, Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), R.string.error_handling_incoming_share, Toast.LENGTH_SHORT).show()
|
||||||
|
@ -1350,11 +1350,11 @@ class RoomDetailFragment @Inject constructor(
|
||||||
|
|
||||||
private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) {
|
private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) {
|
||||||
when (type) {
|
when (type) {
|
||||||
AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera()
|
AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera(this)
|
||||||
AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile()
|
AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile(this)
|
||||||
AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery()
|
AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(this)
|
||||||
AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio()
|
AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio(this)
|
||||||
AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact()
|
AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact(this)
|
||||||
AttachmentTypeSelectorView.Type.STICKER -> vectorBaseActivity.notImplemented("Adding stickers")
|
AttachmentTypeSelectorView.Type.STICKER -> vectorBaseActivity.notImplemented("Adding stickers")
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
|
@ -610,7 +610,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||||
when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) {
|
when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) {
|
||||||
null -> room.sendMedias(attachments, action.compressBeforeSending, emptySet())
|
null -> room.sendMedias(attachments, action.compressBeforeSending, emptySet())
|
||||||
else -> _viewEvents.post(RoomDetailViewEvents.FileTooBigError(
|
else -> _viewEvents.post(RoomDetailViewEvents.FileTooBigError(
|
||||||
tooBigFile.name ?: tooBigFile.path,
|
tooBigFile.name ?: tooBigFile.queryUri.toString(),
|
||||||
tooBigFile.size,
|
tooBigFile.size,
|
||||||
maxUploadFileSize
|
maxUploadFileSize
|
||||||
))
|
))
|
||||||
|
|
|
@ -58,7 +58,7 @@ class ViewReactionsBottomSheet : VectorBaseBottomSheetDialogFragment(), ViewReac
|
||||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
super.onActivityCreated(savedInstanceState)
|
super.onActivityCreated(savedInstanceState)
|
||||||
sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java)
|
sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java)
|
||||||
recyclerView.configureWith(epoxyController, hasFixedSize = false)
|
recyclerView.configureWith(epoxyController, hasFixedSize = false, showDivider = true)
|
||||||
bottomSheetTitle.text = context?.getString(R.string.reactions)
|
bottomSheetTitle.text = context?.getString(R.string.reactions)
|
||||||
epoxyController.listener = this
|
epoxyController.listener = this
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,14 +20,13 @@ import android.app.Activity
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import im.vector.riotx.features.popup.PopupAlertManager
|
import im.vector.riotx.features.popup.PopupAlertManager
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class VectorActivityLifecycleCallbacks @Inject constructor() : Application.ActivityLifecycleCallbacks {
|
class VectorActivityLifecycleCallbacks constructor(private val popupAlertManager: PopupAlertManager) : Application.ActivityLifecycleCallbacks {
|
||||||
override fun onActivityPaused(activity: Activity) {
|
override fun onActivityPaused(activity: Activity) {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResumed(activity: Activity) {
|
override fun onActivityResumed(activity: Activity) {
|
||||||
PopupAlertManager.onNewActivityDisplayed(activity)
|
popupAlertManager.onNewActivityDisplayed(activity)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityStarted(activity: Activity) {
|
override fun onActivityStarted(activity: Activity) {
|
||||||
|
|
|
@ -83,12 +83,12 @@ class DefaultNavigator @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun requestSessionVerification(context: Context) {
|
override fun requestSessionVerification(context: Context, otherSessionId: String) {
|
||||||
val session = sessionHolder.getSafeActiveSession() ?: return
|
val session = sessionHolder.getSafeActiveSession() ?: return
|
||||||
val pr = session.cryptoService().verificationService().requestKeyVerification(
|
val pr = session.cryptoService().verificationService().requestKeyVerification(
|
||||||
supportedVerificationMethodsProvider.provide(),
|
supportedVerificationMethodsProvider.provide(),
|
||||||
session.myUserId,
|
session.myUserId,
|
||||||
session.cryptoService().getUserDevices(session.myUserId).map { it.deviceId }
|
listOf(otherSessionId)
|
||||||
)
|
)
|
||||||
if (context is VectorBaseActivity) {
|
if (context is VectorBaseActivity) {
|
||||||
VerificationBottomSheet.withArgs(
|
VerificationBottomSheet.withArgs(
|
||||||
|
|
|
@ -30,7 +30,7 @@ interface Navigator {
|
||||||
|
|
||||||
fun performDeviceVerification(context: Context, otherUserId: String, sasTransactionId: String)
|
fun performDeviceVerification(context: Context, otherUserId: String, sasTransactionId: String)
|
||||||
|
|
||||||
fun requestSessionVerification(context: Context)
|
fun requestSessionVerification(context: Context, otherSessionId: String)
|
||||||
|
|
||||||
fun waitSessionVerification(context: Context)
|
fun waitSessionVerification(context: Context)
|
||||||
|
|
||||||
|
|
|
@ -20,20 +20,23 @@ import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.annotation.ColorInt
|
import android.widget.ImageView
|
||||||
import androidx.annotation.ColorRes
|
|
||||||
import androidx.annotation.DrawableRes
|
|
||||||
import com.tapadoo.alerter.Alerter
|
import com.tapadoo.alerter.Alerter
|
||||||
import com.tapadoo.alerter.OnHideAlertListener
|
import com.tapadoo.alerter.OnHideAlertListener
|
||||||
|
import dagger.Lazy
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Responsible of displaying important popup alerts on top of the screen.
|
* Responsible of displaying important popup alerts on top of the screen.
|
||||||
* Alerts are stacked and will be displayed sequentially
|
* Alerts are stacked and will be displayed sequentially
|
||||||
*/
|
*/
|
||||||
object PopupAlertManager {
|
@Singleton
|
||||||
|
class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<AvatarRenderer>) {
|
||||||
|
|
||||||
private var weakCurrentActivity: WeakReference<Activity>? = null
|
private var weakCurrentActivity: WeakReference<Activity>? = null
|
||||||
private var currentAlerter: VectorAlert? = null
|
private var currentAlerter: VectorAlert? = null
|
||||||
|
@ -160,9 +163,19 @@ object PopupAlertManager {
|
||||||
clearLightStatusBar()
|
clearLightStatusBar()
|
||||||
|
|
||||||
alert.weakCurrentActivity = WeakReference(activity)
|
alert.weakCurrentActivity = WeakReference(activity)
|
||||||
Alerter.create(activity)
|
val alerter = if (alert is VerificationVectorAlert) Alerter.create(activity, R.layout.alerter_verification_layout)
|
||||||
.setTitle(alert.title)
|
else Alerter.create(activity)
|
||||||
|
|
||||||
|
alerter.setTitle(alert.title)
|
||||||
.setText(alert.description)
|
.setText(alert.description)
|
||||||
|
.also { al ->
|
||||||
|
if (alert is VerificationVectorAlert) {
|
||||||
|
val tvCustomView = al.getLayoutContainer()
|
||||||
|
tvCustomView?.findViewById<ImageView>(R.id.ivUserAvatar)?.let { imageView ->
|
||||||
|
alert.matrixItem?.let { avatarRenderer.get().render(it, imageView) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.apply {
|
.apply {
|
||||||
if (!animate) {
|
if (!animate) {
|
||||||
setEnterAnimation(R.anim.anim_alerter_no_anim)
|
setEnterAnimation(R.anim.anim_alerter_no_anim)
|
||||||
|
@ -226,37 +239,4 @@ object PopupAlertManager {
|
||||||
displayNextIfPossible()
|
displayNextIfPossible()
|
||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Dataclass to describe an important alert with actions.
|
|
||||||
*/
|
|
||||||
data class VectorAlert(val uid: String,
|
|
||||||
val title: String,
|
|
||||||
val description: String,
|
|
||||||
@DrawableRes val iconId: Int?,
|
|
||||||
val shouldBeDisplayedIn: ((Activity) -> Boolean)? = null) {
|
|
||||||
|
|
||||||
data class Button(val title: String, val action: Runnable, val autoClose: Boolean)
|
|
||||||
|
|
||||||
// will be set by manager, and accessible by actions at runtime
|
|
||||||
var weakCurrentActivity: WeakReference<Activity>? = null
|
|
||||||
|
|
||||||
val actions = ArrayList<Button>()
|
|
||||||
|
|
||||||
var contentAction: Runnable? = null
|
|
||||||
var dismissedAction: Runnable? = null
|
|
||||||
|
|
||||||
/** If this timestamp is after current time, this alert will be skipped */
|
|
||||||
var expirationTimestamp: Long? = null
|
|
||||||
|
|
||||||
fun addButton(title: String, action: Runnable, autoClose: Boolean = true) {
|
|
||||||
actions.add(Button(title, action, autoClose))
|
|
||||||
}
|
|
||||||
|
|
||||||
@ColorRes
|
|
||||||
var colorRes: Int? = null
|
|
||||||
|
|
||||||
@ColorInt
|
|
||||||
var colorInt: Int? = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.features.popup
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.annotation.ColorRes
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import im.vector.matrix.android.api.util.MatrixItem
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
|
interface VectorAlert {
|
||||||
|
val uid: String
|
||||||
|
val title: String
|
||||||
|
val description: String
|
||||||
|
val iconId: Int?
|
||||||
|
val shouldBeDisplayedIn: ((Activity) -> Boolean)?
|
||||||
|
|
||||||
|
data class Button(val title: String, val action: Runnable, val autoClose: Boolean)
|
||||||
|
|
||||||
|
// will be set by manager, and accessible by actions at runtime
|
||||||
|
var weakCurrentActivity: WeakReference<Activity>?
|
||||||
|
|
||||||
|
val actions: MutableList<Button>
|
||||||
|
|
||||||
|
var contentAction: Runnable?
|
||||||
|
var dismissedAction: Runnable?
|
||||||
|
|
||||||
|
/** If this timestamp is after current time, this alert will be skipped */
|
||||||
|
var expirationTimestamp: Long?
|
||||||
|
|
||||||
|
fun addButton(title: String, action: Runnable, autoClose: Boolean = true) {
|
||||||
|
actions.add(Button(title, action, autoClose))
|
||||||
|
}
|
||||||
|
|
||||||
|
var colorRes: Int?
|
||||||
|
|
||||||
|
var colorInt: Int?
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dataclass to describe an important alert with actions.
|
||||||
|
*/
|
||||||
|
open class DefaultVectorAlert(override val uid: String,
|
||||||
|
override val title: String,
|
||||||
|
override val description: String,
|
||||||
|
@DrawableRes override val iconId: Int?,
|
||||||
|
override val shouldBeDisplayedIn: ((Activity) -> Boolean)? = null) : VectorAlert {
|
||||||
|
|
||||||
|
// will be set by manager, and accessible by actions at runtime
|
||||||
|
override var weakCurrentActivity: WeakReference<Activity>? = null
|
||||||
|
|
||||||
|
override val actions = ArrayList<VectorAlert.Button>()
|
||||||
|
|
||||||
|
override var contentAction: Runnable? = null
|
||||||
|
override var dismissedAction: Runnable? = null
|
||||||
|
|
||||||
|
/** If this timestamp is after current time, this alert will be skipped */
|
||||||
|
override var expirationTimestamp: Long? = null
|
||||||
|
|
||||||
|
override fun addButton(title: String, action: Runnable, autoClose: Boolean) {
|
||||||
|
actions.add(VectorAlert.Button(title, action, autoClose))
|
||||||
|
}
|
||||||
|
|
||||||
|
@ColorRes
|
||||||
|
override var colorRes: Int? = null
|
||||||
|
|
||||||
|
@ColorInt
|
||||||
|
override var colorInt: Int? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
class VerificationVectorAlert(uid: String,
|
||||||
|
title: String,
|
||||||
|
override val description: String,
|
||||||
|
@DrawableRes override val iconId: Int?,
|
||||||
|
override val shouldBeDisplayedIn: ((Activity) -> Boolean)? = null
|
||||||
|
) : DefaultVectorAlert(
|
||||||
|
uid, title, description, iconId, shouldBeDisplayedIn
|
||||||
|
) {
|
||||||
|
var matrixItem: MatrixItem? = null
|
||||||
|
}
|
|
@ -57,6 +57,8 @@ class VectorSettingsActivity : VectorBaseActivity(),
|
||||||
when (intent.getIntExtra(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOT)) {
|
when (intent.getIntExtra(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOT)) {
|
||||||
EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS ->
|
EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS ->
|
||||||
replaceFragment(R.id.vector_settings_page, VectorSettingsAdvancedSettingsFragment::class.java, null, FRAGMENT_TAG)
|
replaceFragment(R.id.vector_settings_page, VectorSettingsAdvancedSettingsFragment::class.java, null, FRAGMENT_TAG)
|
||||||
|
EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY ->
|
||||||
|
replaceFragment(R.id.vector_settings_page, VectorSettingsSecurityPrivacyFragment::class.java, null, FRAGMENT_TAG)
|
||||||
else ->
|
else ->
|
||||||
replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG)
|
replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG)
|
||||||
}
|
}
|
||||||
|
@ -116,6 +118,7 @@ class VectorSettingsActivity : VectorBaseActivity(),
|
||||||
|
|
||||||
const val EXTRA_DIRECT_ACCESS_ROOT = 0
|
const val EXTRA_DIRECT_ACCESS_ROOT = 0
|
||||||
const val EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS = 1
|
const val EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS = 1
|
||||||
|
const val EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY = 2
|
||||||
|
|
||||||
private const val FRAGMENT_TAG = "VectorSettingsPreferencesFragment"
|
private const val FRAGMENT_TAG = "VectorSettingsPreferencesFragment"
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,6 +78,17 @@ class CrossSigningEpoxyController @Inject constructor(
|
||||||
interactionListener?.onResetCrossSigningKeys()
|
interactionListener?.onResetCrossSigningKeys()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bottomSheetVerificationActionItem {
|
||||||
|
id("verify")
|
||||||
|
title(stringProvider.getString(R.string.complete_security))
|
||||||
|
titleColor(colorProvider.getColor(R.color.riotx_positive_accent))
|
||||||
|
iconRes(R.drawable.ic_arrow_right)
|
||||||
|
iconColor(colorProvider.getColor(R.color.riotx_positive_accent))
|
||||||
|
listener {
|
||||||
|
interactionListener?.verifySession()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (data.xSigningIsEnableInAccount) {
|
} else if (data.xSigningIsEnableInAccount) {
|
||||||
genericItem {
|
genericItem {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue