mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-02-16 20:10:04 +03:00
Merge pull request #1162 from vector-im/feature/xs_detect_new_session
Feature/xs detect new session
This commit is contained in:
commit
535cdf0ef5
65 changed files with 1479 additions and 318 deletions
|
@ -3,6 +3,7 @@ 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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -25,7 +25,9 @@ enum class GossipingRequestState {
|
|||
NONE,
|
||||
PENDING,
|
||||
REJECTED,
|
||||
ACCEPTING,
|
||||
ACCEPTED,
|
||||
FAILED_TO_ACCEPTED,
|
||||
// USER_REJECTED,
|
||||
UNABLE_TO_PROCESS,
|
||||
CANCELLED_BY_REQUESTER,
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.Data
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.ListenableWorker
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.internal.di.WorkManagerProvider
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.util.CancelableWork
|
||||
import im.vector.matrix.android.internal.worker.startChain
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
@SessionScope
|
||||
internal class GossipingWorkManager @Inject constructor(
|
||||
private val workManagerProvider: WorkManagerProvider
|
||||
) {
|
||||
|
||||
inline fun <reified W : ListenableWorker> createWork(data: Data, startChain: Boolean): OneTimeWorkRequest {
|
||||
return workManagerProvider.matrixOneTimeWorkRequestBuilder<W>()
|
||||
.setConstraints(WorkManagerProvider.workConstraints)
|
||||
.startChain(startChain)
|
||||
.setInputData(data)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, 10_000L, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
}
|
||||
|
||||
// Prevent sending queue to stay broken after app restart
|
||||
// The unique queue id will stay the same as long as this object is instanciated
|
||||
val queueSuffixApp = System.currentTimeMillis()
|
||||
|
||||
fun postWork(workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND): Cancelable {
|
||||
workManagerProvider.workManager
|
||||
.beginUniqueWork(this::class.java.name + "_$queueSuffixApp", policy, workRequest)
|
||||
.enqueue()
|
||||
|
||||
return CancelableWork(workManagerProvider.workManager, workRequest.id)
|
||||
}
|
||||
}
|
|
@ -17,6 +17,7 @@
|
|||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.Data
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.failure.shouldBeRetried
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||
import im.vector.matrix.android.api.session.events.model.toContent
|
||||
import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
||||
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.model.event.SecretSendEventContent
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||
import im.vector.matrix.android.internal.worker.getSessionComponent
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class SendGossipWorker(context: Context,
|
||||
params: WorkerParameters)
|
||||
: CoroutineWorker(context, params) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class Params(
|
||||
val sessionId: String,
|
||||
val secretValue: String,
|
||||
val request: IncomingSecretShareRequest
|
||||
)
|
||||
|
||||
@Inject lateinit var sendToDeviceTask: SendToDeviceTask
|
||||
@Inject lateinit var cryptoStore: IMXCryptoStore
|
||||
@Inject lateinit var eventBus: EventBus
|
||||
@Inject lateinit var credentials: Credentials
|
||||
@Inject lateinit var messageEncrypter: MessageEncrypter
|
||||
@Inject lateinit var ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val errorOutputData = Data.Builder().putBoolean("failed", true).build()
|
||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||
?: return Result.success(errorOutputData)
|
||||
|
||||
val sessionComponent = getSessionComponent(params.sessionId)
|
||||
?: return Result.success(errorOutputData).also {
|
||||
// TODO, can this happen? should I update local echo?
|
||||
Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}")
|
||||
}
|
||||
sessionComponent.inject(this)
|
||||
|
||||
val localId = LocalEcho.createLocalEchoId()
|
||||
val eventType: String = EventType.SEND_SECRET
|
||||
|
||||
val toDeviceContent = SecretSendEventContent(
|
||||
requestId = params.request.requestId ?: "",
|
||||
secretValue = params.secretValue
|
||||
)
|
||||
|
||||
val requestingUserId = params.request.userId ?: ""
|
||||
val requestingDeviceId = params.request.deviceId ?: ""
|
||||
val deviceInfo = cryptoStore.getUserDevice(requestingUserId, requestingDeviceId)
|
||||
?: return Result.success(errorOutputData).also {
|
||||
cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED)
|
||||
Timber.e("Unknown deviceInfo, cannot send message, sessionId: ${params.request.deviceId}")
|
||||
}
|
||||
|
||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
||||
|
||||
val devicesByUser = mapOf(requestingUserId to listOf(deviceInfo))
|
||||
val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
||||
val olmSessionResult = usersDeviceMap.getObject(requestingUserId, requestingDeviceId)
|
||||
if (olmSessionResult?.sessionId == null) {
|
||||
// no session with this device, probably because there
|
||||
// were no one-time keys.
|
||||
return Result.success(errorOutputData).also {
|
||||
cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED)
|
||||
Timber.e("no session with this device, probably because there were no one-time keys.")
|
||||
}
|
||||
}
|
||||
|
||||
val payloadJson = mapOf(
|
||||
"type" to EventType.SEND_SECRET,
|
||||
"content" to toDeviceContent.toContent()
|
||||
)
|
||||
|
||||
try {
|
||||
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
||||
sendToDeviceMap.setObject(requestingUserId, requestingDeviceId, encodedPayload)
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e("## Fail to encrypt gossip + ${failure.localizedMessage}")
|
||||
}
|
||||
|
||||
cryptoStore.saveGossipingEvent(Event(
|
||||
type = eventType,
|
||||
content = toDeviceContent.toContent(),
|
||||
senderId = credentials.userId
|
||||
).also {
|
||||
it.ageLocalTs = System.currentTimeMillis()
|
||||
})
|
||||
|
||||
try {
|
||||
sendToDeviceTask.execute(
|
||||
SendToDeviceTask.Params(
|
||||
eventType = EventType.ENCRYPTED,
|
||||
contentMap = sendToDeviceMap,
|
||||
transactionId = localId
|
||||
)
|
||||
)
|
||||
cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.ACCEPTED)
|
||||
return Result.success()
|
||||
} catch (exception: Throwable) {
|
||||
return if (exception.shouldBeRetried()) {
|
||||
Result.retry()
|
||||
} else {
|
||||
cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED)
|
||||
Result.success(errorOutputData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmDecryptionFactory
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.model.event.SecretSendEventContent
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class ShareSecretCryptoProvider @Inject constructor(
|
||||
val messageEncrypter: MessageEncrypter,
|
||||
val sendToDeviceTask: SendToDeviceTask,
|
||||
val deviceListManager: DeviceListManager,
|
||||
private val olmDecryptionFactory: MXOlmDecryptionFactory,
|
||||
val cryptoCoroutineScope: CoroutineScope,
|
||||
val cryptoStore: IMXCryptoStore,
|
||||
val coroutineDispatchers: MatrixCoroutineDispatchers
|
||||
) {
|
||||
fun shareSecretWithDevice(request: IncomingSecretShareRequest, secretValue: String) {
|
||||
val userId = request.userId ?: return
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
runCatching { deviceListManager.downloadKeys(listOf(userId), false) }
|
||||
.mapCatching {
|
||||
val deviceId = request.deviceId
|
||||
val deviceInfo = cryptoStore.getUserDevice(userId, deviceId ?: "") ?: throw RuntimeException()
|
||||
|
||||
Timber.i("## shareSecretWithDevice() : sharing secret ${request.secretName} with device $userId:$deviceId")
|
||||
|
||||
val payloadJson = mutableMapOf<String, Any>("type" to EventType.SEND_SECRET)
|
||||
payloadJson["content"] = SecretSendEventContent(
|
||||
requestId = request.requestId ?: "",
|
||||
secretValue = secretValue
|
||||
)
|
||||
|
||||
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
||||
sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
|
||||
Timber.i("## shareSecretWithDevice() : sending to $userId:$deviceId")
|
||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
||||
sendToDeviceTask.execute(sendToDeviceParams)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun decryptEvent(event: Event): MXEventDecryptionResult {
|
||||
return runBlocking(coroutineDispatchers.crypto) {
|
||||
olmDecryptionFactory.create().decryptEvent(event, ShareSecretCryptoProvider::class.java.name ?: "")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -149,7 +149,7 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
|
||||
val senderDevice = encryptedEventContent?.deviceId ?: return
|
||||
|
||||
val recipients = if (event.senderId != userId) {
|
||||
val recipients = if (event.senderId == userId) {
|
||||
mapOf(
|
||||
userId to listOf("*")
|
||||
)
|
||||
|
|
|
@ -25,7 +25,6 @@ import im.vector.matrix.android.api.util.Optional
|
|||
import im.vector.matrix.android.internal.crypto.DeviceListManager
|
||||
import im.vector.matrix.android.internal.crypto.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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 ?: "*")))
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -323,7 +323,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'
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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? {
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.crypto.verification.cancel
|
||||
|
||||
import com.airbnb.epoxy.EpoxyController
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.epoxy.dividerItem
|
||||
import im.vector.riotx.core.resources.ColorProvider
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
import im.vector.riotx.features.crypto.verification.VerificationBottomSheetViewState
|
||||
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
|
||||
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem
|
||||
import javax.inject.Inject
|
||||
|
||||
class VerificationCancelController @Inject constructor(
|
||||
private val stringProvider: StringProvider,
|
||||
private val colorProvider: ColorProvider
|
||||
) : EpoxyController() {
|
||||
|
||||
var listener: Listener? = null
|
||||
|
||||
private var viewState: VerificationBottomSheetViewState? = null
|
||||
|
||||
fun update(viewState: VerificationBottomSheetViewState) {
|
||||
this.viewState = viewState
|
||||
requestModelBuild()
|
||||
}
|
||||
|
||||
override fun buildModels() {
|
||||
val state = viewState ?: return
|
||||
|
||||
if (state.isMe) {
|
||||
if (state.currentDeviceCanCrossSign) {
|
||||
bottomSheetVerificationNoticeItem {
|
||||
id("notice")
|
||||
notice(stringProvider.getString(R.string.verify_cancel_self_verification_from_trusted))
|
||||
}
|
||||
} else {
|
||||
bottomSheetVerificationNoticeItem {
|
||||
id("notice")
|
||||
notice(stringProvider.getString(R.string.verify_cancel_self_verification_from_untrusted))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
bottomSheetVerificationNoticeItem {
|
||||
id("notice")
|
||||
notice(stringProvider.getString(R.string.verify_cancel_self_verification_from_untrusted))
|
||||
}
|
||||
}
|
||||
|
||||
dividerItem {
|
||||
id("sep0")
|
||||
}
|
||||
|
||||
bottomSheetVerificationActionItem {
|
||||
id("cancel")
|
||||
title(stringProvider.getString(R.string.cancel))
|
||||
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
|
||||
iconRes(R.drawable.ic_arrow_right)
|
||||
iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
|
||||
listener { listener?.onTapCancel() }
|
||||
}
|
||||
|
||||
dividerItem {
|
||||
id("sep1")
|
||||
}
|
||||
|
||||
bottomSheetVerificationActionItem {
|
||||
id("continue")
|
||||
title(stringProvider.getString(R.string._continue))
|
||||
titleColor(colorProvider.getColor(R.color.riotx_positive_accent))
|
||||
iconRes(R.drawable.ic_arrow_right)
|
||||
iconColor(colorProvider.getColor(R.color.riotx_positive_accent))
|
||||
listener { listener?.onTapContinue() }
|
||||
}
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun onTapCancel()
|
||||
fun onTapContinue()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.crypto.verification.cancel
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import com.airbnb.mvrx.parentFragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.extensions.cleanup
|
||||
import im.vector.riotx.core.extensions.configureWith
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import im.vector.riotx.features.crypto.verification.VerificationBottomSheetViewModel
|
||||
import kotlinx.android.synthetic.main.bottom_sheet_verification_child_fragment.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class VerificationCancelFragment @Inject constructor(
|
||||
val controller: VerificationCancelController
|
||||
) : VectorBaseFragment(), VerificationCancelController.Listener {
|
||||
|
||||
private val viewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class)
|
||||
|
||||
override fun getLayoutResId() = R.layout.bottom_sheet_verification_child_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setupRecyclerView()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
bottomSheetVerificationRecyclerView.cleanup()
|
||||
controller.listener = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
bottomSheetVerificationRecyclerView.configureWith(controller, hasFixedSize = false, disableItemAnimation = true)
|
||||
controller.listener = this
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(viewModel) { state ->
|
||||
controller.update(state)
|
||||
}
|
||||
|
||||
override fun onTapCancel() {
|
||||
viewModel.confirmCancel()
|
||||
}
|
||||
|
||||
override fun onTapContinue() {
|
||||
viewModel.continueFromCancel()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.crypto.verification.cancel
|
||||
|
||||
import com.airbnb.epoxy.EpoxyController
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.epoxy.dividerItem
|
||||
import im.vector.riotx.core.resources.ColorProvider
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
import im.vector.riotx.features.crypto.verification.VerificationBottomSheetViewState
|
||||
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
|
||||
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem
|
||||
import im.vector.riotx.features.html.EventHtmlRenderer
|
||||
import javax.inject.Inject
|
||||
|
||||
class VerificationNotMeController @Inject constructor(
|
||||
private val stringProvider: StringProvider,
|
||||
private val colorProvider: ColorProvider,
|
||||
private val eventHtmlRenderer: EventHtmlRenderer
|
||||
) : EpoxyController() {
|
||||
|
||||
var listener: Listener? = null
|
||||
|
||||
private var viewState: VerificationBottomSheetViewState? = null
|
||||
|
||||
fun update(viewState: VerificationBottomSheetViewState) {
|
||||
this.viewState = viewState
|
||||
requestModelBuild()
|
||||
}
|
||||
|
||||
override fun buildModels() {
|
||||
bottomSheetVerificationNoticeItem {
|
||||
id("notice")
|
||||
notice(eventHtmlRenderer.render(stringProvider.getString(R.string.verify_not_me_self_verification)))
|
||||
}
|
||||
|
||||
dividerItem {
|
||||
id("sep0")
|
||||
}
|
||||
|
||||
bottomSheetVerificationActionItem {
|
||||
id("skip")
|
||||
title(stringProvider.getString(R.string.skip))
|
||||
titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
|
||||
iconRes(R.drawable.ic_arrow_right)
|
||||
iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
|
||||
listener { listener?.onTapSkip() }
|
||||
}
|
||||
|
||||
dividerItem {
|
||||
id("sep1")
|
||||
}
|
||||
|
||||
bottomSheetVerificationActionItem {
|
||||
id("settings")
|
||||
title(stringProvider.getString(R.string.settings))
|
||||
titleColor(colorProvider.getColor(R.color.riotx_positive_accent))
|
||||
iconRes(R.drawable.ic_arrow_right)
|
||||
iconColor(colorProvider.getColor(R.color.riotx_positive_accent))
|
||||
listener { listener?.onTapSettings() }
|
||||
}
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun onTapSkip()
|
||||
fun onTapSettings()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.crypto.verification.cancel
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import com.airbnb.mvrx.parentFragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.extensions.cleanup
|
||||
import im.vector.riotx.core.extensions.configureWith
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import im.vector.riotx.features.crypto.verification.VerificationBottomSheetViewModel
|
||||
import kotlinx.android.synthetic.main.bottom_sheet_verification_child_fragment.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class VerificationNotMeFragment @Inject constructor(
|
||||
val controller: VerificationNotMeController
|
||||
) : VectorBaseFragment(), VerificationNotMeController.Listener {
|
||||
|
||||
private val viewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class)
|
||||
|
||||
override fun getLayoutResId() = R.layout.bottom_sheet_verification_child_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setupRecyclerView()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
bottomSheetVerificationRecyclerView.cleanup()
|
||||
controller.listener = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
bottomSheetVerificationRecyclerView.configureWith(controller, hasFixedSize = false, disableItemAnimation = true)
|
||||
controller.listener = this
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(viewModel) { state ->
|
||||
controller.update(state)
|
||||
}
|
||||
|
||||
override fun onTapSkip() {
|
||||
viewModel.continueFromWasNotMe()
|
||||
}
|
||||
|
||||
override fun onTapSettings() {
|
||||
viewModel.goToSettings()
|
||||
}
|
||||
}
|
|
@ -95,10 +95,27 @@ class VerificationChooseMethodController @Inject constructor(
|
|||
listener { listener?.doVerifyBySas() }
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -89,6 +89,10 @@ class VerificationChooseMethodFragment @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun onClickOnWasNotMe() {
|
||||
sharedViewModel.itWasNotMe()
|
||||
}
|
||||
|
||||
private fun doOpenQRCodeScanner() {
|
||||
QrCodeScannerActivity.startForResult(this)
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -69,4 +69,8 @@ class VerificationRequestFragment @Inject constructor(
|
|||
override fun onClickDismiss() {
|
||||
viewModel.handle(VerificationAction.SkipVerification)
|
||||
}
|
||||
|
||||
override fun onClickOnWasNotMe() {
|
||||
viewModel.itWasNotMe()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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?) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.popup
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import im.vector.matrix.android.api.util.MatrixItem
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
interface VectorAlert {
|
||||
val uid: String
|
||||
val title: String
|
||||
val description: String
|
||||
val iconId: Int?
|
||||
val shouldBeDisplayedIn: ((Activity) -> Boolean)?
|
||||
|
||||
data class Button(val title: String, val action: Runnable, val autoClose: Boolean)
|
||||
|
||||
// will be set by manager, and accessible by actions at runtime
|
||||
var weakCurrentActivity: WeakReference<Activity>?
|
||||
|
||||
val actions: MutableList<Button>
|
||||
|
||||
var contentAction: Runnable?
|
||||
var dismissedAction: Runnable?
|
||||
|
||||
/** If this timestamp is after current time, this alert will be skipped */
|
||||
var expirationTimestamp: Long?
|
||||
|
||||
fun addButton(title: String, action: Runnable, autoClose: Boolean = true) {
|
||||
actions.add(Button(title, action, autoClose))
|
||||
}
|
||||
|
||||
var colorRes: Int?
|
||||
|
||||
var colorInt: Int?
|
||||
}
|
||||
|
||||
/**
|
||||
* Dataclass to describe an important alert with actions.
|
||||
*/
|
||||
open class DefaultVectorAlert(override val uid: String,
|
||||
override val title: String,
|
||||
override val description: String,
|
||||
@DrawableRes override val iconId: Int?,
|
||||
override val shouldBeDisplayedIn: ((Activity) -> Boolean)? = null) : VectorAlert {
|
||||
|
||||
// will be set by manager, and accessible by actions at runtime
|
||||
override var weakCurrentActivity: WeakReference<Activity>? = null
|
||||
|
||||
override val actions = ArrayList<VectorAlert.Button>()
|
||||
|
||||
override var contentAction: Runnable? = null
|
||||
override var dismissedAction: Runnable? = null
|
||||
|
||||
/** If this timestamp is after current time, this alert will be skipped */
|
||||
override var expirationTimestamp: Long? = null
|
||||
|
||||
override fun addButton(title: String, action: Runnable, autoClose: Boolean) {
|
||||
actions.add(VectorAlert.Button(title, action, autoClose))
|
||||
}
|
||||
|
||||
@ColorRes
|
||||
override var colorRes: Int? = null
|
||||
|
||||
@ColorInt
|
||||
override var colorInt: Int? = null
|
||||
}
|
||||
|
||||
class VerificationVectorAlert(uid: String,
|
||||
title: String,
|
||||
override val description: String,
|
||||
@DrawableRes override val iconId: Int?,
|
||||
override val shouldBeDisplayedIn: ((Activity) -> Boolean)? = null
|
||||
) : DefaultVectorAlert(
|
||||
uid, title, description, iconId, shouldBeDisplayedIn
|
||||
) {
|
||||
var matrixItem: MatrixItem? = null
|
||||
}
|
|
@ -57,6 +57,8 @@ class VectorSettingsActivity : VectorBaseActivity(),
|
|||
when (intent.getIntExtra(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOT)) {
|
||||
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"
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
5
vector/src/main/res/drawable/divider_horizontal_dark.xml
Normal file
5
vector/src/main/res/drawable/divider_horizontal_dark.xml
Normal file
|
@ -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>
|
|
@ -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>
|
92
vector/src/main/res/layout/alerter_verification_layout.xml
Normal file
92
vector/src/main/res/layout/alerter_verification_layout.xml
Normal file
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 -->
|
||||
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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-->
|
||||
|
|
Loading…
Add table
Reference in a new issue