Merge develop into the branch.

This commit is contained in:
onurays 2020-03-26 14:39:50 +03:00
commit 12429d8091
112 changed files with 2951 additions and 916 deletions

View file

@ -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 🗣:
- -

View file

@ -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

View file

@ -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

View file

@ -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()
}
}
}
} }

View file

@ -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 {

View file

@ -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.
*/ */

View file

@ -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)
} }
/** /**

View file

@ -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,

View file

@ -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)
}
}

View file

@ -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
}
} }

View file

@ -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")
} }

View file

@ -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)
}
} }

View file

@ -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)
}
}
}
}

View file

@ -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)
}
}
}

View file

@ -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("*")
) )

View file

@ -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())
}
} }
} }
} }

View file

@ -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> {

View file

@ -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.

View file

@ -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?) {

View file

@ -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)

View file

@ -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,

View file

@ -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,

View file

@ -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)

View file

@ -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 ?: "*")))
} }

View file

@ -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,

View file

@ -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,

View file

@ -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(

View file

@ -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)
} }

View file

@ -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
} }
} }

View file

@ -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)
} }
} }

View file

@ -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)
} }

View file

@ -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
View file

@ -0,0 +1 @@
/build

56
multipicker/build.gradle Normal file
View 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'
}

View file

21
multipicker/proguard-rules.pro vendored Normal file
View 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

View 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>

View file

@ -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/*"
}
}
}

View file

@ -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 */
)
}
}

View file

@ -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
}
}
}

View file

@ -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 = "*/*"
}
}
}

View file

@ -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/*"
}
}
}

View file

@ -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")
}
}
}
}

View 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
}
}

View file

@ -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/*"
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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

View file

@ -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

View file

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

View file

@ -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
}
}

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path
name="external_files"
path="." />
</paths>

View file

@ -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'

View file

@ -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"

View file

@ -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 -->

View file

@ -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",

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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
}
}
} }

View file

@ -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
) )
} }

View file

@ -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()
}
}

View file

@ -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)
}
}
}

View file

@ -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 { _ ->

View file

@ -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 {

View file

@ -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()
} }

View file

@ -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()

View file

@ -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
} }

View file

@ -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"

View file

@ -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))
} }
} }

View file

@ -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

View file

@ -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()
} }

View file

@ -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? {

View file

@ -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()
}
}

View file

@ -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()
}
}

View file

@ -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()
}
}

View file

@ -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()
}
}

View file

@ -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()
} }
} }

View file

@ -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)
} }

View file

@ -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(),

View file

@ -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")
} }

View file

@ -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() {

View file

@ -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()
} }

View file

@ -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()
}
} }

View file

@ -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)

View file

@ -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),

View file

@ -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?) {

View file

@ -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)
}
}
}

View file

@ -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
} }

View file

@ -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
)) ))

View file

@ -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
} }

View file

@ -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) {

View file

@ -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(

View file

@ -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)

View file

@ -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
}
} }

View file

@ -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
}

View file

@ -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"
} }

View file

@ -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