Merge pull request #4325 from vector-im/feature/bca/auto_uisi

Auto report unable to decrypt errors via lab option
This commit is contained in:
Valere 2022-01-10 14:55:30 +01:00 committed by GitHub
commit 30aae3f07a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 860 additions and 24 deletions

View file

@ -36,6 +36,7 @@
<w>ssss</w>
<w>sygnal</w>
<w>threepid</w>
<w>uisi</w>
<w>unpublish</w>
<w>unwedging</w>
<w>vctr</w>

View file

@ -0,0 +1,24 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.session
interface EventStreamService {
fun addEventStreamListener(streamListener: LiveEventListener)
fun removeEventStreamListener(streamListener: LiveEventListener)
}

View file

@ -0,0 +1,35 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.session
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.util.JsonDict
interface LiveEventListener {
fun onLiveEvent(roomId: String, event: Event)
fun onPaginatedEvent(roomId: String, event: Event)
fun onEventDecrypted(eventId: String, roomId: String, clearEvent: JsonDict)
fun onEventDecryptionError(eventId: String, roomId: String, throwable: Throwable)
fun onLiveToDeviceEvent(event: Event)
// Maybe later add more, like onJoin, onLeave..
}

View file

@ -84,7 +84,9 @@ interface Session :
SyncStatusService,
HomeServerCapabilitiesService,
SecureStorageService,
AccountService {
AccountService,
ToDeviceService,
EventStreamService {
val coroutineDispatchers: MatrixCoroutineDispatchers

View file

@ -0,0 +1,37 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.session
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
import java.util.UUID
interface ToDeviceService {
/**
* Send an event to a specific list of devices
*/
suspend fun sendToDevice(eventType: String, contentMap: MXUsersDevicesMap<Any>, txnId: String? = UUID.randomUUID().toString())
suspend fun sendToDevice(eventType: String, userId: String, deviceId: String, content: Content, txnId: String? = UUID.randomUUID().toString()) {
sendToDevice(eventType, mapOf(userId to listOf(deviceId)), content, txnId)
}
suspend fun sendToDevice(eventType: String, targets: Map<String, List<String>>, content: Content, txnId: String? = UUID.randomUUID().toString())
suspend fun sendEncryptedToDevice(eventType: String, targets: Map<String, List<String>>, content: Content, txnId: String? = UUID.randomUUID().toString())
}

View file

@ -90,6 +90,7 @@ import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.extensions.foldToCallback
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.StreamEventsManager
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.TaskThread
@ -168,7 +169,8 @@ internal class DefaultCryptoService @Inject constructor(
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val taskExecutor: TaskExecutor,
private val cryptoCoroutineScope: CoroutineScope,
private val eventDecryptor: EventDecryptor
private val eventDecryptor: EventDecryptor,
private val liveEventManager: Lazy<StreamEventsManager>
) : CryptoService {
private val isStarting = AtomicBoolean(false)
@ -782,6 +784,7 @@ internal class DefaultCryptoService @Inject constructor(
}
}
}
liveEventManager.get().dispatchOnLiveToDevice(event)
}
/**

View file

@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
import dagger.Lazy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
@ -43,6 +44,7 @@ import org.matrix.android.sdk.internal.crypto.model.rest.ForwardedRoomKeyContent
import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.session.StreamEventsManager
import timber.log.Timber
private val loggerTag = LoggerTag("MXMegolmDecryption", LoggerTag.CRYPTO)
@ -56,7 +58,8 @@ internal class MXMegolmDecryption(private val userId: String,
private val cryptoStore: IMXCryptoStore,
private val sendToDeviceTask: SendToDeviceTask,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope
private val cryptoCoroutineScope: CoroutineScope,
private val liveEventManager: Lazy<StreamEventsManager>
) : IMXDecrypting, IMXWithHeldExtension {
var newSessionListener: NewSessionListener? = null
@ -108,12 +111,15 @@ internal class MXMegolmDecryption(private val userId: String,
claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"),
forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain
.orEmpty()
)
).also {
liveEventManager.get().dispatchLiveEventDecrypted(event, it)
}
} else {
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
}
},
{ throwable ->
liveEventManager.get().dispatchLiveEventDecryptionFailed(event, throwable)
if (throwable is MXCryptoError.OlmError) {
// TODO Check the value of .message
if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") {

View file

@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
import dagger.Lazy
import kotlinx.coroutines.CoroutineScope
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.internal.crypto.DeviceListManager
@ -26,6 +27,7 @@ import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.StreamEventsManager
import javax.inject.Inject
internal class MXMegolmDecryptionFactory @Inject constructor(
@ -38,7 +40,8 @@ internal class MXMegolmDecryptionFactory @Inject constructor(
private val cryptoStore: IMXCryptoStore,
private val sendToDeviceTask: SendToDeviceTask,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope
private val cryptoCoroutineScope: CoroutineScope,
private val eventsManager: Lazy<StreamEventsManager>
) {
fun create(): MXMegolmDecryption {
@ -52,6 +55,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor(
cryptoStore,
sendToDeviceTask,
coroutineDispatchers,
cryptoCoroutineScope)
cryptoCoroutineScope,
eventsManager)
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session
import org.matrix.android.sdk.api.session.EventStreamService
import org.matrix.android.sdk.api.session.LiveEventListener
import javax.inject.Inject
internal class DefaultEventStreamService @Inject constructor(
private val streamEventsManager: StreamEventsManager
) : EventStreamService {
override fun addEventStreamListener(streamListener: LiveEventListener) {
streamEventsManager.addLiveEventListener(streamListener)
}
override fun removeEventStreamListener(streamListener: LiveEventListener) {
streamEventsManager.removeLiveEventListener(streamListener)
}
}

View file

@ -27,8 +27,10 @@ import org.matrix.android.sdk.api.auth.data.SessionParams
import org.matrix.android.sdk.api.failure.GlobalError
import org.matrix.android.sdk.api.federation.FederationService
import org.matrix.android.sdk.api.pushrules.PushRuleService
import org.matrix.android.sdk.api.session.EventStreamService
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.SessionLifecycleObserver
import org.matrix.android.sdk.api.session.ToDeviceService
import org.matrix.android.sdk.api.session.account.AccountService
import org.matrix.android.sdk.api.session.accountdata.SessionAccountDataService
import org.matrix.android.sdk.api.session.cache.CacheService
@ -133,6 +135,8 @@ internal class DefaultSession @Inject constructor(
private val spaceService: Lazy<SpaceService>,
private val openIdService: Lazy<OpenIdService>,
private val presenceService: Lazy<PresenceService>,
private val toDeviceService: Lazy<ToDeviceService>,
private val eventStreamService: Lazy<EventStreamService>,
@UnauthenticatedWithCertificate
private val unauthenticatedWithCertificateOkHttpClient: Lazy<OkHttpClient>
) : Session,
@ -152,7 +156,9 @@ internal class DefaultSession @Inject constructor(
HomeServerCapabilitiesService by homeServerCapabilitiesService.get(),
ProfileService by profileService.get(),
PresenceService by presenceService.get(),
AccountService by accountService.get() {
AccountService by accountService.get(),
ToDeviceService by toDeviceService.get(),
EventStreamService by eventStreamService.get() {
override val sharedSecretStorageService: SharedSecretStorageService
get() = _sharedSecretStorageService.get()

View file

@ -0,0 +1,73 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session
import org.matrix.android.sdk.api.session.ToDeviceService
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import javax.inject.Inject
internal class DefaultToDeviceService @Inject constructor(
private val sendToDeviceTask: SendToDeviceTask,
private val messageEncrypter: MessageEncrypter,
private val cryptoStore: IMXCryptoStore
) : ToDeviceService {
override suspend fun sendToDevice(eventType: String, targets: Map<String, List<String>>, content: Content, txnId: String?) {
val sendToDeviceMap = MXUsersDevicesMap<Any>()
targets.forEach { (userId, deviceIdList) ->
deviceIdList.forEach { deviceId ->
sendToDeviceMap.setObject(userId, deviceId, content)
}
}
sendToDevice(eventType, sendToDeviceMap, txnId)
}
override suspend fun sendToDevice(eventType: String, contentMap: MXUsersDevicesMap<Any>, txnId: String?) {
sendToDeviceTask.executeRetry(
SendToDeviceTask.Params(
eventType = eventType,
contentMap = contentMap,
transactionId = txnId
),
3
)
}
override suspend fun sendEncryptedToDevice(eventType: String, targets: Map<String, List<String>>, content: Content, txnId: String?) {
val payloadJson = mapOf(
"type" to eventType,
"content" to content
)
val sendToDeviceMap = MXUsersDevicesMap<Any>()
// Should I do an ensure olm session?
targets.forEach { (userId, deviceIdList) ->
deviceIdList.forEach { deviceId ->
cryptoStore.getUserDevice(userId, deviceId)?.let { deviceInfo ->
sendToDeviceMap.setObject(userId, deviceId, messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)))
}
}
}
sendToDevice(EventType.ENCRYPTED, sendToDeviceMap, txnId)
}
}

View file

@ -32,8 +32,10 @@ import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.data.SessionParams
import org.matrix.android.sdk.api.auth.data.sessionId
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
import org.matrix.android.sdk.api.session.EventStreamService
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.SessionLifecycleObserver
import org.matrix.android.sdk.api.session.ToDeviceService
import org.matrix.android.sdk.api.session.accountdata.SessionAccountDataService
import org.matrix.android.sdk.api.session.events.EventService
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
@ -374,6 +376,12 @@ internal abstract class SessionModule {
@Binds
abstract fun bindOpenIdTokenService(service: DefaultOpenIdService): OpenIdService
@Binds
abstract fun bindToDeviceService(service: DefaultToDeviceService): ToDeviceService
@Binds
abstract fun bindEventStreamService(service: DefaultEventStreamService): EventStreamService
@Binds
abstract fun bindTypingUsersTracker(tracker: DefaultTypingUsersTracker): TypingUsersTracker

View file

@ -0,0 +1,101 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.LiveEventListener
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
import timber.log.Timber
import javax.inject.Inject
@SessionScope
internal class StreamEventsManager @Inject constructor() {
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val listeners = mutableListOf<LiveEventListener>()
fun addLiveEventListener(listener: LiveEventListener) {
listeners.add(listener)
}
fun removeLiveEventListener(listener: LiveEventListener) {
listeners.remove(listener)
}
fun dispatchLiveEventReceived(event: Event, roomId: String, initialSync: Boolean) {
Timber.v("## dispatchLiveEventReceived ${event.eventId}")
coroutineScope.launch {
if (!initialSync) {
listeners.forEach {
tryOrNull {
it.onLiveEvent(roomId, event)
}
}
}
}
}
fun dispatchPaginatedEventReceived(event: Event, roomId: String) {
Timber.v("## dispatchPaginatedEventReceived ${event.eventId}")
coroutineScope.launch {
listeners.forEach {
tryOrNull {
it.onPaginatedEvent(roomId, event)
}
}
}
}
fun dispatchLiveEventDecrypted(event: Event, result: MXEventDecryptionResult) {
Timber.v("## dispatchLiveEventDecrypted ${event.eventId}")
coroutineScope.launch {
listeners.forEach {
tryOrNull {
it.onEventDecrypted(event.eventId ?: "", event.roomId ?: "", result.clearEvent)
}
}
}
}
fun dispatchLiveEventDecryptionFailed(event: Event, error: Throwable) {
Timber.v("## dispatchLiveEventDecryptionFailed ${event.eventId}")
coroutineScope.launch {
listeners.forEach {
tryOrNull {
it.onEventDecryptionError(event.eventId ?: "", event.roomId ?: "", error)
}
}
}
}
fun dispatchOnLiveToDevice(event: Event) {
Timber.v("## dispatchOnLiveToDevice ${event.eventId}")
coroutineScope.launch {
listeners.forEach {
tryOrNull {
it.onLiveToDeviceEvent(event)
}
}
}
}
}

View file

@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.session.room.timeline
import com.zhuinden.monarchy.Monarchy
import dagger.Lazy
import io.realm.Realm
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
@ -35,6 +36,7 @@ import org.matrix.android.sdk.internal.database.query.create
import org.matrix.android.sdk.internal.database.query.find
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.StreamEventsManager
import org.matrix.android.sdk.internal.util.awaitTransaction
import timber.log.Timber
import javax.inject.Inject
@ -42,7 +44,9 @@ import javax.inject.Inject
/**
* Insert Chunk in DB, and eventually link next and previous chunk in db.
*/
internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase private val monarchy: Monarchy) {
internal class TokenChunkEventPersistor @Inject constructor(
@SessionDatabase private val monarchy: Monarchy,
private val liveEventManager: Lazy<StreamEventsManager>) {
enum class Result {
SHOULD_FETCH_MORE,
@ -170,6 +174,7 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
}
roomMemberContentsByUser[event.stateKey] = contentToUse.toModel<RoomMemberContent>()
}
liveEventManager.get().dispatchPaginatedEventReceived(event, roomId)
currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser)
}
}

