diff --git a/CHANGES.md b/CHANGES.md index 6f8b8ead15..ea0e51561b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,12 +3,16 @@ Changes in RiotX 0.19.0 (2020-XX-XX) Features ✨: - Cross-Signing | Support SSSS secret sharing (#944) + - Cross-Signing | Verify new session from existing session (#1134) Improvements 🙌: - Verification DM / Handle concurrent .start after .ready (#794) + - Reimplementation of multiple attachment picker Bugfix 🐛: - 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 🗣: - diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 60f22a4bdf..c10aaf3545 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -119,7 +119,7 @@ dependencies { implementation "ru.noties.markwon:core:$markwon_version" // Image - implementation 'androidx.exifinterface:exifinterface:1.1.0' + implementation 'androidx.exifinterface:exifinterface:1.3.0-alpha01' implementation 'id.zelory:compressor:3.0.0' // Database diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt index 08c81f56c0..3cf03fff53 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt @@ -266,8 +266,8 @@ class CommonTestHelper(context: Context) { * @param latch * @throws InterruptedException */ - fun await(latch: CountDownLatch) { - assertTrue(latch.await(TestConstants.timeOutMillis, TimeUnit.MILLISECONDS)) + fun await(latch: CountDownLatch, timout: Long? = TestConstants.timeOutMillis) { + assertTrue(latch.await(timout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS)) } 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) block(latch) - await(latch) + await(latch, timout) } // Transform a method with a MatrixCallback to a synchronous method diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt index c8d2df38ce..aec5e6c423 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt @@ -19,6 +19,12 @@ package im.vector.matrix.android.internal.crypto.gossiping import android.util.Log import androidx.test.ext.junit.runners.AndroidJUnit4 import im.vector.matrix.android.InstrumentedTest +import im.vector.matrix.android.api.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.room.model.RoomDirectoryVisibility 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.OutgoingGossipingRequestState 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.rest.UserPasswordAuth +import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.assertTrue import junit.framework.TestCase.fail @@ -174,4 +184,85 @@ class KeyShareTests : InstrumentedTest { mTestHelper.signOutAndClose(aliceSession) 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() + } + } + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt index e32bb9f21f..b80a17b017 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.api.session.content +import android.net.Uri import android.os.Parcelable import androidx.exifinterface.media.ExifInterface import kotlinx.android.parcel.Parcelize @@ -29,8 +30,7 @@ data class ContentAttachmentData( val width: Long? = 0, val exifOrientation: Int = ExifInterface.ORIENTATION_UNDEFINED, val name: String? = null, - val queryUri: String, - val path: String, + val queryUri: Uri, private val mimeType: String?, val type: Type ) : Parcelable { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/VerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/VerificationService.kt index 75033082d6..4482101434 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/VerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/VerificationService.kt @@ -60,6 +60,8 @@ interface VerificationService { roomId: String, localId: String? = LocalEcho.createLocalEchoId()): PendingVerificationRequest + fun cancelVerificationRequest(request: PendingVerificationRequest) + /** * Request a key verification from another user using toDevice events. */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt index 9e702ee9ac..d0a08b17ab 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt @@ -140,7 +140,7 @@ internal class DefaultCryptoService @Inject constructor( private val crossSigningService: DefaultCrossSigningService, // - private val incomingRoomKeyRequestManager: IncomingRoomKeyRequestManager, + private val incomingGossipingRequestManager: IncomingGossipingRequestManager, // private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, // Actions @@ -239,7 +239,7 @@ internal class DefaultCryptoService @Inject constructor( override fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) { getDevicesTask .configureWith { - this.executionThread = TaskThread.CRYPTO +// this.executionThread = TaskThread.CRYPTO this.callback = callback } .executeBy(taskExecutor) @@ -317,7 +317,7 @@ internal class DefaultCryptoService @Inject constructor( deviceListManager.invalidateAllDeviceLists() deviceListManager.refreshOutdatedDeviceLists() } else { - incomingRoomKeyRequestManager.processReceivedGossipingRequests() + incomingGossipingRequestManager.processReceivedGossipingRequests() } }.fold( { @@ -376,7 +376,7 @@ internal class DefaultCryptoService @Inject constructor( // Make sure we process to-device messages before generating new one-time-keys #2782 deviceListManager.refreshOutdatedDeviceLists() oneTimeKeysUploader.maybeUploadOneTimeKeys() - incomingRoomKeyRequestManager.processReceivedGossipingRequests() + incomingGossipingRequestManager.processReceivedGossipingRequests() } } } @@ -709,7 +709,7 @@ internal class DefaultCryptoService @Inject constructor( // save audit trail cryptoStore.saveGossipingEvent(event) // 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 -> { cryptoStore.saveGossipingEvent(event) @@ -729,30 +729,30 @@ internal class DefaultCryptoService @Inject constructor( */ private fun onRoomKeyEvent(event: Event) { 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()) { - Timber.e("## onRoomKeyEvent() : missing fields") + Timber.e("## GOSSIP onRoomKeyEvent() : missing fields") return } val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(roomKeyContent.roomId, roomKeyContent.algorithm) 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 } alg.onRoomKeyEvent(event, keysBackupService) } 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()) { // secret send messages must be encrypted - Timber.e("## onSecretSend() :Received unencrypted secret send event") + Timber.e("## GOSSIP onSecretSend() :Received unencrypted secret send event") return } // Was that sent by us? 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 } @@ -762,7 +762,7 @@ internal class DefaultCryptoService @Inject constructor( .getOutgoingSecretKeyRequests().firstOrNull { it.requestId == secretContent.requestId } 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 } @@ -1111,7 +1111,7 @@ internal class DefaultCryptoService @Inject constructor( * @param listener listener */ override fun addRoomKeysRequestListener(listener: GossipingRequestListener) { - incomingRoomKeyRequestManager.addRoomKeysRequestListener(listener) + incomingGossipingRequestManager.addRoomKeysRequestListener(listener) } /** @@ -1120,7 +1120,7 @@ internal class DefaultCryptoService @Inject constructor( * @param listener listener */ override fun removeRoomKeysRequestListener(listener: GossipingRequestListener) { - incomingRoomKeyRequestManager.removeRoomKeysRequestListener(listener) + incomingGossipingRequestManager.removeRoomKeysRequestListener(listener) } /** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/GossipingRequestState.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/GossipingRequestState.kt index b218a2e387..f2e45ef109 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/GossipingRequestState.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/GossipingRequestState.kt @@ -25,7 +25,9 @@ enum class GossipingRequestState { NONE, PENDING, REJECTED, + ACCEPTING, ACCEPTED, + FAILED_TO_ACCEPTED, // USER_REJECTED, UNABLE_TO_PROCESS, CANCELLED_BY_REQUESTER, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/GossipingWorkManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/GossipingWorkManager.kt new file mode 100644 index 0000000000..b7c782c5b4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/GossipingWorkManager.kt @@ -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) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingGossipingRequestManager.kt similarity index 72% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingGossipingRequestManager.kt index 0bb89154f1..8bec87b341 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingGossipingRequestManager.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.crypto 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.session.crypto.crosssigning.SELF_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.GossipingToDeviceObject 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.worker.WorkerParamsFactory import timber.log.Timber import javax.inject.Inject @SessionScope -internal class IncomingRoomKeyRequestManager @Inject constructor( +internal class IncomingGossipingRequestManager @Inject constructor( + @SessionId private val sessionId: String, private val credentials: Credentials, private val cryptoStore: IMXCryptoStore, private val cryptoConfig: MXCryptoConfig, - private val secretSecretCryptoProvider: ShareSecretCryptoProvider, + private val gossipingWorkManager: GossipingWorkManager, private val roomDecryptorProvider: RoomDecryptorProvider) { // list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations @@ -51,6 +55,32 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( 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 * It must be called on CryptoThread @@ -58,7 +88,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( * @param event the announcement 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 ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it } when (roomKeyShare?.action) { @@ -67,7 +97,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( IncomingSecretShareRequest.fromEvent(event)?.let { if (event.senderId == credentials.userId && it.deviceId == credentials.deviceId) { // 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 { // save in DB cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs) @@ -78,7 +108,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( IncomingRoomKeyRequest.fromEvent(event)?.let { if (event.senderId == credentials.userId && it.deviceId == credentials.deviceId) { // 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 { cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs) receivedGossipingRequests.add(it) @@ -92,7 +122,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( } } 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 */ fun processReceivedGossipingRequests() { - Timber.v("## processReceivedGossipingRequests()") - val roomKeyRequestsToProcess = receivedGossipingRequests.toList() receivedGossipingRequests.clear() for (request in roomKeyRequestsToProcess) { @@ -125,7 +153,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( } 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 // about, but we don't currently have a record of that, so we just pass // everything through. @@ -154,10 +182,10 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( val roomId = body!!.roomId 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) { // 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) return } @@ -166,18 +194,18 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( // the keys for the requested events, and can drop the requests. val decryptor = roomDecryptorProvider.getRoomDecryptor(roomId, alg) 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) return } 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) return } 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) return } @@ -192,13 +220,13 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( val device = cryptoStore.getUserDevice(userId, deviceId!!) if (device != null) { 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() return } if (device.isBlocked) { - Timber.v("## processReceivedGossipingRequests() : device is blocked -> ignored") + Timber.v("## GOSSIP processReceivedGossipingRequests() : device is blocked -> ignored") cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) return } @@ -219,30 +247,30 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( private fun processIncomingSecretShareRequest(request: IncomingSecretShareRequest) { val secretName = request.secretName ?: return Unit.also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) - Timber.v("## processIncomingSecretShareRequest() : Missing secret name") + Timber.v("## GOSSIP processIncomingSecretShareRequest() : Missing secret name") } val userId = request.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) return } val deviceId = request.deviceId ?: return Unit.also { - Timber.e("## processIncomingSecretShareRequest() : Malformed request, no ") + Timber.e("## GOSSIP processIncomingSecretShareRequest() : Malformed request, no ") cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) } val device = cryptoStore.getUserDevice(userId, deviceId) ?: 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) } 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) return } @@ -255,11 +283,20 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user else -> null }?.let { secretValue -> - // TODO check if locally trusted and not outdated - Timber.i("## processIncomingSecretShareRequest() : Sharing secret $secretName with $device locally trusted") - if (isDeviceLocallyVerified == true) { - secretSecretCryptoProvider.shareSecretWithDevice(request, secretValue) - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED) + Timber.i("## GOSSIP processIncomingSecretShareRequest() : Sharing secret $secretName with $device locally trusted") + if (isDeviceLocallyVerified == true && hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId)) { + val params = SendGossipWorker.Params( + sessionId = sessionId, + 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 } @@ -269,7 +306,16 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( } 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) } @@ -304,7 +350,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( return } } catch (e: Exception) { - Timber.e(e, "## onRoomKeyRequest() failed") + Timber.e(e, "## GOSSIP onRoomKeyRequest() failed") } } } @@ -323,7 +369,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( try { listener.onRoomKeyRequestCancellation(request) } 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) } } + + companion object { + private const val FIVE_MINUTES_IN_MILLIS = 5 * 60 * 1000 + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt index 54f34c6c9d..0f48b5ecfb 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt @@ -59,9 +59,6 @@ internal class MXOlmDevice @Inject constructor( var deviceEd25519Key: String? = null private set - // The OLM lib account instance. - private var olmAccount: OlmAccount? = null - // The OLM lib utility instance. private var olmUtility: OlmUtility? = null @@ -86,19 +83,10 @@ internal class MXOlmDevice @Inject constructor( init { // Retrieve the account from the store - olmAccount = store.getAccount() - - if (null == olmAccount) { - Timber.v("MXOlmDevice : create a new olm account") - // 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 { + store.getOrCreateOlmAccount() + } catch (e: Exception) { + Timber.e(e, "MXOlmDevice : cannot initialize olmAccount") } try { @@ -109,13 +97,13 @@ internal class MXOlmDevice @Inject constructor( } try { - deviceCurve25519Key = olmAccount!!.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY] + deviceCurve25519Key = store.getOlmAccount().identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY] } catch (e: Exception) { Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_IDENTITY_KEY} with error") } try { - deviceEd25519Key = olmAccount!!.identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY] + deviceEd25519Key = store.getOlmAccount().identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY] } catch (e: Exception) { 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>>? { try { - return olmAccount!!.oneTimeKeys() + return store.getOlmAccount().oneTimeKeys() } catch (e: Exception) { 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. */ fun getMaxNumberOfOneTimeKeys(): Long { - return olmAccount?.maxOneTimeKeys() ?: -1 + return store.getOlmAccount().maxOneTimeKeys() } /** * Release the instance */ fun release() { - olmAccount?.releaseAccount() olmUtility?.releaseUtility() } @@ -157,7 +144,7 @@ internal class MXOlmDevice @Inject constructor( */ fun signMessage(message: String): String? { try { - return olmAccount!!.signMessage(message) + return store.getOlmAccount().signMessage(message) } catch (e: Exception) { Timber.e(e, "## signMessage() : failed") } @@ -170,8 +157,8 @@ internal class MXOlmDevice @Inject constructor( */ fun markKeysAsPublished() { try { - olmAccount!!.markOneTimeKeysAsPublished() - store.storeAccount(olmAccount!!) + store.getOlmAccount().markOneTimeKeysAsPublished() + store.saveOlmAccount() } catch (e: Exception) { Timber.e(e, "## markKeysAsPublished() : failed") } @@ -184,8 +171,8 @@ internal class MXOlmDevice @Inject constructor( */ fun generateOneTimeKeys(numKeys: Int) { try { - olmAccount!!.generateOneTimeKeys(numKeys) - store.storeAccount(olmAccount!!) + store.getOlmAccount().generateOneTimeKeys(numKeys) + store.saveOlmAccount() } catch (e: Exception) { Timber.e(e, "## generateOneTimeKeys() : failed") } @@ -205,7 +192,7 @@ internal class MXOlmDevice @Inject constructor( try { olmSession = OlmSession() - olmSession.initOutboundSession(olmAccount!!, theirIdentityKey, theirOneTimeKey) + olmSession.initOutboundSession(store.getOlmAccount(), theirIdentityKey, theirOneTimeKey) val olmSessionWrapper = OlmSessionWrapper(olmSession, 0) @@ -245,7 +232,7 @@ internal class MXOlmDevice @Inject constructor( try { try { olmSession = OlmSession() - olmSession.initInboundSessionFrom(olmAccount!!, theirDeviceIdentityKey, ciphertext) + olmSession.initInboundSessionFrom(store.getOlmAccount(), theirDeviceIdentityKey, ciphertext) } catch (e: Exception) { Timber.e(e, "## createInboundSession() : the session creation failed") return null @@ -254,8 +241,8 @@ internal class MXOlmDevice @Inject constructor( Timber.v("## createInboundSession() : sessionId: ${olmSession.sessionIdentifier()}") try { - olmAccount!!.removeOneTimeKeys(olmSession) - store.storeAccount(olmAccount!!) + store.getOlmAccount().removeOneTimeKeys(olmSession) + store.saveOlmAccount() } catch (e: Exception) { Timber.e(e, "## createInboundSession() : removeOneTimeKeys failed") } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingGossipingRequestManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingGossipingRequestManager.kt index 7c83ccc9bf..c06f10b106 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingGossipingRequestManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingGossipingRequestManager.kt @@ -17,27 +17,17 @@ 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.util.Cancelable 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.di.SessionId -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.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.worker.WorkerParamsFactory -import im.vector.matrix.android.internal.worker.startChain import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import timber.log.Timber -import java.util.concurrent.TimeUnit import javax.inject.Inject @SessionScope @@ -46,7 +36,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor( private val cryptoStore: IMXCryptoStore, private val coroutineDispatchers: MatrixCoroutineDispatchers, 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. @@ -65,7 +55,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor( cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients)?.let { // Don't resend if it's already done, you need to cancel first (reRequest) 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 } @@ -82,7 +72,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor( cryptoStore.getOrAddOutgoingSecretShareRequest(secretName, recipients)?.let { // TODO check if there is already one that is being 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 } @@ -123,7 +113,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor( val req = cryptoStore.getOutgoingRoomKeyRequest(requestBody) ?: // no request was made for this key return Unit.also { - Timber.v("## cancelRoomKeyRequest() Unknown request") + Timber.v("## GOSSIP cancelRoomKeyRequest() Unknown request") } sendOutgoingRoomKeyRequestCancellation(req, andResend) @@ -135,7 +125,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor( * @param request the request */ private fun sendOutgoingGossipingRequest(request: OutgoingGossipingRequest) { - Timber.v("## sendOutgoingRoomKeyRequest() : Requesting keys $request") + Timber.v("## GOSSIP sendOutgoingRoomKeyRequest() : Requesting keys $request") val params = SendGossipRequestWorker.Params( sessionId = sessionId, @@ -143,8 +133,8 @@ internal class OutgoingGossipingRequestManager @Inject constructor( secretShareRequest = request as? OutgoingSecretRequest ) cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.SENDING) - val workRequest = createWork<SendGossipRequestWorker>(WorkerParamsFactory.toData(params), true) - postWork(workRequest) + val workRequest = gossipingWorkManager.createWork<SendGossipRequestWorker>(WorkerParamsFactory.toData(params), true) + gossipingWorkManager.postWork(workRequest) } /** @@ -157,33 +147,16 @@ internal class OutgoingGossipingRequestManager @Inject constructor( val params = CancelGossipRequestWorker.Params.fromRequest(sessionId, request) cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.CANCELLING) - val workRequest = createWork<CancelGossipRequestWorker>(WorkerParamsFactory.toData(params), true) - postWork(workRequest) + val workRequest = gossipingWorkManager.createWork<CancelGossipRequestWorker>(WorkerParamsFactory.toData(params), true) + gossipingWorkManager.postWork(workRequest) if (resend) { val reSendParams = SendGossipRequestWorker.Params( sessionId = sessionId, keyShareRequest = request.copy(requestId = LocalEcho.createLocalEchoId()) ) - val reSendWorkRequest = createWork<SendGossipRequestWorker>(WorkerParamsFactory.toData(reSendParams), true) - postWork(reSendWorkRequest) + val reSendWorkRequest = gossipingWorkManager.createWork<SendGossipRequestWorker>(WorkerParamsFactory.toData(reSendParams), true) + 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) - } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/SendGossipWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/SendGossipWorker.kt new file mode 100644 index 0000000000..8a273da338 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/SendGossipWorker.kt @@ -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) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/ShareSecretCryptoProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/ShareSecretCryptoProvider.kt deleted file mode 100644 index f4457f3a7f..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/ShareSecretCryptoProvider.kt +++ /dev/null @@ -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) - } - } -} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt index d5b3b3b034..1d7a2765fa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt @@ -149,7 +149,7 @@ internal class MXMegolmDecryption(private val userId: String, val encryptedEventContent = event.content.toModel<EncryptedEventContent>() val senderDevice = encryptedEventContent?.deviceId ?: return - val recipients = if (event.senderId != userId) { + val recipients = if (event.senderId == userId) { mapOf( userId to listOf("*") ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt index 74a4c6bee8..389fa1ea50 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt @@ -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.MXOlmDevice 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.KeyUsage 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 coroutineDispatchers: MatrixCoroutineDispatchers, private val cryptoCoroutineScope: CoroutineScope, - private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, private val eventBus: EventBus) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener { private var olmUtility: OlmUtility? = null @@ -599,6 +597,7 @@ internal class DefaultCrossSigningService @Inject constructor( override fun canCrossSign(): Boolean { return checkSelfTrust().isVerified() && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null + && cryptoStore.getCrossSigningPrivateKeys()?.user != null } 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()}") 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? val devices = cryptoStore.getUserDeviceList(otherUserId) devices?.forEach { device -> @@ -791,24 +795,22 @@ internal class DefaultCrossSigningService @Inject constructor( } private fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - val currentTrust = cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted() - cryptoStore.setUserKeysAsTrusted(otherUserId, trusted) - // If it's me, recheck trust of all users and devices? - val users = ArrayList<String>() - if (otherUserId == userId && currentTrust != trusted) { + val currentTrust = cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted() + cryptoStore.setUserKeysAsTrusted(otherUserId, trusted) + // If it's me, recheck trust of all users and devices? + val users = ArrayList<String>() + if (otherUserId == userId && currentTrust != trusted) { // reRequestAllPendingRoomKeyRequest() - cryptoStore.updateUsersTrust { - users.add(it) - checkUserTrust(it).isVerified() - } + cryptoStore.updateUsersTrust { + users.add(it) + checkUserTrust(it).isVerified() + } - users.forEach { - cryptoStore.getUserDeviceList(it)?.forEach { device -> - val updatedTrust = checkDeviceTrust(it, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false) - Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust") - cryptoStore.setDeviceTrust(it, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified()) - } + users.forEach { + cryptoStore.getUserDeviceList(it)?.forEach { device -> + val updatedTrust = checkDeviceTrust(it, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false) + Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust") + cryptoStore.setDeviceTrust(it, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified()) } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt index 1bd55dd35d..42f72a0a33 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt @@ -272,7 +272,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor( val ivParameterSpec = IvParameterSpec(iv) cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec) // secret are not that big, just do Final - val cipherBytes = cipher.doFinal(clearDataBase64.fromBase64()) + val cipherBytes = cipher.doFinal(clearDataBase64.toByteArray()) require(cipherBytes.isNotEmpty()) val macKeySpec = SecretKeySpec(macKey, "HmacSHA256") @@ -303,6 +303,15 @@ internal class DefaultSharedSecretStorageService @Inject constructor( 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 secretKeySpec = SecretKeySpec(aesKey, "AES") @@ -313,17 +322,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor( require(decryptedSecret.isNotEmpty()) - // 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 - } else { - // we are good - return decryptedSecret.toBase64NoPadding() - } + return String(decryptedSecret, Charsets.UTF_8) } override fun getAlgorithmsForSecret(name: String): List<KeyInfoResult> { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt index 5594cbdf17..726d56a2f7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt @@ -49,7 +49,9 @@ internal interface IMXCryptoStore { /** * @return the olm account */ - fun getAccount(): OlmAccount? + fun getOlmAccount(): OlmAccount + + fun getOrCreateOlmAccount(): OlmAccount /** * Retrieve the known inbound group sessions. @@ -159,7 +161,7 @@ internal interface IMXCryptoStore { * * @param account the account to save */ - fun storeAccount(account: OlmAccount) + fun saveOlmAccount() /** * Store a device for a user. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt index 20cc327b3d..7f136df54d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt @@ -122,27 +122,7 @@ internal class RealmCryptoStore @Inject constructor( .setRealmConfiguration(realmConfiguration) .build() - /* ========================================================================================== - * 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) - + init { // Ensure CryptoMetadataEntity is inserted in DB doRealmTransaction(realmConfiguration) { realm -> 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() { olmSessionsToRelease.forEach { @@ -203,20 +204,31 @@ internal class RealmCryptoStore @Inject constructor( }?.deviceId ?: "" } - override fun storeAccount(account: OlmAccount) { - olmAccount = account - + override fun saveOlmAccount() { doRealmTransaction(realmConfiguration) { - it.where<CryptoMetadataEntity>().findFirst()?.putOlmAccount(account) + it.where<CryptoMetadataEntity>().findFirst()?.putOlmAccount(olmAccount) } } - override fun getAccount(): OlmAccount? { - if (olmAccount == null) { - olmAccount = doRealmQueryAndCopy(realmConfiguration) { it.where<CryptoMetadataEntity>().findFirst() }?.getOlmAccount() - } + override fun getOlmAccount(): OlmAccount { + return olmAccount!! + } - 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?) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt index af097f4431..6574132c7f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt @@ -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.util.convertToUTF8 import org.greenrobot.eventbus.EventBus +import timber.log.Timber import javax.inject.Inject internal interface UploadKeysTask : Task<UploadKeysTask.Params, KeysUploadResponse> { @@ -50,6 +51,8 @@ internal class DefaultUploadKeysTask @Inject constructor( oneTimeKeys = params.oneTimeKeys ) + Timber.i("## Uploading device keys -> $body") + return executeRequest(eventBus) { apiCall = if (encodedDeviceId.isBlank()) { cryptoApi.uploadKeys(body) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt index 989ddc9804..e3a765f95c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt @@ -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.VerificationTxState import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.internal.crypto.IncomingGossipingRequestManager import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore @@ -35,6 +36,7 @@ internal class DefaultIncomingSASDefaultVerificationTransaction( private val cryptoStore: IMXCryptoStore, crossSigningService: CrossSigningService, outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + incomingGossipingRequestManager: IncomingGossipingRequestManager, deviceFingerprint: String, transactionId: String, otherUserID: String, @@ -46,6 +48,7 @@ internal class DefaultIncomingSASDefaultVerificationTransaction( cryptoStore, crossSigningService, outgoingGossipingRequestManager, + incomingGossipingRequestManager, deviceFingerprint, transactionId, otherUserID, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt index 6c7e8f29d3..7fd97d0231 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt @@ -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.VerificationTxState import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.internal.crypto.IncomingGossipingRequestManager import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore @@ -32,6 +33,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction( cryptoStore: IMXCryptoStore, crossSigningService: CrossSigningService, outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + incomingGossipingRequestManager: IncomingGossipingRequestManager, deviceFingerprint: String, transactionId: String, otherUserId: String, @@ -43,6 +45,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction( cryptoStore, crossSigningService, outgoingGossipingRequestManager, + incomingGossipingRequestManager, deviceFingerprint, transactionId, otherUserId, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt index 400b2c520e..dc68fa6b76 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt @@ -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.ValidVerificationDone 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.OutgoingGossipingRequestManager import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction @@ -90,6 +91,7 @@ internal class DefaultVerificationService @Inject constructor( @DeviceId private val deviceId: String?, private val cryptoStore: IMXCryptoStore, private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + private val incomingGossipingRequestManager: IncomingGossipingRequestManager, private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>, private val deviceListManager: DeviceListManager, private val setDeviceVerificationAction: SetDeviceVerificationAction, @@ -454,7 +456,7 @@ internal class DefaultVerificationService @Inject constructor( private suspend fun handleStart(otherUserId: String?, startReq: ValidVerificationInfoStart, txConfigure: (DefaultVerificationTransaction) -> Unit): CancelCode? { - Timber.d("## SAS onStartRequestReceived ${startReq.transactionId}") + Timber.d("## SAS onStartRequestReceived $startReq") if (otherUserId?.let { checkKeysAreDownloaded(it, startReq.fromDevice) } != null) { val tid = startReq.transactionId 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. // In the case of a single user verifying two of their devices, the device ID is // compared instead . - if (existing != null && !existing.isIncoming) { + if (existing is DefaultOutgoingSASDefaultVerificationTransaction) { val readyRequest = getExistingVerificationRequest(otherUserId, tid) if (readyRequest?.isReady == true) { if (isOtherPrioritary(otherUserId, existing.otherDeviceId ?: "")) { + Timber.d("## SAS concurrent start isOtherPrioritary, clear") // The other is prioritary! // I should replace my outgoing with an incoming removeTransaction(otherUserId, tid) existing = null } else { + Timber.d("## SAS concurrent start i am prioritary, ignore") // i am prioritary, ignore this start event! return null } @@ -530,6 +534,7 @@ internal class DefaultVerificationService @Inject constructor( cryptoStore, crossSigningService, outgoingGossipingRequestManager, + incomingGossipingRequestManager, myDeviceInfoHolder.get().myDevice.fingerprint()!!, startReq.transactionId, otherUserId, @@ -544,7 +549,7 @@ internal class DefaultVerificationService @Inject constructor( existing.onStartReceived(startReq) return null } else { - Timber.w("## SAS onStartRequestReceived - unexpected message ${startReq.transactionId}") + Timber.w("## SAS onStartRequestReceived - unexpected message ${startReq.transactionId} / $existing") return CancelCode.UnexpectedMessage } } @@ -754,6 +759,7 @@ internal class DefaultVerificationService @Inject constructor( private suspend fun onReadyReceived(event: Event) { val readyReq = event.getClearContent().toModel<KeyVerificationReady>()?.asValidObject() + Timber.v("## SAS onReadyReceived $readyReq") if (readyReq == null || event.senderId == null) { // ignore @@ -834,17 +840,18 @@ internal class DefaultVerificationService @Inject constructor( if (readyReq.methods.contains(VERIFICATION_METHOD_RECIPROCATE)) { // Create the pending transaction val tx = DefaultQrCodeVerificationTransaction( - setDeviceVerificationAction, - readyReq.transactionId, - senderId, - readyReq.fromDevice, - crossSigningService, - outgoingGossipingRequestManager, - cryptoStore, - qrCodeData, - userId, - deviceId ?: "", - false) + setDeviceVerificationAction = setDeviceVerificationAction, + transactionId = readyReq.transactionId, + otherUserId = senderId, + otherDeviceId = readyReq.fromDevice, + crossSigningService = crossSigningService, + outgoingGossipingRequestManager = outgoingGossipingRequestManager, + incomingGossipingRequestManager = incomingGossipingRequestManager, + cryptoStore = cryptoStore, + qrCodeData = qrCodeData, + userId = userId, + deviceId = deviceId ?: "", + isIncoming = false) tx.transport = transportCreator.invoke(tx) @@ -1001,13 +1008,11 @@ internal class DefaultVerificationService @Inject constructor( } private fun addTransaction(tx: DefaultVerificationTransaction) { - tx.otherUserId.let { otherUserId -> - synchronized(txMap) { - val txInnerMap = txMap.getOrPut(otherUserId) { HashMap() } - txInnerMap[tx.transactionId] = tx - dispatchTxAdded(tx) - tx.addListener(this) - } + synchronized(txMap) { + val txInnerMap = txMap.getOrPut(tx.otherUserId) { HashMap() } + txInnerMap[tx.transactionId] = tx + dispatchTxAdded(tx) + tx.addListener(this) } } @@ -1022,6 +1027,7 @@ internal class DefaultVerificationService @Inject constructor( cryptoStore, crossSigningService, outgoingGossipingRequestManager, + incomingGossipingRequestManager, myDeviceInfoHolder.get().myDevice.fingerprint()!!, txID, otherUserId, @@ -1096,6 +1102,18 @@ internal class DefaultVerificationService @Inject constructor( 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 { // TODO refactor this with the DM one Timber.i("## Requesting verification to user: $otherUserId with device list $otherDevices") @@ -1198,6 +1216,7 @@ internal class DefaultVerificationService @Inject constructor( cryptoStore, crossSigningService, outgoingGossipingRequestManager, + incomingGossipingRequestManager, myDeviceInfoHolder.get().myDevice.fingerprint()!!, transactionId, otherUserId, @@ -1329,17 +1348,18 @@ internal class DefaultVerificationService @Inject constructor( if (VERIFICATION_METHOD_RECIPROCATE in result) { // Create the pending transaction val tx = DefaultQrCodeVerificationTransaction( - setDeviceVerificationAction, - transactionId, - otherUserId, - otherDeviceId, - crossSigningService, - outgoingGossipingRequestManager, - cryptoStore, - qrCodeData, - userId, - deviceId ?: "", - false) + setDeviceVerificationAction = setDeviceVerificationAction, + transactionId = transactionId, + otherUserId = otherUserId, + otherDeviceId = otherDeviceId, + crossSigningService = crossSigningService, + outgoingGossipingRequestManager = outgoingGossipingRequestManager, + incomingGossipingRequestManager = incomingGossipingRequestManager, + cryptoStore = cryptoStore, + qrCodeData = qrCodeData, + userId = userId, + deviceId = deviceId ?: "", + isIncoming = false) tx.transport = transportCreator.invoke(tx) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationTransaction.kt index cb83fdfe8b..973b9944cb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationTransaction.kt @@ -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.verification.VerificationTransaction 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.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel @@ -33,6 +34,7 @@ internal abstract class DefaultVerificationTransaction( private val setDeviceVerificationAction: SetDeviceVerificationAction, private val crossSigningService: CrossSigningService, private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + private val incomingGossipingRequestManager: IncomingGossipingRequestManager, private val userId: String, override val transactionId: String, override val otherUserId: String, @@ -86,6 +88,8 @@ internal abstract class DefaultVerificationTransaction( } if (otherUserId == userId) { + incomingGossipingRequestManager.onVerificationCompleteForDevice(otherDeviceId!!) + // 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 crossSigningService.trustDevice(otherDeviceId!!, object : MatrixCallback<Unit> { @@ -96,7 +100,7 @@ internal abstract class DefaultVerificationTransaction( } 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(USER_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*"))) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASDefaultVerificationTransaction.kt index cfb3d7e38e..a878ad06eb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASDefaultVerificationTransaction.kt @@ -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.VerificationTxState import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.internal.crypto.IncomingGossipingRequestManager import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.model.MXKey @@ -44,6 +45,7 @@ internal abstract class SASDefaultVerificationTransaction( private val cryptoStore: IMXCryptoStore, crossSigningService: CrossSigningService, outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + incomingGossipingRequestManager: IncomingGossipingRequestManager, private val deviceFingerprint: String, transactionId: String, otherUserId: String, @@ -53,6 +55,7 @@ internal abstract class SASDefaultVerificationTransaction( setDeviceVerificationAction, crossSigningService, outgoingGossipingRequestManager, + incomingGossipingRequestManager, userId, transactionId, otherUserId, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt index 8fc12f000a..41d8ce7f44 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt @@ -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.VerificationTxState import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.internal.crypto.IncomingGossipingRequestManager import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64 @@ -38,6 +39,7 @@ internal class DefaultQrCodeVerificationTransaction( override var otherDeviceId: String?, private val crossSigningService: CrossSigningService, outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + incomingGossipingRequestManager: IncomingGossipingRequestManager, private val cryptoStore: IMXCryptoStore, // Not null only if other user is able to scan QR code private val qrCodeData: QrCodeData?, @@ -48,6 +50,7 @@ internal class DefaultQrCodeVerificationTransaction( setDeviceVerificationAction, crossSigningService, outgoingGossipingRequestManager, + incomingGossipingRequestManager, userId, transactionId, otherUserId, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt index 2185d3b278..22ebb4273a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt @@ -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.CryptoModule 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.di.MatrixComponent import im.vector.matrix.android.internal.di.SessionAssistedInjectModule @@ -109,8 +110,11 @@ internal interface SessionComponent { fun inject(worker: SendVerificationMessageWorker) fun inject(worker: SendGossipRequestWorker) + fun inject(worker: CancelGossipRequestWorker) + fun inject(worker: SendGossipWorker) + @Component.Factory interface Factory { fun create( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt index 4071c9224f..4fa0cb5013 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt @@ -53,9 +53,9 @@ internal class FileUploader @Inject constructor(@Authenticated suspend fun uploadByteArray(byteArray: ByteArray, filename: String?, - mimeType: String, + mimeType: String?, progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { - val uploadBody = byteArray.toRequestBody(mimeType.toMediaTypeOrNull()) + val uploadBody = byteArray.toRequestBody(mimeType?.toMediaTypeOrNull()) return upload(uploadBody, filename, progressListener) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ThumbnailExtractor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ThumbnailExtractor.kt index 083cac0278..eae2bf8f6d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ThumbnailExtractor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ThumbnailExtractor.kt @@ -16,12 +16,12 @@ package im.vector.matrix.android.internal.session.content +import android.content.Context import android.graphics.Bitmap -import android.media.ThumbnailUtils -import android.provider.MediaStore +import android.media.MediaMetadataRetriever import im.vector.matrix.android.api.session.content.ContentAttachmentData +import timber.log.Timber import java.io.ByteArrayOutputStream -import java.io.File internal object ThumbnailExtractor { @@ -33,34 +33,40 @@ internal object ThumbnailExtractor { val mimeType: String ) - fun extractThumbnail(attachment: ContentAttachmentData): ThumbnailData? { - val file = File(attachment.path) - if (!file.exists() || !file.isFile) { - return null - } + fun extractThumbnail(context: Context, attachment: ContentAttachmentData): ThumbnailData? { return if (attachment.type == ContentAttachmentData.Type.VIDEO) { - extractVideoThumbnail(attachment) + extractVideoThumbnail(context, attachment) } else { null } } - private fun extractVideoThumbnail(attachment: ContentAttachmentData): ThumbnailData? { - val thumbnail = ThumbnailUtils.createVideoThumbnail(attachment.path, MediaStore.Video.Thumbnails.MINI_KIND) ?: return null - val outputStream = ByteArrayOutputStream() - thumbnail.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) - val thumbnailWidth = thumbnail.width - val thumbnailHeight = thumbnail.height - val thumbnailSize = outputStream.size() - val thumbnailData = ThumbnailData( - width = thumbnailWidth, - height = thumbnailHeight, - size = thumbnailSize.toLong(), - bytes = outputStream.toByteArray(), - mimeType = "image/jpeg" - ) - thumbnail.recycle() - outputStream.reset() + private fun extractVideoThumbnail(context: Context, attachment: ContentAttachmentData): ThumbnailData? { + var thumbnailData: ThumbnailData? = null + val mediaMetadataRetriever = MediaMetadataRetriever() + try { + mediaMetadataRetriever.setDataSource(context, attachment.queryUri) + val thumbnail = mediaMetadataRetriever.frameAtTime + + val outputStream = ByteArrayOutputStream() + thumbnail.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) + val thumbnailWidth = thumbnail.width + val thumbnailHeight = thumbnail.height + val thumbnailSize = outputStream.size() + thumbnailData = ThumbnailData( + width = thumbnailWidth, + height = thumbnailHeight, + 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 } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt index 1c88f87804..1b736d349f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt @@ -17,12 +17,9 @@ package im.vector.matrix.android.internal.session.content import android.content.Context -import android.graphics.BitmapFactory import androidx.work.CoroutineWorker import androidx.work.WorkerParameters 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.events.model.Event 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 timber.log.Timber import java.io.ByteArrayInputStream -import java.io.File -import java.io.FileInputStream import javax.inject.Inject private data class NewImageAttributes( @@ -94,8 +89,90 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter var newImageAttributes: NewImageAttributes? = null - val attachmentFile = try { - File(attachment.path) + try { + 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) { Timber.e(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) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index f10c40ded5..a4a6eb6972 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.internal.session.room.send +import android.content.Context import android.graphics.Bitmap import android.media.MediaMetadataRetriever import androidx.exifinterface.media.ExifInterface @@ -74,6 +75,7 @@ import javax.inject.Inject * The transactionId is used as loc */ internal class LocalEchoEventFactory @Inject constructor( + private val context: Context, @UserId private val userId: String, private val stringProvider: StringProvider, private val textPillsUtils: TextPillsUtils, @@ -266,14 +268,14 @@ internal class LocalEchoEventFactory @Inject constructor( height = height?.toInt() ?: 0, size = attachment.size.toInt() ), - url = attachment.path + url = attachment.queryUri.toString() ) return createEvent(roomId, content) } private fun createVideoEvent(roomId: String, attachment: ContentAttachmentData): Event { 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 val firstFrame: Bitmap? = mediaDataRetriever.frameAtTime @@ -281,7 +283,7 @@ internal class LocalEchoEventFactory @Inject constructor( val width = firstFrame?.width ?: 0 mediaDataRetriever.release() - val thumbnailInfo = ThumbnailExtractor.extractThumbnail(attachment)?.let { + val thumbnailInfo = ThumbnailExtractor.extractThumbnail(context, attachment)?.let { ThumbnailInfo( width = it.width, height = it.height, @@ -299,10 +301,10 @@ internal class LocalEchoEventFactory @Inject constructor( size = attachment.size, duration = attachment.duration?.toInt() ?: 0, // Glide will be able to use the local path and extract a thumbnail. - thumbnailUrl = attachment.path, + thumbnailUrl = attachment.queryUri.toString(), thumbnailInfo = thumbnailInfo ), - url = attachment.path + url = attachment.queryUri.toString() ) return createEvent(roomId, content) } @@ -315,7 +317,7 @@ internal class LocalEchoEventFactory @Inject constructor( mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() } ?: "audio/mpeg", size = attachment.size ), - url = attachment.path + url = attachment.queryUri.toString() ) return createEvent(roomId, content) } @@ -329,7 +331,7 @@ internal class LocalEchoEventFactory @Inject constructor( ?: "application/octet-stream", size = attachment.size ), - url = attachment.path + url = attachment.queryUri.toString() ) return createEvent(roomId, content) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt index 02c06b0e56..e129513d4b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt @@ -128,7 +128,7 @@ internal class TimelineEventDecryptor @Inject constructor( } } } catch (t: Throwable) { - Timber.e(t, "Failed to decrypt event $eventId") + Timber.e("Failed to decrypt event $eventId, ${t.localizedMessage}") } finally { synchronized(existingRequests) { existingRequests.remove(request) diff --git a/multipicker/.gitignore b/multipicker/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/multipicker/.gitignore @@ -0,0 +1 @@ +/build diff --git a/multipicker/build.gradle b/multipicker/build.gradle new file mode 100644 index 0000000000..8b08a9d3ef --- /dev/null +++ b/multipicker/build.gradle @@ -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' +} diff --git a/multipicker/consumer-rules.pro b/multipicker/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/multipicker/proguard-rules.pro b/multipicker/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/multipicker/proguard-rules.pro @@ -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 diff --git a/multipicker/src/main/AndroidManifest.xml b/multipicker/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..e1f12697e0 --- /dev/null +++ b/multipicker/src/main/AndroidManifest.xml @@ -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> diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/AudioPicker.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/AudioPicker.kt new file mode 100644 index 0000000000..05e4c337b6 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/AudioPicker.kt @@ -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/*" + } + } +} diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/CameraPicker.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/CameraPicker.kt new file mode 100644 index 0000000000..240d809373 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/CameraPicker.kt @@ -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 */ + ) + } +} diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/ContactPicker.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/ContactPicker.kt new file mode 100644 index 0000000000..b0ae0e4cda --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/ContactPicker.kt @@ -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 + } + } +} diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/FilePicker.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/FilePicker.kt new file mode 100644 index 0000000000..e8c74fad19 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/FilePicker.kt @@ -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 = "*/*" + } + } +} diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/ImagePicker.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/ImagePicker.kt new file mode 100644 index 0000000000..d7bf383f03 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/ImagePicker.kt @@ -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/*" + } + } +} diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/MultiPicker.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/MultiPicker.kt new file mode 100644 index 0000000000..24769e11c3 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/MultiPicker.kt @@ -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") + } + } + } +} diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/Picker.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/Picker.kt new file mode 100644 index 0000000000..43ac5d5fdd --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/Picker.kt @@ -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 + } +} diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/VideoPicker.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/VideoPicker.kt new file mode 100644 index 0000000000..b85ffacd48 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/VideoPicker.kt @@ -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/*" + } + } +} diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerAudioType.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerAudioType.kt new file mode 100644 index 0000000000..6afe022024 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerAudioType.kt @@ -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 diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerBaseType.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerBaseType.kt new file mode 100644 index 0000000000..777e4d8441 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerBaseType.kt @@ -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 +} diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerContactType.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerContactType.kt new file mode 100644 index 0000000000..a9135443a2 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerContactType.kt @@ -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> +) diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerFileType.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerFileType.kt new file mode 100644 index 0000000000..5417520d28 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerFileType.kt @@ -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 diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerImageType.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerImageType.kt new file mode 100644 index 0000000000..b1aef171b4 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerImageType.kt @@ -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 diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerVideoType.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerVideoType.kt new file mode 100644 index 0000000000..ba9a8d233e --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerVideoType.kt @@ -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 diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/provider/MultiPickerFileProvider.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/provider/MultiPickerFileProvider.kt new file mode 100644 index 0000000000..048b2ca199 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/provider/MultiPickerFileProvider.kt @@ -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() diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/utils/ImageUtils.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/utils/ImageUtils.kt new file mode 100644 index 0000000000..c5171e7d84 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/utils/ImageUtils.kt @@ -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 + } +} diff --git a/multipicker/src/main/res/xml/multipicker_provider_paths.xml b/multipicker/src/main/res/xml/multipicker_provider_paths.xml new file mode 100644 index 0000000000..ff9b81ce98 --- /dev/null +++ b/multipicker/src/main/res/xml/multipicker_provider_paths.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<paths> + <files-path + name="external_files" + path="." /> +</paths> \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index d020abade4..04307e89d9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,2 @@ include ':vector', ':matrix-sdk-android', ':matrix-sdk-android-rx', ':diff-match-patch' +include ':multipicker' diff --git a/vector/build.gradle b/vector/build.gradle index 5b7a18b531..96e6e5a2a8 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -254,6 +254,7 @@ dependencies { implementation project(":matrix-sdk-android") implementation project(":matrix-sdk-android-rx") implementation project(":diff-match-patch") + implementation project(":multipicker") implementation 'com.android.support:multidex:1.0.3' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" @@ -323,7 +324,7 @@ dependencies { implementation 'com.nulab-inc:zxcvbn:1.2.7' //Alerter - implementation 'com.tapadoo.android:alerter:4.0.3' + implementation 'com.tapadoo.android:alerter:5.1.2' implementation 'com.otaliastudios:autocomplete:1.1.0' @@ -347,9 +348,6 @@ dependencies { // Badge for compatibility implementation 'me.leolin:ShortcutBadger:1.1.22@aar' - // File picker - implementation 'com.kbeanie:multipicker:1.6@aar' - // DI implementation "com.google.dagger:dagger:$daggerVersion" kapt "com.google.dagger:dagger-compiler:$daggerVersion" diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 2e56e20ce7..092817a6cc 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.VIBRATE" /> <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 --> <!-- Tell that the Camera is not mandatory to install the application --> diff --git a/vector/src/main/java/im/vector/riotx/VectorApplication.kt b/vector/src/main/java/im/vector/riotx/VectorApplication.kt index 81cf1402b0..bd85596924 100644 --- a/vector/src/main/java/im/vector/riotx/VectorApplication.kt +++ b/vector/src/main/java/im/vector/riotx/VectorApplication.kt @@ -48,6 +48,7 @@ import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.notifications.NotificationUtils 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.session.SessionListener import im.vector.riotx.features.settings.VectorPreferences @@ -77,6 +78,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration. @Inject lateinit var notificationUtils: NotificationUtils @Inject lateinit var appStateHandler: AppStateHandler @Inject lateinit var rxConfig: RxConfig + @Inject lateinit var popupAlertManager: PopupAlertManager lateinit var vectorComponent: VectorComponent private var fontThreadHandler: Handler? = null @@ -102,7 +104,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration. BigImageViewer.initialize(GlideImageLoader.with(applicationContext)) EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler() EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler() - registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks()) + registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks(popupAlertManager)) val fontRequest = FontRequest( "com.google.android.gms.fonts", "com.google.android.gms", diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index 4e60a1bdf7..4deaef32ab 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -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.CreateDirectRoomKnownUsersFragment 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.conclusion.VerificationConclusionFragment import im.vector.riotx.features.crypto.verification.emoji.VerificationEmojiCodeFragment @@ -336,6 +338,16 @@ interface FragmentModule { @FragmentKey(VerificationConclusionFragment::class) 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 @IntoMap @FragmentKey(QrCodeScannerFragment::class) diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt index 4ae92b29b1..2652f58b04 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt @@ -45,6 +45,7 @@ import im.vector.riotx.features.notifications.NotificationBroadcastReceiver import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.notifications.NotificationUtils 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.VectorFileLogger import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler @@ -128,6 +129,8 @@ interface VectorComponent { fun emojiDataSource(): EmojiDataSource + fun alertManager() : PopupAlertManager + @Component.Factory interface Factory { fun create(@BindsInstance context: Context): VectorComponent diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/RecyclerView.kt b/vector/src/main/java/im/vector/riotx/core/extensions/RecyclerView.kt index 51f3ce611a..3b3132229c 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/RecyclerView.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/RecyclerView.kt @@ -16,10 +16,13 @@ package im.vector.riotx.core.extensions +import androidx.core.content.ContextCompat import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView 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 @@ -40,7 +43,13 @@ fun RecyclerView.configureWith(epoxyController: EpoxyController, itemAnimator?.let { this.itemAnimator = it } } 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) adapter = epoxyController.adapter diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt index c576ebe1b9..daea538e12 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt @@ -18,20 +18,13 @@ package im.vector.riotx.features.attachments import android.app.Activity import android.content.Context import android.content.Intent +import android.net.Uri import android.os.Bundle 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.api.session.content.ContentAttachmentData 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 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. - * 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, - 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)) - } - } +class AttachmentsHelper(val context: Context, val callback: Callback) : Restorable { interface Callback { 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. - 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. 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 override fun onSaveInstanceState(outState: Bundle) { - capturePath?.also { - outState.putString(CAPTURE_PATH_KEY, it) + captureUri?.also { + outState.putParcelable(CAPTURE_PATH_KEY, it) } pendingType?.also { outState.putSerializable(PENDING_TYPE_KEY, it) @@ -106,10 +63,7 @@ class AttachmentsHelper private constructor(private val context: Context, } override fun onRestoreInstanceState(savedInstanceState: Bundle?) { - capturePath = savedInstanceState?.getString(CAPTURE_PATH_KEY) - if (capturePath != null) { - cameraImagePicker.reinitialize(capturePath) - } + captureUri = savedInstanceState?.getParcelable(CAPTURE_PATH_KEY) as? Uri 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 */ - fun selectFile() { - filePicker.pickFile() + fun selectFile(fragment: Fragment) { + MultiPicker.get(MultiPicker.FILE).startWith(fragment) } /** * Starts the process for handling image picking */ - fun selectGallery() { - imagePicker.pickImage() + fun selectGallery(fragment: Fragment) { + MultiPicker.get(MultiPicker.IMAGE).startWith(fragment) } /** * Starts the process for handling audio picking */ - fun selectAudio() { - audioPicker.pickAudio() + fun selectAudio(fragment: Fragment) { + MultiPicker.get(MultiPicker.AUDIO).startWith(fragment) } /** * Starts the process for handling capture image picking */ - fun openCamera() { - capturePath = cameraImagePicker.pickImage() + fun openCamera(fragment: Fragment) { + captureUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(fragment) } /** * Starts the process for handling contact picking */ - fun selectContact() { - contactPicker.pickContact() + fun selectContact(fragment: Fragment) { + 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 { if (resultCode == Activity.RESULT_OK) { - val pickerManager = getPickerManagerForRequestCode(requestCode) - if (pickerManager != null) { - if (pickerManager is ImagePickerImpl) { - pickerManager.reinitialize(capturePath) + when (requestCode) { + MultiPicker.REQUEST_CODE_PICK_FILE -> { + callback.onContentAttachmentsReady( + MultiPicker.get(MultiPicker.FILE) + .getSelectedFiles(context, requestCode, resultCode, data) + .map { it.toContentAttachmentData() } + ) } - pickerManager.submit(data) - return true + MultiPicker.REQUEST_CODE_PICK_AUDIO -> { + 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 } @@ -174,39 +172,35 @@ class AttachmentsHelper private constructor(private val context: Context, * * @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 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")) { - videoPicker.submit(safeShareIntent(intent)) + callback.onContentAttachmentsReady( + MultiPicker.get(MultiPicker.VIDEO).getIncomingFiles(context, intent).map { + it.toContentAttachmentData() + } + ) } 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("*")) { - filePicker.submit(safeShareIntent(intent)) + callback.onContentAttachmentsReady( + MultiPicker.get(MultiPicker.FILE).getIncomingFiles(context, intent).map { + it.toContentAttachmentData() + } + ) } else { return false } 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 - } - } } diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt index a3de5084de..02b712b8a7 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt @@ -16,51 +16,48 @@ 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.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 -fun ChosenContact.toContactAttachment(): ContactAttachment { +fun MultiPickerContactType.toContactAttachment(): ContactAttachment { return ContactAttachment( displayName = displayName, photoUri = photoUri, - emails = emails.toList(), - phones = phones.toList() + emails = emailList.toList(), + phones = phoneNumberList.toList() ) } -fun ChosenFile.toContentAttachmentData(): ContentAttachmentData { +fun MultiPickerFileType.toContentAttachmentData(): ContentAttachmentData { if (mimeType == null) Timber.w("No mimeType") return ContentAttachmentData( - path = originalPath, mimeType = mimeType, type = mapType(), size = size, - date = createdAt?.time ?: System.currentTimeMillis(), name = displayName, - queryUri = queryUri + queryUri = contentUri ) } -fun ChosenAudio.toContentAttachmentData(): ContentAttachmentData { +fun MultiPickerAudioType.toContentAttachmentData(): ContentAttachmentData { if (mimeType == null) Timber.w("No mimeType") return ContentAttachmentData( - path = originalPath, mimeType = mimeType, type = mapType(), size = size, - date = createdAt?.time ?: System.currentTimeMillis(), name = displayName, duration = duration, - queryUri = queryUri + queryUri = contentUri ) } -private fun ChosenFile.mapType(): ContentAttachmentData.Type { +private fun MultiPickerBaseType.mapType(): ContentAttachmentData.Type { return when { mimeType?.startsWith("image/") == true -> ContentAttachmentData.Type.IMAGE 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") return ContentAttachmentData( - path = originalPath, mimeType = mimeType, type = mapType(), name = displayName, @@ -80,23 +76,20 @@ fun ChosenImage.toContentAttachmentData(): ContentAttachmentData { height = height.toLong(), width = width.toLong(), exifOrientation = orientation, - date = createdAt?.time ?: System.currentTimeMillis(), - queryUri = queryUri + queryUri = contentUri ) } -fun ChosenVideo.toContentAttachmentData(): ContentAttachmentData { +fun MultiPickerVideoType.toContentAttachmentData(): ContentAttachmentData { if (mimeType == null) Timber.w("No mimeType") return ContentAttachmentData( - path = originalPath, mimeType = mimeType, type = ContentAttachmentData.Type.VIDEO, size = size, - date = createdAt?.time ?: System.currentTimeMillis(), height = height.toLong(), width = width.toLong(), duration = duration, name = displayName, - queryUri = queryUri + queryUri = contentUri ) } diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsPickerCallback.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsPickerCallback.kt deleted file mode 100644 index 62956e08c8..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsPickerCallback.kt +++ /dev/null @@ -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() - } -} diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/PickerManagerFactory.kt b/vector/src/main/java/im/vector/riotx/features/attachments/PickerManagerFactory.kt deleted file mode 100644 index 6c03f21ab3..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/attachments/PickerManagerFactory.kt +++ /dev/null @@ -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) - } - } -} diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewControllers.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewControllers.kt index 34f018aaf9..60ee722116 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewControllers.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewControllers.kt @@ -25,7 +25,7 @@ class AttachmentBigPreviewController @Inject constructor() : TypedEpoxyControlle override fun buildModels(data: AttachmentsPreviewViewState) { data.attachments.forEach { attachmentBigPreviewItem { - id(it.path) + id(it.queryUri.toString()) attachment(it) } } @@ -43,7 +43,7 @@ class AttachmentMiniaturePreviewController @Inject constructor() : TypedEpoxyCon override fun buildModels(data: AttachmentsPreviewViewState) { data.attachments.forEachIndexed { index, contentAttachmentData -> attachmentMiniaturePreviewItem { - id(contentAttachmentData.path) + id(contentAttachmentData.queryUri.toString()) attachment(contentAttachmentData) checked(data.currentAttachmentIndex == index) clickListener { _ -> diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewItems.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewItems.kt index 3b43fa6e20..373298bf31 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewItems.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewItems.kt @@ -33,11 +33,10 @@ abstract class AttachmentPreviewItem<H : AttachmentPreviewItem.Holder> : VectorE abstract val attachment: ContentAttachmentData override fun bind(holder: H) { - val path = attachment.path if (attachment.type == ContentAttachmentData.Type.VIDEO || attachment.type == ContentAttachmentData.Type.IMAGE) { Glide.with(holder.view.context) .asBitmap() - .load(path) + .load(attachment.queryUri) .apply(RequestOptions().frame(0)) .into(holder.imageView) } else { diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewAction.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewAction.kt index 5acc59b035..aef724331f 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewAction.kt @@ -17,10 +17,11 @@ package im.vector.riotx.features.attachments.preview +import android.net.Uri import im.vector.riotx.core.platform.VectorViewModelAction sealed class AttachmentsPreviewAction : VectorViewModelAction { object RemoveCurrentAttachment : AttachmentsPreviewAction() data class SetCurrentAttachment(val index: Int): AttachmentsPreviewAction() - data class UpdatePathOfCurrentAttachment(val newPath: String): AttachmentsPreviewAction() + data class UpdatePathOfCurrentAttachment(val newUri: Uri): AttachmentsPreviewAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt index e52b497df4..3b1972ffbc 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt @@ -172,9 +172,9 @@ class AttachmentsPreviewFragment @Inject constructor( } private fun handleCropResult(result: Intent) { - val resultPath = UCrop.getOutput(result)?.path - if (resultPath != null) { - viewModel.handle(AttachmentsPreviewAction.UpdatePathOfCurrentAttachment(resultPath)) + val resultUri = UCrop.getOutput(result) + if (resultUri != null) { + viewModel.handle(AttachmentsPreviewAction.UpdatePathOfCurrentAttachment(resultUri)) } else { Toast.makeText(requireContext(), "Cannot retrieve cropped value", Toast.LENGTH_SHORT).show() } @@ -202,8 +202,7 @@ class AttachmentsPreviewFragment @Inject constructor( private fun doHandleEditAction() = withState(viewModel) { val currentAttachment = it.attachments.getOrNull(it.currentAttachmentIndex) ?: return@withState 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 = File(currentAttachment.path).toUri() + val uri = currentAttachment.queryUri UCrop.of(uri, destinationFile.toUri()) .withOptions( UCrop.Options() diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewModel.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewModel.kt index 1f6c8c2f8b..d1e44fa963 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewModel.kt @@ -62,7 +62,7 @@ class AttachmentsPreviewViewModel @AssistedInject constructor(@Assisted initialS private fun handleUpdatePathOfCurrentAttachment(action: AttachmentsPreviewAction.UpdatePathOfCurrentAttachment) = withState { val attachments = it.attachments.mapIndexed { index, contentAttachmentData -> if (index == it.currentAttachmentIndex) { - contentAttachmentData.copy(path = action.newPath) + contentAttachmentData.copy(queryUri = action.newUri) } else { contentAttachmentData } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt index d9860e6bad..ddb50628d6 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt @@ -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.DevicesListResponse import im.vector.riotx.R +import im.vector.riotx.features.popup.DefaultVectorAlert import im.vector.riotx.features.popup.PopupAlertManager import timber.log.Timber import java.text.DateFormat @@ -54,7 +55,7 @@ import javax.inject.Singleton */ @Singleton -class KeyRequestHandler @Inject constructor(private val context: Context) +class KeyRequestHandler @Inject constructor(private val context: Context, private val popupAlertManager: PopupAlertManager) : GossipingRequestListener, VerificationService.Listener { @@ -118,9 +119,9 @@ class KeyRequestHandler @Inject constructor(private val context: Context) } 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? 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), context.getString(R.string.key_share_request), dialogText, @@ -210,7 +211,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context) denyAllRequests(mappingKey) }) - PopupAlertManager.postVectorAlert(alert) + popupAlertManager.postVectorAlert(alert) } private fun denyAllRequests(mappingKey: String) { @@ -250,7 +251,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context) && it.requestId == request.requestId } if (alertsToRequests[alertMgrUniqueKey]?.isEmpty() == true) { - PopupAlertManager.cancelAlert(alertMgrUniqueKey) + popupAlertManager.cancelAlert(alertMgrUniqueKey) alertsToRequests.remove(keyForMap(userId, deviceId)) } } @@ -261,7 +262,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context) if (state == VerificationTxState.Verified) { // ok it's verified, see if we have key request for that 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 @@ -271,7 +272,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context) override fun markedAsManuallyVerified(userId: String, deviceId: String) { // accept related requests shareAllSessions(keyForMap(userId, deviceId)) - PopupAlertManager.cancelAlert(alertManagerId(userId, deviceId)) + popupAlertManager.cancelAlert(alertManagerId(userId, deviceId)) } private fun keyForMap(userId: String, deviceId: String) = "$deviceId$userId" diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/IncomingVerificationRequestHandler.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/IncomingVerificationRequestHandler.kt index e7e26f52a4..ccd3e6578a 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/IncomingVerificationRequestHandler.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/IncomingVerificationRequestHandler.kt @@ -17,15 +17,17 @@ package im.vector.riotx.features.crypto.verification import android.content.Context 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.VerificationTransaction 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.core.platform.VectorBaseActivity import im.vector.riotx.features.home.room.detail.RoomDetailActivity import im.vector.riotx.features.home.room.detail.RoomDetailArgs import im.vector.riotx.features.popup.PopupAlertManager +import im.vector.riotx.features.popup.VerificationVectorAlert import im.vector.riotx.features.themes.ThemeUtils import javax.inject.Inject 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. */ @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 @@ -58,7 +62,7 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context val name = session?.getUser(tx.otherUserId)?.displayName ?: tx.otherUserId - val alert = PopupAlertManager.VectorAlert( + val alert = VerificationVectorAlert( uid, context.getString(R.string.sas_incoming_request_notif_title), 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 :/ activity.supportFragmentManager.findFragmentByTag(VerificationBottomSheet.WAITING_SELF_VERIF_TAG)?.let { false.also { - PopupAlertManager.cancelAlert(uid) + popupAlertManager.cancelAlert(uid) } } ?: true } else true }) .apply { + matrixItem = session?.getUser(tx.otherUserId)?.toMatrixItem() + contentAction = Runnable { (weakCurrentActivity?.get() as? VectorBaseActivity)?.let { it.navigator.performDeviceVerification(it, tx.otherUserId, tx.transactionId) @@ -99,11 +105,11 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context // 10mn expiration expirationTimestamp = System.currentTimeMillis() + (10 * 60 * 1000L) } - PopupAlertManager.postVectorAlert(alert) + popupAlertManager.postVectorAlert(alert) } is VerificationTxState.TerminalTxState -> { // cancel related notification - PopupAlertManager.cancelAlert(uid) + popupAlertManager.cancelAlert(uid) } else -> Unit } @@ -115,7 +121,7 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context val name = session?.getUser(pr.otherUserId)?.displayName ?: pr.otherUserId - val alert = PopupAlertManager.VectorAlert( + val alert = VerificationVectorAlert( uniqueIdForVerificationRequest(pr), context.getString(R.string.sas_incoming_request_notif_title), "$name(${pr.otherUserId})", @@ -128,6 +134,8 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context } else true }) .apply { + matrixItem = session?.getUser(pr.otherUserId)?.toMatrixItem() + contentAction = Runnable { (weakCurrentActivity?.get() as? VectorBaseActivity)?.let { val roomId = pr.roomId @@ -148,14 +156,14 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context // 5mn expiration expirationTimestamp = System.currentTimeMillis() + (5 * 60 * 1000L) } - PopupAlertManager.postVectorAlert(alert) + popupAlertManager.postVectorAlert(alert) } } override fun verificationRequestUpdated(pr: PendingVerificationRequest) { // If an incoming request is readied (by another device?) we should discard the alert if (pr.isIncoming && (pr.isReady || pr.handledByOtherSession)) { - PopupAlertManager.cancelAlert(uniqueIdForVerificationRequest(pr)) + popupAlertManager.cancelAlert(uniqueIdForVerificationRequest(pr)) } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt index e1218ec4a9..5b7adbdb91 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt @@ -16,9 +16,11 @@ package im.vector.riotx.features.crypto.verification import android.app.Activity +import android.app.Dialog import android.content.Intent import android.os.Bundle import android.os.Parcelable +import android.view.KeyEvent import android.view.View import android.widget.ImageView 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.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.verification.CancelCode import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.commitTransaction 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.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.conclusion.VerificationConclusionFragment 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.request.VerificationRequestFragment import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.settings.VectorSettingsActivity import kotlinx.android.parcel.Parcelize import timber.log.Timber import javax.inject.Inject @@ -58,6 +65,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { data class VerificationArgs( val otherUserId: String, val verificationId: String? = null, + val verificationLocalId: String? = null, val roomId: String? = null, // Special mode where UX should show loading wheel until other session sends a request/tx val selfVerificationMode: Boolean = false @@ -80,13 +88,17 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { lateinit var otherUserNameText: TextView @BindView(R.id.verificationRequestShield) - lateinit var otherUserShield: View + lateinit var otherUserShield: ImageView @BindView(R.id.verificationRequestAvatar) lateinit var otherUserAvatarImageView: ImageView override fun getLayoutResId() = R.layout.bottom_sheet_verification + init { + isCancelable = false + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -110,10 +122,27 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { .show() Unit } + VerificationBottomSheetViewEvents.GoToSettings -> { + dismiss() + (activity as? VectorBaseActivity)?.navigator?.openSettings(requireContext(), VectorSettingsActivity.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY) + } }.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?) { if (resultCode == Activity.RESULT_OK && requestCode == SECRET_REQUEST_CODE) { data?.getStringExtra(SharedSecureStorageActivity.EXTRA_DATA_RESULT)?.let { @@ -127,15 +156,16 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { state.otherUserMxItem?.let { matrixItem -> if (state.isMe) { + avatarRenderer.render(matrixItem, otherUserAvatarImageView) if (state.sasTransactionState == VerificationTxState.Verified || state.qrTransactionState == VerificationTxState.Verified || state.verifiedFromPrivateKeys) { - otherUserAvatarImageView.setImageResource(R.drawable.ic_shield_trusted) + otherUserShield.setImageResource(R.drawable.ic_shield_trusted) } else { - otherUserAvatarImageView.setImageResource(R.drawable.ic_shield_warning) + otherUserShield.setImageResource(R.drawable.ic_shield_warning) } otherUserNameText.text = getString(R.string.complete_security) - otherUserShield.isVisible = false + otherUserShield.isVisible = true } else { 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) { showFragment(VerificationConclusionFragment::class, Bundle().apply { putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(true, null, state.isMe)) @@ -222,7 +264,14 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { // Transaction has not yet started if (state.pendingRequest.invoke()?.cancelConclusion != null) { // 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 @@ -267,6 +316,10 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { } } + override fun dismiss() { + super.dismiss() + } + companion object { const val SECRET_REQUEST_CODE = 101 diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewEvents.kt index d7c02a8d3b..7e3a5441de 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewEvents.kt @@ -24,5 +24,6 @@ import im.vector.riotx.core.platform.VectorViewEvents sealed class VerificationBottomSheetViewEvents : VectorViewEvents { object Dismiss : VerificationBottomSheetViewEvents() object AccessSecretStore : VerificationBottomSheetViewEvents() + object GoToSettings : VerificationBottomSheetViewEvents() data class ModalError(val errorMessage: CharSequence) : VerificationBottomSheetViewEvents() } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt index db8dd895b4..731f12cca4 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt @@ -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.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.verification.CancelCode 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.SasVerificationTransaction 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.internal.crypto.crosssigning.fromBase64 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.platform.VectorViewModel 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) val selfVerificationMode: 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 class VerificationBottomSheetViewModel @AssistedInject constructor( @@ -111,7 +115,8 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( pendingRequest = if (pr != null) Success(pr) else Uninitialized, selfVerificationMode = selfVerificationMode, 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 } + 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> { override fun create(viewModelContext: ViewModelContext, state: VerificationBottomSheetViewState): VerificationBottomSheetViewModel? { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationCancelController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationCancelController.kt new file mode 100644 index 0000000000..1beea4ae9f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationCancelController.kt @@ -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() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationCancelFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationCancelFragment.kt new file mode 100644 index 0000000000..0c5c070156 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationCancelFragment.kt @@ -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() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationNotMeController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationNotMeController.kt new file mode 100644 index 0000000000..3978ab8ba5 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationNotMeController.kt @@ -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() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationNotMeFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationNotMeFragment.kt new file mode 100644 index 0000000000..b764639078 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationNotMeFragment.kt @@ -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() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodController.kt index 87bb843291..919869500f 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodController.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodController.kt @@ -95,10 +95,27 @@ class VerificationChooseMethodController @Inject constructor( 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 { fun openCamera() fun doVerifyBySas() + fun onClickOnWasNotMe() } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodFragment.kt index e0b7f97383..eb32f5b0e3 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodFragment.kt @@ -89,6 +89,10 @@ class VerificationChooseMethodFragment @Inject constructor( } } + override fun onClickOnWasNotMe() { + sharedViewModel.itWasNotMe() + } + private fun doOpenQRCodeScanner() { QrCodeScannerActivity.startForResult(this) } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt index c7fdf77123..3c3009ed01 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt @@ -39,7 +39,9 @@ data class VerificationChooseMethodViewState( val otherCanShowQrCode: Boolean = false, val otherCanScanQrCode: Boolean = false, val qrCodeText: String? = null, - val SASModeAvailable: Boolean = false + val SASModeAvailable: Boolean = false, + val isMe: Boolean = false, + val canCrossSign: Boolean = false ) : MvRxState 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 -> 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 ?: "") return VerificationChooseMethodViewState(otherUserId = args.otherUserId, + isMe = session.myUserId == pvr?.otherUserId, + canCrossSign = session.cryptoService().crossSigningService().canCrossSign(), transactionId = args.verificationId ?: "", otherCanShowQrCode = pvr?.otherCanShowQrCode().orFalse(), otherCanScanQrCode = pvr?.otherCanScanQrCode().orFalse(), diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/conclusion/VerificationConclusionController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/conclusion/VerificationConclusionController.kt index 9719651bd4..bd40bec210 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/conclusion/VerificationConclusionController.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/conclusion/VerificationConclusionController.kt @@ -58,6 +58,8 @@ class VerificationConclusionController @Inject constructor( id("image") imageRes(R.drawable.ic_shield_trusted) } + + bottomDone() } ConclusionState.WARNING -> { bottomSheetVerificationNoticeItem { @@ -74,10 +76,32 @@ class VerificationConclusionController @Inject constructor( id("warning_notice") 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 { id("sep0") } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/conclusion/VerificationConclusionFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/conclusion/VerificationConclusionFragment.kt index 854809084e..7405722c04 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/conclusion/VerificationConclusionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/conclusion/VerificationConclusionFragment.kt @@ -66,12 +66,7 @@ class VerificationConclusionFragment @Inject constructor( } override fun invalidate() = withState(viewModel) { state -> - if (state.conclusionState == ConclusionState.CANCELLED) { - // Just dismiss in this case - sharedViewModel.handle(VerificationAction.GotItConclusion) - } else { - controller.update(state) - } + controller.update(state) } override fun onButtonTapped() { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt index 05ed2f1799..60d0d86aeb 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt @@ -84,11 +84,16 @@ class VerificationRequestController @Inject constructor( listener { listener?.onClickDismiss() } } } else { - val styledText = matrixItem.let { - stringProvider.getString(R.string.verification_request_notice, it.id) - .toSpannable() - .colorizeMatchingText(it.id, colorProvider.getColorFromAttribute(R.attr.vctr_notice_text_color)) - } + val styledText = + if (state.isMe) { + stringProvider.getString(R.string.verify_new_session_notice) + } 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 { id("notice") @@ -119,18 +124,42 @@ class VerificationRequestController @Inject constructor( } is Success -> { if (!pr.invoke().isReady) { - bottomSheetVerificationWaitingItem { - id("waiting") - title(stringProvider.getString(R.string.verification_request_waiting_for, matrixItem.getBestName())) + if (state.isMe) { + bottomSheetVerificationWaitingItem { + 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 { fun onClickOnVerificationStart() + fun onClickOnWasNotMe() fun onClickRecoverFromPassphrase() fun onClickDismiss() } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestFragment.kt index 64000d07a1..b6c3659988 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestFragment.kt @@ -69,4 +69,8 @@ class VerificationRequestFragment @Inject constructor( override fun onClickDismiss() { viewModel.handle(VerificationAction.SkipVerification) } + + override fun onClickOnWasNotMe() { + viewModel.itWasNotMe() + } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt index ac4d29dd96..6d85dd8a3e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt @@ -96,8 +96,8 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active // PRIVATE API ********************************************************************************* private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest<Drawable> { - val resolvedUrl = activeSessionHolder.getActiveSession().contentUrlResolver() - .resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE) + val resolvedUrl = activeSessionHolder.getSafeActiveSession()?.contentUrlResolver() + ?.resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE) return glideRequest .load(resolvedUrl) diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt index dfe80de9de..0fa3e5416d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt @@ -41,6 +41,7 @@ import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.pushers.PushersManager import im.vector.riotx.features.disclaimer.showDisclaimerDialog 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.rageshake.VectorUncaughtExceptionHandler import im.vector.riotx.features.settings.VectorPreferences @@ -60,6 +61,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { @Inject lateinit var pushManager: PushersManager @Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var vectorPreferences: VectorPreferences + @Inject lateinit var popupAlertManager: PopupAlertManager private val drawerListener = object : DrawerLayout.SimpleDrawerListener() { override fun onDrawerStateChanged(newState: Int) { @@ -149,8 +151,8 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { if (crossSigningEnabledOnAccount && myCrossSigningKeys?.isTrusted() == false) { // We need to ask sharedActionViewModel.hasDisplayedCompleteSecurityPrompt = true - PopupAlertManager.postVectorAlert( - PopupAlertManager.VectorAlert( + popupAlertManager.postVectorAlert( + DefaultVectorAlert( uid = "completeSecurity", title = getString(R.string.new_signin), description = getString(R.string.complete_security), diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt index 85f14e99a8..47338f6335 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt @@ -1,3 +1,4 @@ + /* * Copyright 2019 New Vector Ltd * @@ -19,8 +20,10 @@ package im.vector.riotx.features.home import android.os.Bundle import android.view.LayoutInflater import android.view.View +import androidx.core.content.ContextCompat import androidx.core.view.forEachIndexed import androidx.lifecycle.Observer +import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState 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.glide.GlideApp 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.ui.views.KeysBackupBanner 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.UnreadCounterBadgeView +import im.vector.riotx.features.popup.PopupAlertManager +import im.vector.riotx.features.popup.VerificationVectorAlert import im.vector.riotx.features.workers.signout.SignOutViewModel import kotlinx.android.synthetic.main.fragment_home_detail.* import timber.log.Timber @@ -48,12 +54,15 @@ private const val INDEX_ROOMS = 2 class HomeDetailFragment @Inject constructor( val homeDetailViewModelFactory: HomeDetailViewModel.Factory, - private val avatarRenderer: AvatarRenderer + private val avatarRenderer: AvatarRenderer, + private val alertManager: PopupAlertManager ) : VectorBaseFragment(), KeysBackupBanner.Delegate { private val unreadCounterBadgeViews = arrayListOf<UnreadCounterBadgeView>() private val viewModel: HomeDetailViewModel by fragmentViewModel() + private val unknownDeviceDetectorSharedViewModel : UnknownDeviceDetectorSharedViewModel by activityViewModel() + private lateinit var sharedActionViewModel: HomeSharedActionViewModel override fun getLayoutResId() = R.layout.fragment_home_detail @@ -77,6 +86,38 @@ class HomeDetailFragment @Inject constructor( viewModel.selectSubscribe(this, HomeDetailViewState::displayMode) { 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?) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/UnknwonDeviceDetectorSharedViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/UnknwonDeviceDetectorSharedViewModel.kt new file mode 100644 index 0000000000..180a989858 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/UnknwonDeviceDetectorSharedViewModel.kt @@ -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) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index e748478e6a..f58d7be718 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -250,7 +250,7 @@ class RoomDetailFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java) - attachmentsHelper = AttachmentsHelper.create(this, this).register() + attachmentsHelper = AttachmentsHelper(requireContext(), this).register() keyboardStateUtils = KeyboardStateUtils(requireActivity()) setupToolbar(roomToolbar) setupRecyclerView() @@ -290,9 +290,9 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.observeViewEvents { when (it) { - is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable) - is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds) - is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it) + is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable) + is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds) + is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it) is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(it) is RoomDetailViewEvents.ShowMessage -> showSnackWithMessage(it.message, Snackbar.LENGTH_LONG) is RoomDetailViewEvents.NavigateToEvent -> navigateToEvent(it) @@ -665,7 +665,7 @@ class RoomDetailFragment @Inject constructor( private fun sendUri(uri: Uri): Boolean { roomDetailViewModel.preventAttachmentPreview = true val shareIntent = Intent(Intent.ACTION_SEND, uri) - val isHandled = attachmentsHelper.handleShareIntent(shareIntent) + val isHandled = attachmentsHelper.handleShareIntent(requireContext(), shareIntent) if (!isHandled) { roomDetailViewModel.preventAttachmentPreview = false 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) { when (type) { - AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera() - AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile() - AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery() - AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio() - AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact() + AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera(this) + AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile(this) + AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(this) + AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio(this) + AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact(this) AttachmentTypeSelectorView.Type.STICKER -> vectorBaseActivity.notImplemented("Adding stickers") }.exhaustive } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 2ad90f073a..cef172da73 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -610,7 +610,7 @@ class RoomDetailViewModel @AssistedInject constructor( when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) { null -> room.sendMedias(attachments, action.compressBeforeSending, emptySet()) else -> _viewEvents.post(RoomDetailViewEvents.FileTooBigError( - tooBigFile.name ?: tooBigFile.path, + tooBigFile.name ?: tooBigFile.queryUri.toString(), tooBigFile.size, maxUploadFileSize )) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt index aac48202c4..f7c9ff04e4 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt @@ -58,7 +58,7 @@ class ViewReactionsBottomSheet : VectorBaseBottomSheetDialogFragment(), ViewReac override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) 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) epoxyController.listener = this } diff --git a/vector/src/main/java/im/vector/riotx/features/lifecycle/VectorActivityLifecycleCallbacks.kt b/vector/src/main/java/im/vector/riotx/features/lifecycle/VectorActivityLifecycleCallbacks.kt index bf932a74be..668adc7edd 100644 --- a/vector/src/main/java/im/vector/riotx/features/lifecycle/VectorActivityLifecycleCallbacks.kt +++ b/vector/src/main/java/im/vector/riotx/features/lifecycle/VectorActivityLifecycleCallbacks.kt @@ -20,14 +20,13 @@ import android.app.Activity import android.app.Application import android.os.Bundle 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 onActivityResumed(activity: Activity) { - PopupAlertManager.onNewActivityDisplayed(activity) + popupAlertManager.onNewActivityDisplayed(activity) } override fun onActivityStarted(activity: Activity) { diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index a080cabf1b..2e91090ec4 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -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 pr = session.cryptoService().verificationService().requestKeyVerification( supportedVerificationMethodsProvider.provide(), session.myUserId, - session.cryptoService().getUserDevices(session.myUserId).map { it.deviceId } + listOf(otherSessionId) ) if (context is VectorBaseActivity) { VerificationBottomSheet.withArgs( diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt index fcb3d7bb44..65ef08dd05 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt @@ -30,7 +30,7 @@ interface Navigator { fun performDeviceVerification(context: Context, otherUserId: String, sasTransactionId: String) - fun requestSessionVerification(context: Context) + fun requestSessionVerification(context: Context, otherSessionId: String) fun waitSessionVerification(context: Context) diff --git a/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt b/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt index aa198eba02..1876d83617 100644 --- a/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt +++ b/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt @@ -20,20 +20,23 @@ import android.os.Build import android.os.Handler import android.os.Looper import android.view.View -import androidx.annotation.ColorInt -import androidx.annotation.ColorRes -import androidx.annotation.DrawableRes +import android.widget.ImageView import com.tapadoo.alerter.Alerter import com.tapadoo.alerter.OnHideAlertListener +import dagger.Lazy import im.vector.riotx.R +import im.vector.riotx.features.home.AvatarRenderer import timber.log.Timber import java.lang.ref.WeakReference +import javax.inject.Inject +import javax.inject.Singleton /** * Responsible of displaying important popup alerts on top of the screen. * 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 currentAlerter: VectorAlert? = null @@ -160,9 +163,19 @@ object PopupAlertManager { clearLightStatusBar() alert.weakCurrentActivity = WeakReference(activity) - Alerter.create(activity) - .setTitle(alert.title) + val alerter = if (alert is VerificationVectorAlert) Alerter.create(activity, R.layout.alerter_verification_layout) + else Alerter.create(activity) + + alerter.setTitle(alert.title) .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 { if (!animate) { setEnterAnimation(R.anim.anim_alerter_no_anim) @@ -226,37 +239,4 @@ object PopupAlertManager { displayNextIfPossible() }, 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 - } } diff --git a/vector/src/main/java/im/vector/riotx/features/popup/VectorAlert.kt b/vector/src/main/java/im/vector/riotx/features/popup/VectorAlert.kt new file mode 100644 index 0000000000..259df1c7e0 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/popup/VectorAlert.kt @@ -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 +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt index 909d40a74c..5db14fdbd2 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt @@ -57,6 +57,8 @@ class VectorSettingsActivity : VectorBaseActivity(), when (intent.getIntExtra(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOT)) { EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS -> 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 -> 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_ADVANCED_SETTINGS = 1 + const val EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY = 2 private const val FRAGMENT_TAG = "VectorSettingsPreferencesFragment" } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningEpoxyController.kt index cf74e83b1f..e33b12d19a 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningEpoxyController.kt @@ -78,6 +78,17 @@ class CrossSigningEpoxyController @Inject constructor( 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) { genericItem { diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoBottomSheet.kt index 9863134cb7..8e18472499 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoBottomSheet.kt @@ -62,7 +62,7 @@ class DeviceVerificationInfoBottomSheet : VectorBaseBottomSheetDialogFragment(), super.onActivityCreated(savedInstanceState) recyclerView.configureWith( epoxyController, - showDivider = true, + showDivider = false, hasFixedSize = false) epoxyController.callback = this bottomSheetTitle.isVisible = false diff --git a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareFragment.kt b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareFragment.kt index 74821ab2fe..aa665b5653 100644 --- a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareFragment.kt @@ -72,18 +72,18 @@ class IncomingShareFragment @Inject constructor( super.onViewCreated(view, savedInstanceState) setupRecyclerView() setupToolbar(incomingShareToolbar) - attachmentsHelper = AttachmentsHelper.create(this, this).register() + attachmentsHelper = AttachmentsHelper(requireContext(), this).register() val intent = vectorBaseActivity.intent val isShareManaged = when (intent?.action) { Intent.ACTION_SEND -> { - var isShareManaged = attachmentsHelper.handleShareIntent(intent) + var isShareManaged = attachmentsHelper.handleShareIntent(requireContext(), intent) if (!isShareManaged) { isShareManaged = handleTextShare(intent) } isShareManaged } - Intent.ACTION_SEND_MULTIPLE -> attachmentsHelper.handleShareIntent(intent) + Intent.ACTION_SEND_MULTIPLE -> attachmentsHelper.handleShareIntent(requireContext(), intent) else -> false } diff --git a/vector/src/main/java/im/vector/riotx/features/themes/ThemeUtils.kt b/vector/src/main/java/im/vector/riotx/features/themes/ThemeUtils.kt index 40a14b3e6f..1f835164db 100644 --- a/vector/src/main/java/im/vector/riotx/features/themes/ThemeUtils.kt +++ b/vector/src/main/java/im/vector/riotx/features/themes/ThemeUtils.kt @@ -62,10 +62,10 @@ object ThemeUtils { */ fun setApplicationTheme(context: Context, aTheme: String) { when (aTheme) { - THEME_DARK_VALUE -> context.setTheme(R.style.AppTheme_Dark) - THEME_BLACK_VALUE -> context.setTheme(R.style.AppTheme_Black) + THEME_DARK_VALUE -> context.setTheme(R.style.AppTheme_Dark) + THEME_BLACK_VALUE -> context.setTheme(R.style.AppTheme_Black) THEME_STATUS_VALUE -> context.setTheme(R.style.AppTheme_Status) - else -> context.setTheme(R.style.AppTheme_Light) + else -> context.setTheme(R.style.AppTheme_Light) } // Clear the cache @@ -170,6 +170,7 @@ object ThemeUtils { R.drawable.bg_search_edit_text_light -> R.drawable.bg_search_edit_text_dark R.drawable.bg_unread_notification_light -> R.drawable.bg_unread_notification_dark R.drawable.vector_label_background_light -> R.drawable.vector_label_background_dark + R.drawable.divider_horizontal_light -> R.drawable.divider_horizontal_dark else -> { Timber.w("Warning, missing case for wanted drawable in dark theme") resourceId @@ -181,6 +182,7 @@ object ThemeUtils { R.drawable.bg_search_edit_text_light -> R.drawable.bg_search_edit_text_black R.drawable.bg_unread_notification_light -> R.drawable.bg_unread_notification_black R.drawable.vector_label_background_light -> R.drawable.vector_label_background_black + R.drawable.divider_horizontal_light -> R.drawable.divider_horizontal_black else -> { Timber.w("Warning, missing case for wanted drawable in black theme") resourceId diff --git a/vector/src/main/res/drawable/divider_horizontal_black.xml b/vector/src/main/res/drawable/divider_horizontal_black.xml new file mode 100644 index 0000000000..43a68bbe2d --- /dev/null +++ b/vector/src/main/res/drawable/divider_horizontal_black.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <size android:height="1dp" /> + <solid android:color="@color/riotx_header_panel_border_mobile_black" /> +</shape> \ No newline at end of file diff --git a/vector/src/main/res/drawable/divider_horizontal_dark.xml b/vector/src/main/res/drawable/divider_horizontal_dark.xml new file mode 100644 index 0000000000..24a9307799 --- /dev/null +++ b/vector/src/main/res/drawable/divider_horizontal_dark.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <size android:height="1dp" /> + <solid android:color="@color/riotx_header_panel_border_mobile_dark" /> +</shape> \ No newline at end of file diff --git a/vector/src/main/res/drawable/divider_horizontal_light.xml b/vector/src/main/res/drawable/divider_horizontal_light.xml new file mode 100644 index 0000000000..4b215ecaa4 --- /dev/null +++ b/vector/src/main/res/drawable/divider_horizontal_light.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <size android:height="1dp" /> + <solid android:color="@color/riotx_header_panel_border_mobile_light" /> +</shape> \ No newline at end of file diff --git a/vector/src/main/res/layout/alerter_verification_layout.xml b/vector/src/main/res/layout/alerter_verification_layout.xml new file mode 100644 index 0000000000..b06883b056 --- /dev/null +++ b/vector/src/main/res/layout/alerter_verification_layout.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clipToPadding="false" + tools:background="@android:color/darker_gray" + tools:foreground="?android:attr/selectableItemBackground" + tools:style="@style/AlertStyle"> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/ivUserAvatar" + android:layout_width="40dp" + android:layout_height="40dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@+id/alerter_texts" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:src="@tools:sample/avatars" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/ivIcon" + android:layout_width="24dp" + android:layout_height="24dp" + app:layout_constraintCircle="@+id/ivUserAvatar" + app:layout_constraintCircleAngle="135" + app:layout_constraintCircleRadius="20dp" + tools:ignore="MissingConstraints" + android:src="@drawable/ic_shield_warning" + tools:visibility="visible" /> + + <LinearLayout + android:id="@+id/alerter_texts" + android:layout_width="0dp" + android:layout_height="0dp" + android:orientation="vertical" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/ivUserAvatar" + app:layout_constraintTop_toTopOf="parent"> + + <androidx.appcompat.widget.AppCompatTextView + android:id="@+id/tvTitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/alerter_padding_half" + android:layout_marginEnd="@dimen/alerter_padding_half" + android:paddingStart="@dimen/alerter_padding_small" + android:paddingLeft="@dimen/alerter_padding_small" + android:paddingEnd="@dimen/alerter_padding_small" + android:textAppearance="@style/AlertTextAppearance.Title" + android:visibility="gone" + tools:text="Title" + tools:visibility="visible" /> + + <androidx.appcompat.widget.AppCompatTextView + android:id="@+id/tvText" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/alerter_padding_half" + android:layout_marginEnd="@dimen/alerter_padding_half" + android:paddingStart="@dimen/alerter_padding_small" + android:paddingLeft="@dimen/alerter_padding_small" + android:paddingTop="@dimen/alerter_padding_small" + android:paddingEnd="@dimen/alerter_padding_small" + android:paddingBottom="@dimen/alerter_padding_small" + android:textAppearance="@style/AlertTextAppearance.Text" + android:visibility="gone" + tools:text="Text" + tools:visibility="visible" /> + + </LinearLayout> + + <!-- <FrameLayout--> + <!-- android:id="@+id/flRightIconContainer"--> + <!-- android:layout_width="wrap_content"--> + <!-- android:layout_height="wrap_content"--> + <!-- android:layout_gravity="center_vertical">--> + + <!-- <androidx.appcompat.widget.AppCompatImageView--> + <!-- android:id="@+id/ivRightIcon"--> + <!-- android:layout_width="@dimen/alerter_alert_icn_size"--> + <!-- android:layout_height="@dimen/alerter_alert_icn_size"--> + <!-- android:maxWidth="@dimen/alerter_alert_icn_size"--> + <!-- android:maxHeight="@dimen/alerter_alert_icn_size"--> + <!-- android:visibility="gone"--> + <!-- app:srcCompat="@drawable/alerter_ic_notifications"--> + <!-- app:tint="@color/alert_default_icon_color"--> + <!-- tools:visibility="visible" />--> + <!-- </FrameLayout>--> +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/vector/src/main/res/layout/bottom_sheet_generic_list_with_title.xml b/vector/src/main/res/layout/bottom_sheet_generic_list_with_title.xml index 80d877ac2d..6a4b6d3eaa 100644 --- a/vector/src/main/res/layout/bottom_sheet_generic_list_with_title.xml +++ b/vector/src/main/res/layout/bottom_sheet_generic_list_with_title.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" + android:paddingTop="8dp" android:orientation="vertical"> <TextView diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 76bcfe7290..9e07f43cb8 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -287,7 +287,7 @@ <string name="login_error_no_homeserver_found">This is not a valid Matrix server address</string> <string name="login_error_homeserver_not_found">Cannot reach a homeserver at this URL, please check it</string> <string name="login_error_ssl_handshake">Your device is using an outdated TLS security protocol, vulnerable to attack, for your security you will not be able to connect</string> - <string name="login_mobile_device">Mobile</string> + <string name="login_mobile_device">RiotX Android</string> <string name="login_error_forbidden">Invalid username/password</string> <string name="login_error_unknown_token">The access token specified was not recognised</string> diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 45fc3a3781..de28f316fa 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -14,6 +14,23 @@ <string name="refresh">Refresh</string> + + <string name="new_session">New Session</string> + <string name="new_session_review">Tap to review & verify</string> + <string name="verify_new_session_notice">Use this session to verify your new one, granting it access to encrypted messages.</string> + <string name="verify_new_session_was_not_me">This wasn’t me</string> + <string name="verify_new_session_compromized">Your account may be compromised</string> + + <string name="verify_cancel_self_verification_from_untrusted">If you cancel, you won’t be able to read encrypted messages on this device, and other users won’t trust it</string> + <string name="verify_cancel_self_verification_from_trusted">If you cancel, you won’t be able to read encrypted messages on your new device, and other users won’t trust it</string> + + <string name="verify_not_me_self_verification"> + One of the following may be compromised:\n\n- Your password\n- Your homeserver\n- This device, or the other device\n- The internet connection either device is using\n\nWe recommend you change your password & recovery key in Settings immediately. + </string> + + <string name="verify_cancelled_notice">Verify your devices from Settings.</string> + <string name="verification_cancelled">Verification Cancelled</string> + <!-- END Strings added by Valere --> diff --git a/vector/src/main/res/values/theme_dark.xml b/vector/src/main/res/values/theme_dark.xml index 9bfdcc8231..09775d4d41 100644 --- a/vector/src/main/res/values/theme_dark.xml +++ b/vector/src/main/res/values/theme_dark.xml @@ -36,8 +36,6 @@ <!-- Drawables --> <item name="riotx_highlighted_message_background">@drawable/highlighted_message_background_dark</item> - <item name="android:listDivider">@color/riotx_header_panel_border_mobile_dark</item> - <!-- Material color: Note: this block should be the same in all theme because it references only common colors and ?riotx attributes --> <item name="colorPrimary">@color/riotx_accent</item> <item name="colorPrimaryVariant">@color/primary_color_dark_light</item> diff --git a/vector/src/main/res/values/theme_light.xml b/vector/src/main/res/values/theme_light.xml index 3a479c9368..c63dfa9057 100644 --- a/vector/src/main/res/values/theme_light.xml +++ b/vector/src/main/res/values/theme_light.xml @@ -35,8 +35,6 @@ <!-- Drawables --> <item name="riotx_highlighted_message_background">@drawable/highlighted_message_background_light</item> - <item name="android:listDivider">@color/riotx_header_panel_border_mobile_light</item> - <!-- Material color: Note: this block should be the same in all theme because it references only common colors and ?riotx attributes --> <item name="colorPrimary">@color/riotx_accent</item> <!--item name="colorPrimaryVariant">@color/primary_color_dark_light</item-->