Support for lab auto rageshake UISI

This commit is contained in:
Valere 2021-10-22 18:10:27 +02:00
parent 6a34b999f2
commit be119ea161
11 changed files with 430 additions and 17 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,204 @@
/*
* 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 im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.features.rageshake.BugReporter
import im.vector.app.features.rageshake.ReportType
import io.reactivex.disposables.Disposable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
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
) : Session.Listener {
private lateinit var activeSessionDisposable: Disposable
private val activeSessionIds = mutableSetOf<String>()
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val uisiDetectors = mutableMapOf<String, UISIDetector>()
private var currentActiveSessionId: String? = null
fun initialize() {
observeActiveSession()
}
var _enabled = false
fun enable(enabled: Boolean) {
uisiDetectors.forEach { it.value.enabled = enabled }
}
private fun observeActiveSession() {
activeSessionDisposable = sessionDataSource.observe()
.distinctUntilChanged()
.subscribe {
it.orNull()?.let { session ->
onSessionActive(session)
}
}
}
fun decryptionErrorDetected(target: E2EMessageDetected) {
if (target.source == UISIEventSource.INITIAL_SYNC) return
if (activeSessionHolder.getSafeActiveSession()?.sessionId != currentActiveSessionId) return
coroutineScope.launch {
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() {
}
override fun onUploadFailed(reason: String?) {
}
override fun onProgress(progress: Int) {
}
override fun onUploadSucceed(reportUrl: String?) {
Timber.w("## VALR Report URL is $reportUrl")
// 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,
"matching_issue" to reportUrl
).toContent()
)
} catch (failure: Throwable) {
Timber.w("## VALR : 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 {
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("matching_issue")?.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<String, String>(
"auto-uisi" to buildString {
append("\neventId: $eventId")
append("\nroomId: $roomId")
append("\nsenderKey: $senderKey")
append("\ndeviceId: $deviceId")
append("\nuserId: $userId")
append("\nsessionId: $sessionId")
},
"matching_issue" 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) {
uisiDetectors.get(session.sessionId)?.let {
session.removeEventStreamListener(it)
}
activeSessionIds.remove(session.sessionId)
}
}

View file

@ -0,0 +1,153 @@
/*
* 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 (!event.isEncrypted()) return
executor.execute {
handleEventReceived(E2EMessageDetected.fromEvent(event, roomId, UISIEventSource.INCREMENTAL_SYNC))
}
}
override fun onPaginatedEvent(roomId: String, event: Event) {
if (!event.isEncrypted()) return
executor.execute {
handleEventReceived(E2EMessageDetected.fromEvent(event, roomId, UISIEventSource.PAGINATION))
}
}
override fun onEventDecrypted(eventId: String, roomId: String, clearEvent: JsonDict) {
executor.execute {
unTrack(eventId, roomId)
}
}
override fun onLiveToDeviceEvent(event: Event) {
if (event.type == callback?.reciprocateToDeviceEventType) {
callback?.uisiReciprocateRequest(event)
}
}
override fun onEventDecryptionError(eventId: String, roomId: String, throwable: Throwable) {
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 (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) {
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

@ -156,6 +156,7 @@ class BugReportActivity : VectorBaseActivity<ActivityBugReportBinding>() {
views.bugReportEditText.text.toString(),
state.serverVersion,
views.bugReportButtonContactMe.isChecked,
null,
object : BugReporter.IMXBugReportListener {
override fun onUploadFailed(reason: String?) {
try {
@ -198,7 +199,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 -> {

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,14 @@ class BugReporter @Inject constructor(
}
ReportType.SUGGESTION -> builder.addFormDataPart("label", "[Suggestion]")
ReportType.SPACE_BETA_FEEDBACK -> builder.addFormDataPart("label", "spaces-feedback")
ReportType.AUTO_UISI -> {
builder.addFormDataPart("label", "auto-uisis-receiver")
builder.addFormDataPart("label", "auto-uisis")
}
ReportType.AUTO_UISI_SENDER -> {
builder.addFormDataPart("label", "auto-uisis-sender")
builder.addFormDataPart("label", "auto-uisis")
}
}
if (getCrashFile(context).exists()) {
@ -417,6 +439,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 +460,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

@ -16,13 +16,26 @@
package im.vector.app.features.settings
// import im.vector.app.AutoRageShaker
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,
// private val autoRageShaker: AutoRageShaker
) : 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 ->
pref.isChecked = vectorPreferences.labsAutoReportUISI()
pref.setOnPreferenceChangeListener { _, isChecked ->
// autoRageShaker.enable(isChecked as Boolean)
true
}
}
}
}

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

@ -44,11 +44,6 @@
android:key="SETTINGS_LABS_UNREAD_NOTIFICATIONS_AS_TAB"
android:title="@string/labs_show_unread_notifications_as_tab" />
<im.vector.app.core.preference.VectorSwitchPreference
android:defaultValue="false"
android:key="SETTINGS_LABS_USE_RESTRICTED_JOIN_RULE"
android:summary="@string/labs_use_restricted_join_rule_desc"
android:title="@string/labs_use_restricted_join_rule" />
<im.vector.app.core.preference.VectorSwitchPreference
android:defaultValue="false"
@ -63,4 +58,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>