View file

@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.session.sync.handler.room
import dagger.Lazy
import io.realm.Realm
import io.realm.kotlin.createObject
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
@ -52,6 +53,7 @@ import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.extensions.clearWith
import org.matrix.android.sdk.internal.session.StreamEventsManager
import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent
import org.matrix.android.sdk.internal.session.initsync.ProgressReporter
import org.matrix.android.sdk.internal.session.initsync.mapWithProgress
@ -79,7 +81,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
private val threadsAwarenessHandler: ThreadsAwarenessHandler,
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
@UserId private val userId: String,
private val timelineInput: TimelineInput) {
private val timelineInput: TimelineInput,
private val liveEventService: Lazy<StreamEventsManager>) {
sealed class HandlingStrategy {
data class JOINED(val data: Map<String, RoomSync>) : HandlingStrategy()
@ -364,6 +367,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
continue
}
eventIds.add(event.eventId)
liveEventService.get().dispatchLiveEventReceived(event, roomId, insertType == EventInsertType.INITIAL_SYNC)
val isInitialSync = insertType == EventInsertType.INITIAL_SYNC

View file

@ -160,7 +160,7 @@ Formatter\.formatShortFileSize===1
# android\.text\.TextUtils
### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt
enum class===118
enum class===119
### Do not import temporary legacy classes
import org.matrix.android.sdk.internal.legacy.riot===3

View file

@ -0,0 +1,273 @@
/*
* Copyright (c) 2021 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.app
import android.content.Context
import android.content.SharedPreferences
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.features.rageshake.BugReporter
import im.vector.app.features.rageshake.ReportType
import im.vector.app.features.settings.VectorPreferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.toContent
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
const val AUTO_RS_REQUEST = "im.vector.auto_rs_request"
@Singleton
class AutoRageShaker @Inject constructor(
private val sessionDataSource: ActiveSessionDataSource,
private val activeSessionHolder: ActiveSessionHolder,
private val bugReporter: BugReporter,
private val context: Context,
private val vectorPreferences: VectorPreferences
) : Session.Listener, SharedPreferences.OnSharedPreferenceChangeListener {
private val activeSessionIds = mutableSetOf<String>()
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private var currentActiveSessionId: String? = null
// Simple in memory cache of already sent report
private data class ReportInfo(
val roomId: String,
val sessionId: String
)
private val alreadyReportedUisi = mutableListOf<ReportInfo>()
private val e2eDetectedFlow = MutableSharedFlow<E2EMessageDetected>(replay = 0)
private val matchingRSRequestFlow = MutableSharedFlow<Event>(replay = 0)
fun initialize() {
observeActiveSession()
// It's a singleton...
vectorPreferences.subscribeToChanges(this)
// Simple rate limit, notice that order is not
// necessarily preserved
e2eDetectedFlow
.onEach {
sendRageShake(it)
delay(2_000)
}
.catch { cause ->
Timber.w(cause, "Failed to RS")
}
.launchIn(coroutineScope)
matchingRSRequestFlow
.onEach {
sendMatchingRageShake(it)
delay(2_000)
}
.catch { cause ->
Timber.w(cause, "Failed to send matching rageshake")
}
.launchIn(coroutineScope)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
enable(vectorPreferences.labsAutoReportUISI())
}
var _enabled = false
fun enable(enabled: Boolean) {
if (enabled == _enabled) return
_enabled = enabled
detector.enabled = enabled
}
private fun observeActiveSession() {
sessionDataSource.stream()
.distinctUntilChanged()
.onEach {
it.orNull()?.let { session ->
onSessionActive(session)
}
}
.launchIn(coroutineScope)
}
fun decryptionErrorDetected(target: E2EMessageDetected) {
if (target.source == UISIEventSource.INITIAL_SYNC) return
if (activeSessionHolder.getSafeActiveSession()?.sessionId != currentActiveSessionId) return
val shouldSendRS = synchronized(alreadyReportedUisi) {
val reportInfo = ReportInfo(target.roomId, target.sessionId)
val alreadySent = alreadyReportedUisi.contains(reportInfo)
if (!alreadySent) {
alreadyReportedUisi.add(reportInfo)
}
!alreadySent
}
if (shouldSendRS) {
coroutineScope.launch {
e2eDetectedFlow.emit(target)
}
}
}
private fun sendRageShake(target: E2EMessageDetected) {
bugReporter.sendBugReport(
context = context,
reportType = ReportType.AUTO_UISI,
withDevicesLogs = true,
withCrashLogs = true,
withKeyRequestHistory = true,
withScreenshot = false,
theBugDescription = "UISI detected",
serverVersion = "",
canContact = false,
customFields = mapOf("auto-uisi" to buildString {
append("\neventId: ${target.eventId}")
append("\nroomId: ${target.roomId}")
append("\nsenderKey: ${target.senderKey}")
append("\nsource: ${target.source}")
append("\ndeviceId: ${target.senderDeviceId}")
append("\nuserId: ${target.senderUserId}")
append("\nsessionId: ${target.sessionId}")
}),
listener = object : BugReporter.IMXBugReportListener {
override fun onUploadCancelled() {
synchronized(alreadyReportedUisi) {
alreadyReportedUisi.remove(ReportInfo(target.roomId, target.sessionId))
}
}
override fun onUploadFailed(reason: String?) {
synchronized(alreadyReportedUisi) {
alreadyReportedUisi.remove(ReportInfo(target.roomId, target.sessionId))
}
}
override fun onProgress(progress: Int) {
}
override fun onUploadSucceed(reportUrl: String?) {
// we need to send the toDevice message to the sender
coroutineScope.launch {
try {
activeSessionHolder.getSafeActiveSession()?.sendToDevice(
eventType = AUTO_RS_REQUEST,
userId = target.senderUserId,
deviceId = target.senderDeviceId,
content = mapOf(
"event_id" to target.eventId,
"room_id" to target.roomId,
"session_id" to target.sessionId,
"device_id" to target.senderDeviceId,
"user_id" to target.senderUserId,
"sender_key" to target.senderKey,
"recipient_rageshake" to reportUrl
).toContent()
)
} catch (failure: Throwable) {
Timber.w("failed to send auto-uisi to device")
}
}
}
})
}
fun remoteAutoUISIRequest(event: Event) {
if (event.type != AUTO_RS_REQUEST) return
if (activeSessionHolder.getSafeActiveSession()?.sessionId != currentActiveSessionId) return
coroutineScope.launch {
matchingRSRequestFlow.emit(event)
}
}
private fun sendMatchingRageShake(event: Event) {
val eventId = event.content?.get("event_id")
val roomId = event.content?.get("room_id")
val sessionId = event.content?.get("session_id")
val deviceId = event.content?.get("device_id")
val userId = event.content?.get("user_id")
val senderKey = event.content?.get("sender_key")
val matchingIssue = event.content?.get("recipient_rageshake")?.toString() ?: ""
bugReporter.sendBugReport(
context = context,
reportType = ReportType.AUTO_UISI_SENDER,
withDevicesLogs = true,
withCrashLogs = true,
withKeyRequestHistory = true,
withScreenshot = false,
theBugDescription = "UISI detected $matchingIssue",
serverVersion = "",
canContact = false,
customFields = mapOf(
"auto-uisi" to buildString {
append("\neventId: $eventId")
append("\nroomId: $roomId")
append("\nsenderKey: $senderKey")
append("\ndeviceId: $deviceId")
append("\nuserId: $userId")
append("\nsessionId: $sessionId")
},
"recipient_rageshake" to matchingIssue
),
listener = null
)
}
private val detector = UISIDetector().apply {
callback = object : UISIDetector.UISIDetectorCallback {
override val reciprocateToDeviceEventType: String
get() = AUTO_RS_REQUEST
override fun uisiDetected(source: E2EMessageDetected) {
decryptionErrorDetected(source)
}
override fun uisiReciprocateRequest(source: Event) {
remoteAutoUISIRequest(source)
}
}
}
fun onSessionActive(session: Session) {
val sessionId = session.sessionId
if (sessionId == currentActiveSessionId) {
return
}
this.currentActiveSessionId = sessionId
this.detector.enabled = _enabled
activeSessionIds.add(sessionId)
session.addListener(this)
session.addEventStreamListener(detector)
}
override fun onSessionStopped(session: Session) {
session.removeEventStreamListener(detector)
activeSessionIds.remove(session.sessionId)
}
}

View file

@ -0,0 +1,160 @@
/*
* Copyright (c) 2021 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.app
import org.matrix.android.sdk.api.session.LiveEventListener
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
import timber.log.Timber
import java.util.Timer
import java.util.TimerTask
import java.util.concurrent.Executors
enum class UISIEventSource {
INITIAL_SYNC,
INCREMENTAL_SYNC,
PAGINATION
}
data class E2EMessageDetected(
val eventId: String,
val roomId: String,
val senderUserId: String,
val senderDeviceId: String,
val senderKey: String,
val sessionId: String,
val source: UISIEventSource) {
companion object {
fun fromEvent(event: Event, roomId: String, source: UISIEventSource): E2EMessageDetected {
val encryptedContent = event.content.toModel<EncryptedEventContent>()
return E2EMessageDetected(
eventId = event.eventId ?: "",
roomId = roomId,
senderUserId = event.senderId ?: "",
senderDeviceId = encryptedContent?.deviceId ?: "",
senderKey = encryptedContent?.senderKey ?: "",
sessionId = encryptedContent?.sessionId ?: "",
source = source
)
}
}
}
class UISIDetector : LiveEventListener {
interface UISIDetectorCallback {
val reciprocateToDeviceEventType: String
fun uisiDetected(source: E2EMessageDetected)
fun uisiReciprocateRequest(source: Event)
}
var callback: UISIDetectorCallback? = null
private val trackedEvents = mutableListOf<Pair<E2EMessageDetected, TimerTask>>()
private val executor = Executors.newSingleThreadExecutor()
private val timer = Timer()
private val timeoutMillis = 30_000L
var enabled = false
override fun onLiveEvent(roomId: String, event: Event) {
if (!enabled) return
if (!event.isEncrypted()) return
executor.execute {
handleEventReceived(E2EMessageDetected.fromEvent(event, roomId, UISIEventSource.INCREMENTAL_SYNC))
}
}
override fun onPaginatedEvent(roomId: String, event: Event) {
if (!enabled) return
if (!event.isEncrypted()) return
executor.execute {
handleEventReceived(E2EMessageDetected.fromEvent(event, roomId, UISIEventSource.PAGINATION))
}
}
override fun onEventDecrypted(eventId: String, roomId: String, clearEvent: JsonDict) {
if (!enabled) return
executor.execute {
unTrack(eventId, roomId)
}
}
override fun onLiveToDeviceEvent(event: Event) {
if (!enabled) return
if (event.type == callback?.reciprocateToDeviceEventType) {
callback?.uisiReciprocateRequest(event)
}
}
override fun onEventDecryptionError(eventId: String, roomId: String, throwable: Throwable) {
if (!enabled) return
executor.execute {
unTrack(eventId, roomId)?.let {
triggerUISI(it)
}
// if (throwable is MXCryptoError.OlmError) {
// if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") {
// unTrack(eventId, roomId)?.let {
// triggerUISI(it)
// }
// }
// }
}
}
private fun handleEventReceived(detectorEvent: E2EMessageDetected) {
if (!enabled) return
if (trackedEvents.any { it.first == detectorEvent }) {
Timber.w("## UISIDetector: Event ${detectorEvent.eventId} is already tracked")
} else {
// track it and start timer
val timeoutTask = object : TimerTask() {
override fun run() {
executor.execute {
unTrack(detectorEvent.eventId, detectorEvent.roomId)
Timber.v("## UISIDetector: Timeout on ${detectorEvent.eventId} ")
triggerUISI(detectorEvent)
}
}
}
trackedEvents.add(detectorEvent to timeoutTask)
timer.schedule(timeoutTask, timeoutMillis)
}
}
private fun triggerUISI(source: E2EMessageDetected) {
if (!enabled) return
Timber.i("## UISIDetector: Unable To Decrypt $source")
callback?.uisiDetected(source)
}
private fun unTrack(eventId: String, roomId: String): E2EMessageDetected? {
val index = trackedEvents.indexOfFirst { it.first.eventId == eventId && it.first.roomId == roomId }
return if (index != -1) {
trackedEvents.removeAt(index).let {
it.second.cancel()
it.first
}
} else {
null
}
}
}

View file

@ -96,6 +96,7 @@ class VectorApplication :
@Inject lateinit var pinLocker: PinLocker
@Inject lateinit var callManager: WebRtcCallManager
@Inject lateinit var invitesAcceptor: InvitesAcceptor
@Inject lateinit var autoRageShaker: AutoRageShaker
@Inject lateinit var vectorFileLogger: VectorFileLogger
@Inject lateinit var vectorAnalytics: VectorAnalytics
@ -117,6 +118,7 @@ class VectorApplication :
appContext = this
vectorAnalytics.init()
invitesAcceptor.initialize()
autoRageShaker.initialize()
vectorUncaughtExceptionHandler.activate(this)
// Remove Log handler statically added by Jitsi

View file

@ -84,6 +84,9 @@ class BugReportActivity : VectorBaseActivity<ActivityBugReportBinding>() {
hideBugReportOptions()
}
else -> {
// other types not supported here
}
}
}
@ -156,6 +159,7 @@ class BugReportActivity : VectorBaseActivity<ActivityBugReportBinding>() {
views.bugReportEditText.text.toString(),
state.serverVersion,
views.bugReportButtonContactMe.isChecked,
null,
object : BugReporter.IMXBugReportListener {
override fun onUploadFailed(reason: String?) {
try {
@ -173,6 +177,9 @@ class BugReportActivity : VectorBaseActivity<ActivityBugReportBinding>() {
Toast.makeText(this@BugReportActivity,
getString(R.string.feedback_failed, reason), Toast.LENGTH_LONG).show()
}
else -> {
// nop
}
}
}
} catch (e: Exception) {
@ -198,7 +205,7 @@ class BugReportActivity : VectorBaseActivity<ActivityBugReportBinding>() {
views.bugReportProgressTextView.text = getString(R.string.send_bug_report_progress, myProgress.toString())
}
override fun onUploadSucceed() {
override fun onUploadSucceed(reportUrl: String?) {
try {
when (reportType) {
ReportType.BUG_REPORT -> {
@ -210,6 +217,9 @@ class BugReportActivity : VectorBaseActivity<ActivityBugReportBinding>() {
ReportType.SPACE_BETA_FEEDBACK -> {
Toast.makeText(this@BugReportActivity, R.string.feedback_sent, Toast.LENGTH_LONG).show()
}
else -> {
// nop
}
}
} catch (e: Exception) {
Timber.e(e, "## onUploadSucceed() : failed to dismiss the toast")

View file

@ -24,6 +24,7 @@ import android.os.Build
import android.view.View
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentActivity
import com.squareup.moshi.Types
import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
@ -49,7 +50,9 @@ import okhttp3.Response
import org.json.JSONException
import org.json.JSONObject
import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.api.util.MimeTypes
import org.matrix.android.sdk.internal.di.MoshiProvider
import timber.log.Timber
import java.io.File
import java.io.IOException
@ -93,6 +96,9 @@ class BugReporter @Inject constructor(
// boolean to cancel the bug report
private val mIsCancelled = false
val adapter = MoshiProvider.providesMoshi()
.adapter<JsonDict>(Types.newParameterizedType(Map::class.java, String::class.java, Any::class.java))
/**
* Get current Screenshot
*
@ -141,7 +147,7 @@ class BugReporter @Inject constructor(
/**
* The bug report upload succeeded.
*/
fun onUploadSucceed()
fun onUploadSucceed(reportUrl: String?)
}
/**
@ -166,12 +172,14 @@ class BugReporter @Inject constructor(
theBugDescription: String,
serverVersion: String,
canContact: Boolean = false,
customFields: Map<String, String>? = null,
listener: IMXBugReportListener?) {
// enumerate files to delete
val mBugReportFiles: MutableList<File> = ArrayList()
coroutineScope.launch {
var serverError: String? = null
var reportURL: String? = null
withContext(Dispatchers.IO) {
var bugDescription = theBugDescription
val crashCallStack = getCrashDescription(context)
@ -250,6 +258,8 @@ class BugReporter @Inject constructor(
ReportType.BUG_REPORT -> "[Element] $bugDescription"
ReportType.SUGGESTION -> "[Element] [Suggestion] $bugDescription"
ReportType.SPACE_BETA_FEEDBACK -> "[Element] [spaces-feedback] $bugDescription"
ReportType.AUTO_UISI_SENDER,
ReportType.AUTO_UISI -> "[AutoUISI] $bugDescription"
}
// build the multi part request
@ -273,7 +283,11 @@ class BugReporter @Inject constructor(
.addFormDataPart("app_language", VectorLocale.applicationLocale.toString())
.addFormDataPart("default_app_language", systemLocaleProvider.getSystemLocale().toString())
.addFormDataPart("theme", ThemeUtils.getApplicationTheme(context))
.addFormDataPart("server_version", serverVersion)
.addFormDataPart("server_version", serverVersion).apply {
customFields?.forEach { (name, value) ->
addFormDataPart(name, value)
}
}
val buildNumber = context.getString(R.string.build_number)
if (buildNumber.isNotEmpty() && buildNumber != "0") {
@ -326,6 +340,10 @@ class BugReporter @Inject constructor(
}
ReportType.SUGGESTION -> builder.addFormDataPart("label", "[Suggestion]")
ReportType.SPACE_BETA_FEEDBACK -> builder.addFormDataPart("label", "spaces-feedback")
ReportType.AUTO_UISI,
ReportType.AUTO_UISI_SENDER -> {
builder.addFormDataPart("label", "Z-UISI")
}
}
if (getCrashFile(context).exists()) {
@ -417,6 +435,10 @@ class BugReporter @Inject constructor(
Timber.e(e, "## sendBugReport() : failed to parse error")
}
}
} else {
reportURL = response?.body?.string()?.let { stringBody ->
adapter.fromJson(stringBody)?.get("report_url")?.toString()
}
}
}
}
@ -434,7 +456,7 @@ class BugReporter @Inject constructor(
if (mIsCancelled) {
listener.onUploadCancelled()
} else if (null == serverError) {
listener.onUploadSucceed()
listener.onUploadSucceed(reportURL)
} else {
listener.onUploadFailed(serverError)
}

View file

@ -19,5 +19,7 @@ package im.vector.app.features.rageshake
enum class ReportType {
BUG_REPORT,
SUGGESTION,
SPACE_BETA_FEEDBACK
SPACE_BETA_FEEDBACK,
AUTO_UISI,
AUTO_UISI_SENDER,
}

View file

@ -152,6 +152,7 @@ class VectorPreferences @Inject constructor(private val context: Context) {
const val SETTINGS_LABS_ALLOW_EXTENDED_LOGS = "SETTINGS_LABS_ALLOW_EXTENDED_LOGS"
const val SETTINGS_LABS_USE_RESTRICTED_JOIN_RULE = "SETTINGS_LABS_USE_RESTRICTED_JOIN_RULE"
const val SETTINGS_LABS_SPACES_HOME_AS_ORPHAN = "SETTINGS_LABS_SPACES_HOME_AS_ORPHAN"
const val SETTINGS_LABS_AUTO_REPORT_UISI = "SETTINGS_LABS_AUTO_REPORT_UISI"
const val SETTINGS_PREF_SPACE_SHOW_ALL_ROOM_IN_HOME = "SETTINGS_PREF_SPACE_SHOW_ALL_ROOM_IN_HOME"
private const val SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY = "SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY"
@ -245,7 +246,7 @@ class VectorPreferences @Inject constructor(private val context: Context) {
SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY,
SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY,
SETTINGS_LABS_ALLOW_EXTENDED_LOGS,
SETTINGS_LABS_USE_RESTRICTED_JOIN_RULE,
// SETTINGS_LABS_USE_RESTRICTED_JOIN_RULE,
SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY,
SETTINGS_USE_RAGE_SHAKE_KEY,
@ -974,6 +975,10 @@ class VectorPreferences @Inject constructor(private val context: Context) {
return defaultPrefs.getBoolean(SETTINGS_LABS_SPACES_HOME_AS_ORPHAN, false)
}
fun labsAutoReportUISI(): Boolean {
return defaultPrefs.getBoolean(SETTINGS_LABS_AUTO_REPORT_UISI, false)
}
fun prefSpacesShowAllRoomInHome(): Boolean {
return defaultPrefs.getBoolean(SETTINGS_PREF_SPACE_SHOW_ALL_ROOM_IN_HOME,
// migration of old property

View file

@ -17,12 +17,20 @@
package im.vector.app.features.settings
import im.vector.app.R
import im.vector.app.core.preference.VectorSwitchPreference
import javax.inject.Inject
class VectorSettingsLabsFragment @Inject constructor() : VectorSettingsBaseFragment() {
class VectorSettingsLabsFragment @Inject constructor(
private val vectorPreferences: VectorPreferences
) : VectorSettingsBaseFragment() {
override var titleRes = R.string.room_settings_labs_pref_title
override val preferenceXmlRes = R.xml.vector_settings_labs
override fun bindPref() {}
override fun bindPref() {
findPreference<VectorSwitchPreference>(VectorPreferences.SETTINGS_LABS_AUTO_REPORT_UISI)?.let { pref ->
// ensure correct default
pref.isChecked = vectorPreferences.labsAutoReportUISI()
}
}
}

View file

@ -3565,6 +3565,11 @@
<string name="labs_use_restricted_join_rule">Experimental Space - Restricted Room.</string>
<string name="labs_use_restricted_join_rule_desc">Warning requires server support and experimental room version</string>
<string name="labs_auto_report_uisi">Auto Report Decryption Errors.</string>
<string name="labs_auto_report_uisi_desc">Your system will automatically send logs when an unable to decrypt error occurs</string>
<string name="user_invites_you">%s invites you</string>
<string name="looking_for_someone_not_in_space">Looking for someone not in %s?</string>

View file

@ -63,4 +63,10 @@
android:title="@string/labs_enable_polls" />
<im.vector.app.core.preference.VectorSwitchPreference
android:defaultValue="false"
android:key="SETTINGS_LABS_AUTO_REPORT_UISI"
android:title="@string/labs_auto_report_uisi"
android:summary="@string/labs_auto_report_uisi_desc"/>
</androidx.preference.PreferenceScreen>