diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml
index a2e408b50de..f99842f0671 100644
--- a/.idea/dictionaries/bmarty.xml
+++ b/.idea/dictionaries/bmarty.xml
@@ -36,6 +36,7 @@
ssss
sygnal
threepid
+ uisi
unpublish
unwedging
vctr
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/EventStreamService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/EventStreamService.kt
new file mode 100644
index 00000000000..a1316a54445
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/EventStreamService.kt
@@ -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)
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/LiveEventListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/LiveEventListener.kt
new file mode 100644
index 00000000000..6fda65953ac
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/LiveEventListener.kt
@@ -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..
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt
index 3f817ec4d2b..36ab0073142 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt
@@ -84,7 +84,9 @@ interface Session :
SyncStatusService,
HomeServerCapabilitiesService,
SecureStorageService,
- AccountService {
+ AccountService,
+ ToDeviceService,
+ EventStreamService {
val coroutineDispatchers: MatrixCoroutineDispatchers
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/ToDeviceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/ToDeviceService.kt
new file mode 100644
index 00000000000..45fd39fa954
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/ToDeviceService.kt
@@ -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, 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>, content: Content, txnId: String? = UUID.randomUUID().toString())
+
+ suspend fun sendEncryptedToDevice(eventType: String, targets: Map>, content: Content, txnId: String? = UUID.randomUUID().toString())
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt
index 9dd369f426e..00cd2789217 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt
@@ -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
) : CryptoService {
private val isStarting = AtomicBoolean(false)
@@ -782,6 +784,7 @@ internal class DefaultCryptoService @Inject constructor(
}
}
}
+ liveEventManager.get().dispatchOnLiveToDevice(event)
}
/**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt
index ceceedc8029..2ee24dfbb06 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt
@@ -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
) : 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") {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt
index 29f9d193f84..3eba04b9f18 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt
@@ -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
) {
fun create(): MXMegolmDecryption {
@@ -52,6 +55,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor(
cryptoStore,
sendToDeviceTask,
coroutineDispatchers,
- cryptoCoroutineScope)
+ cryptoCoroutineScope,
+ eventsManager)
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultEventStreamService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultEventStreamService.kt
new file mode 100644
index 00000000000..ed21e9f1c62
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultEventStreamService.kt
@@ -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)
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt
index c07ff48cf48..1e533158a76 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt
@@ -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,
private val openIdService: Lazy,
private val presenceService: Lazy,
+ private val toDeviceService: Lazy,
+ private val eventStreamService: Lazy,
@UnauthenticatedWithCertificate
private val unauthenticatedWithCertificateOkHttpClient: Lazy
) : 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()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultToDeviceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultToDeviceService.kt
new file mode 100644
index 00000000000..1615b8eef9a
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultToDeviceService.kt
@@ -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>, content: Content, txnId: String?) {
+ val sendToDeviceMap = MXUsersDevicesMap()
+ targets.forEach { (userId, deviceIdList) ->
+ deviceIdList.forEach { deviceId ->
+ sendToDeviceMap.setObject(userId, deviceId, content)
+ }
+ }
+ sendToDevice(eventType, sendToDeviceMap, txnId)
+ }
+
+ override suspend fun sendToDevice(eventType: String, contentMap: MXUsersDevicesMap, txnId: String?) {
+ sendToDeviceTask.executeRetry(
+ SendToDeviceTask.Params(
+ eventType = eventType,
+ contentMap = contentMap,
+ transactionId = txnId
+ ),
+ 3
+ )
+ }
+
+ override suspend fun sendEncryptedToDevice(eventType: String, targets: Map>, content: Content, txnId: String?) {
+ val payloadJson = mapOf(
+ "type" to eventType,
+ "content" to content
+ )
+ val sendToDeviceMap = MXUsersDevicesMap()
+
+ // 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)
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt
index e2cfea479d3..531dea1d5a6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt
@@ -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
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt
new file mode 100644
index 00000000000..bb0ca114454
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt
@@ -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()
+
+ 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)
+ }
+ }
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt
index 4625155c0ae..a85f0dbdc93 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt
@@ -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) {
enum class Result {
SHOULD_FETCH_MORE,
@@ -170,6 +174,7 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
}
roomMemberContentsByUser[event.stateKey] = contentToUse.toModel()
}
+ liveEventManager.get().dispatchPaginatedEventReceived(event, roomId)
currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser)
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt
index f090975cade..3fdfb473db9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt
@@ -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) {
sealed class HandlingStrategy {
data class JOINED(val data: Map) : 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
diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt
index 9db73230eee..293e0b2a583 100644
--- a/tools/check/forbidden_strings_in_code.txt
+++ b/tools/check/forbidden_strings_in_code.txt
@@ -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
diff --git a/vector/src/main/java/im/vector/app/AutoRageShaker.kt b/vector/src/main/java/im/vector/app/AutoRageShaker.kt
new file mode 100644
index 00000000000..ca91f728cb6
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/AutoRageShaker.kt
@@ -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()
+ 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()
+
+ private val e2eDetectedFlow = MutableSharedFlow(replay = 0)
+ private val matchingRSRequestFlow = MutableSharedFlow(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)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/UISIDetector.kt b/vector/src/main/java/im/vector/app/UISIDetector.kt
new file mode 100644
index 00000000000..d6a4805e787
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/UISIDetector.kt
@@ -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()
+
+ 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>()
+ 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
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/VectorApplication.kt b/vector/src/main/java/im/vector/app/VectorApplication.kt
index 400fb7eb892..05d20662c74 100644
--- a/vector/src/main/java/im/vector/app/VectorApplication.kt
+++ b/vector/src/main/java/im/vector/app/VectorApplication.kt
@@ -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
diff --git a/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt b/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt
index b27c2e9818b..02df86e14b7 100755
--- a/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt
@@ -62,11 +62,11 @@ class BugReportActivity : VectorBaseActivity() {
// Default screen is for bug report, so modify it for suggestion
when (reportType) {
- ReportType.BUG_REPORT -> {
+ ReportType.BUG_REPORT -> {
supportActionBar?.setTitle(R.string.title_activity_bug_report)
views.bugReportButtonContactMe.isVisible = true
}
- ReportType.SUGGESTION -> {
+ ReportType.SUGGESTION -> {
supportActionBar?.setTitle(R.string.send_suggestion)
views.bugReportFirstText.setText(R.string.send_suggestion_content)
@@ -84,6 +84,9 @@ class BugReportActivity : VectorBaseActivity() {
hideBugReportOptions()
}
+ else -> {
+ // other types not supported here
+ }
}
}
@@ -156,6 +159,7 @@ class BugReportActivity : VectorBaseActivity() {
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() {
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() {
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() {
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")
diff --git a/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt b/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt
index 5b3d194d335..26e9cabccb5 100755
--- a/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt
+++ b/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt
@@ -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(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? = null,
listener: IMXBugReportListener?) {
// enumerate files to delete
val mBugReportFiles: MutableList = ArrayList()
coroutineScope.launch {
var serverError: String? = null
+ var reportURL: String? = null
withContext(Dispatchers.IO) {
var bugDescription = theBugDescription
val crashCallStack = getCrashDescription(context)
@@ -247,9 +255,11 @@ class BugReporter @Inject constructor(
if (!mIsCancelled) {
val text = when (reportType) {
- ReportType.BUG_REPORT -> "[Element] $bugDescription"
- ReportType.SUGGESTION -> "[Element] [Suggestion] $bugDescription"
+ 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") {
@@ -321,11 +335,15 @@ class BugReporter @Inject constructor(
builder.addFormDataPart("label", "[Element]")
when (reportType) {
- ReportType.BUG_REPORT -> {
+ ReportType.BUG_REPORT -> {
/* nop */
}
- ReportType.SUGGESTION -> builder.addFormDataPart("label", "[Suggestion]")
+ 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)
}
diff --git a/vector/src/main/java/im/vector/app/features/rageshake/ReportType.kt b/vector/src/main/java/im/vector/app/features/rageshake/ReportType.kt
index 44682efb407..f9dc6289143 100644
--- a/vector/src/main/java/im/vector/app/features/rageshake/ReportType.kt
+++ b/vector/src/main/java/im/vector/app/features/rageshake/ReportType.kt
@@ -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,
}
diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
index 3f423696ae3..7c2b983859a 100755
--- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
@@ -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
diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt
index f4579804035..a83b4c33f44 100644
--- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt
@@ -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(VectorPreferences.SETTINGS_LABS_AUTO_REPORT_UISI)?.let { pref ->
+ // ensure correct default
+ pref.isChecked = vectorPreferences.labsAutoReportUISI()
+ }
+ }
}
diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
index 1816e0d1fad..d77e04a6f1e 100644
--- a/vector/src/main/res/values/strings.xml
+++ b/vector/src/main/res/values/strings.xml
@@ -3565,6 +3565,11 @@
Experimental Space - Restricted Room.
Warning requires server support and experimental room version
+
+
+ Auto Report Decryption Errors.
+ Your system will automatically send logs when an unable to decrypt error occurs
+
%s invites you
Looking for someone not in %s?
diff --git a/vector/src/main/res/xml/vector_settings_labs.xml b/vector/src/main/res/xml/vector_settings_labs.xml
index f394d1923ed..96d5588ee6a 100644
--- a/vector/src/main/res/xml/vector_settings_labs.xml
+++ b/vector/src/main/res/xml/vector_settings_labs.xml
@@ -63,4 +63,10 @@
android:title="@string/labs_enable_polls" />
+
+
\ No newline at end of file