Migrate BugReport and CrashDetection to new architecture
This commit is contained in:
parent
7edfcac62b
commit
acc091ef5f
12 changed files with 242 additions and 329 deletions
|
|
@ -8,7 +8,6 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
@ -27,14 +26,10 @@ import com.bumble.appyx.core.node.ParentNode
|
||||||
import com.bumble.appyx.core.node.node
|
import com.bumble.appyx.core.node.node
|
||||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.newRoot
|
import com.bumble.appyx.navmodel.backstack.operation.newRoot
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.replace
|
|
||||||
import io.element.android.x.BuildConfig
|
import io.element.android.x.BuildConfig
|
||||||
import io.element.android.x.component.ShowkaseButton
|
import io.element.android.x.component.ShowkaseButton
|
||||||
import io.element.android.x.core.di.DaggerComponentOwner
|
import io.element.android.x.core.di.DaggerComponentOwner
|
||||||
import io.element.android.x.di.SessionComponentsOwner
|
import io.element.android.x.di.SessionComponentsOwner
|
||||||
import io.element.android.x.features.rageshake.bugreport.BugReportScreen
|
|
||||||
import io.element.android.x.features.rageshake.crash.ui.CrashDetectionScreen
|
|
||||||
import io.element.android.x.features.rageshake.detection.RageshakeDetectionScreen
|
|
||||||
import io.element.android.x.getBrowserIntent
|
import io.element.android.x.getBrowserIntent
|
||||||
import io.element.android.x.matrix.Matrix
|
import io.element.android.x.matrix.Matrix
|
||||||
import io.element.android.x.matrix.core.SessionId
|
import io.element.android.x.matrix.core.SessionId
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ plugins {
|
||||||
id("io.element.android-compose-library")
|
id("io.element.android-compose-library")
|
||||||
alias(libs.plugins.ksp)
|
alias(libs.plugins.ksp)
|
||||||
alias(libs.plugins.anvil)
|
alias(libs.plugins.anvil)
|
||||||
|
id("kotlin-parcelize")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package io.element.android.x.features.rageshake.bugreport
|
||||||
|
|
||||||
|
sealed interface BugReportEvents {
|
||||||
|
object SendBugReport : BugReportEvents
|
||||||
|
object ResetAll: BugReportEvents
|
||||||
|
data class SetDescription(val description: String): BugReportEvents
|
||||||
|
data class SetSendLog(val sendLog: Boolean): BugReportEvents
|
||||||
|
data class SetSendCrashLog(val sendCrashlog: Boolean): BugReportEvents
|
||||||
|
data class SetCanContact(val canContact: Boolean): BugReportEvents
|
||||||
|
data class SetSendScreenshot(val sendScreenshot: Boolean) : BugReportEvents
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
package io.element.android.x.features.rageshake.bugreport
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import io.element.android.x.architecture.Async
|
||||||
|
import io.element.android.x.architecture.Presenter
|
||||||
|
import io.element.android.x.features.rageshake.crash.CrashDataStore
|
||||||
|
import io.element.android.x.features.rageshake.logs.VectorFileLogger
|
||||||
|
import io.element.android.x.features.rageshake.reporter.BugReporter
|
||||||
|
import io.element.android.x.features.rageshake.reporter.ReportType
|
||||||
|
import io.element.android.x.features.rageshake.screenshot.ScreenshotHolder
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class BugReportPresenter @Inject constructor(
|
||||||
|
private val bugReporter: BugReporter,
|
||||||
|
private val crashDataStore: CrashDataStore,
|
||||||
|
private val screenshotHolder: ScreenshotHolder,
|
||||||
|
private val appCoroutineScope: CoroutineScope,
|
||||||
|
) : Presenter<BugReportState, BugReportEvents> {
|
||||||
|
|
||||||
|
private class BugReporterUploadListener(
|
||||||
|
private val sendingProgress: MutableState<Float>,
|
||||||
|
private val sendingAction: MutableState<Async<Unit>>
|
||||||
|
) : BugReporter.IMXBugReportListener {
|
||||||
|
override fun onUploadCancelled() {
|
||||||
|
sendingProgress.value = 0f
|
||||||
|
sendingAction.value = Async.Uninitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUploadFailed(reason: String?) {
|
||||||
|
sendingProgress.value = 0f
|
||||||
|
sendingAction.value = Async.Failure(Exception(reason))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onProgress(progress: Int) {
|
||||||
|
sendingProgress.value = progress.toFloat() / 100
|
||||||
|
sendingAction.value = Async.Loading()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUploadSucceed(reportUrl: String?) {
|
||||||
|
sendingProgress.value = 0f
|
||||||
|
sendingAction.value = Async.Success(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun present(events: Flow<BugReportEvents>): BugReportState {
|
||||||
|
val crashInfo: String by crashDataStore
|
||||||
|
.crashInfo()
|
||||||
|
.collectAsState(initial = "")
|
||||||
|
|
||||||
|
val sendingProgress = remember {
|
||||||
|
mutableStateOf(0f)
|
||||||
|
}
|
||||||
|
val sendingAction: MutableState<Async<Unit>> = remember {
|
||||||
|
mutableStateOf(Async.Uninitialized)
|
||||||
|
}
|
||||||
|
val formState: MutableState<BugReportFormState> = rememberSaveable {
|
||||||
|
mutableStateOf(BugReportFormState.Default)
|
||||||
|
}
|
||||||
|
val uploadListener = BugReporterUploadListener(sendingProgress, sendingAction)
|
||||||
|
val state = BugReportState(
|
||||||
|
hasCrashLogs = crashInfo.isNotEmpty(),
|
||||||
|
sendingProgress = sendingProgress.value,
|
||||||
|
sending = sendingAction.value
|
||||||
|
)
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
events.collect { event ->
|
||||||
|
when (event) {
|
||||||
|
BugReportEvents.SendBugReport -> appCoroutineScope.sendBugReport(state, uploadListener)
|
||||||
|
BugReportEvents.ResetAll -> appCoroutineScope.resetAll()
|
||||||
|
is BugReportEvents.SetDescription -> updateFormState(formState) {
|
||||||
|
copy(description = event.description)
|
||||||
|
}
|
||||||
|
is BugReportEvents.SetCanContact -> updateFormState(formState) {
|
||||||
|
copy(canContact = event.canContact)
|
||||||
|
}
|
||||||
|
is BugReportEvents.SetSendCrashLog -> updateFormState(formState) {
|
||||||
|
copy(sendCrashLogs = event.sendCrashlog)
|
||||||
|
}
|
||||||
|
is BugReportEvents.SetSendLog -> updateFormState(formState) {
|
||||||
|
copy(sendLogs = event.sendLog)
|
||||||
|
}
|
||||||
|
is BugReportEvents.SetSendScreenshot -> updateFormState(formState) {
|
||||||
|
copy(sendScreenshot = event.sendScreenshot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateFormState(formState: MutableState<BugReportFormState>, operation: BugReportFormState.() -> BugReportFormState) {
|
||||||
|
formState.value = operation(formState.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.sendBugReport(state: BugReportState, listener: BugReporter.IMXBugReportListener) = launch {
|
||||||
|
bugReporter.sendBugReport(
|
||||||
|
coroutineScope = this,
|
||||||
|
reportType = ReportType.BUG_REPORT,
|
||||||
|
withDevicesLogs = state.formState.sendLogs,
|
||||||
|
withCrashLogs = state.hasCrashLogs && state.formState.sendCrashLogs,
|
||||||
|
withKeyRequestHistory = false,
|
||||||
|
withScreenshot = state.formState.sendScreenshot,
|
||||||
|
theBugDescription = state.formState.description,
|
||||||
|
serverVersion = "",
|
||||||
|
canContact = state.formState.canContact,
|
||||||
|
customFields = emptyMap(),
|
||||||
|
listener = listener
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.resetAll() = launch {
|
||||||
|
screenshotHolder.reset()
|
||||||
|
crashDataStore.reset()
|
||||||
|
VectorFileLogger.getFromTimber().reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,30 +16,37 @@
|
||||||
|
|
||||||
package io.element.android.x.features.rageshake.bugreport
|
package io.element.android.x.features.rageshake.bugreport
|
||||||
|
|
||||||
import com.airbnb.mvrx.Async
|
import android.os.Parcelable
|
||||||
import com.airbnb.mvrx.Loading
|
import io.element.android.x.architecture.Async
|
||||||
import com.airbnb.mvrx.MavericksState
|
import kotlinx.parcelize.Parcelize
|
||||||
import com.airbnb.mvrx.Uninitialized
|
|
||||||
|
|
||||||
data class BugReportViewState(
|
data class BugReportState(
|
||||||
val formState: BugReportFormState = BugReportFormState.Default,
|
val formState: BugReportFormState = BugReportFormState.Default,
|
||||||
val sendLogs: Boolean = true,
|
|
||||||
val hasCrashLogs: Boolean = false,
|
val hasCrashLogs: Boolean = false,
|
||||||
val sendCrashLogs: Boolean = true,
|
|
||||||
val canContact: Boolean = false,
|
|
||||||
val sendScreenshot: Boolean = false,
|
|
||||||
val screenshotUri: String? = null,
|
val screenshotUri: String? = null,
|
||||||
val sendingProgress: Float = 0F,
|
val sendingProgress: Float = 0F,
|
||||||
val sending: Async<Unit> = Uninitialized,
|
val sending: Async<Unit> = Async.Uninitialized,
|
||||||
) : MavericksState {
|
) {
|
||||||
val submitEnabled =
|
val submitEnabled =
|
||||||
formState.description.length > 10 && sending !is Loading
|
formState.description.length > 10 && sending !is Async.Loading
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
data class BugReportFormState(
|
data class BugReportFormState(
|
||||||
val description: String,
|
val description: String,
|
||||||
) {
|
val sendLogs: Boolean,
|
||||||
|
val sendCrashLogs: Boolean,
|
||||||
|
val canContact: Boolean,
|
||||||
|
val sendScreenshot: Boolean
|
||||||
|
|
||||||
|
): Parcelable {
|
||||||
companion object {
|
companion object {
|
||||||
val Default = BugReportFormState("")
|
val Default = BugReportFormState(
|
||||||
|
description = "",
|
||||||
|
sendLogs = true,
|
||||||
|
sendCrashLogs = true,
|
||||||
|
canContact = false,
|
||||||
|
sendScreenshot = false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -37,6 +37,7 @@ import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|
@ -50,48 +51,17 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import com.airbnb.mvrx.Fail
|
import io.element.android.x.architecture.Async
|
||||||
import com.airbnb.mvrx.Loading
|
|
||||||
import com.airbnb.mvrx.Success
|
|
||||||
import com.airbnb.mvrx.Uninitialized
|
|
||||||
import com.airbnb.mvrx.compose.collectAsState
|
|
||||||
import com.airbnb.mvrx.compose.mavericksViewModel
|
|
||||||
import io.element.android.x.core.compose.LogCompositions
|
import io.element.android.x.core.compose.LogCompositions
|
||||||
|
import io.element.android.x.core.compose.textFieldState
|
||||||
import io.element.android.x.designsystem.ElementXTheme
|
import io.element.android.x.designsystem.ElementXTheme
|
||||||
import io.element.android.x.designsystem.components.LabelledCheckbox
|
import io.element.android.x.designsystem.components.LabelledCheckbox
|
||||||
import io.element.android.x.designsystem.components.dialogs.ErrorDialog
|
import io.element.android.x.designsystem.components.dialogs.ErrorDialog
|
||||||
import io.element.android.x.element.resources.R as ElementR
|
import io.element.android.x.element.resources.R as ElementR
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BugReportScreen(
|
fun BugReportView(
|
||||||
viewModel: BugReportViewModel = mavericksViewModel(),
|
state: BugReportState,
|
||||||
onDone: () -> Unit = { },
|
|
||||||
) {
|
|
||||||
val state: BugReportViewState by viewModel.collectAsState()
|
|
||||||
val formState: BugReportFormState by viewModel.formState
|
|
||||||
LogCompositions(tag = "Rageshake", msg = "Root")
|
|
||||||
if (state.sending is Success) {
|
|
||||||
onDone()
|
|
||||||
}
|
|
||||||
BugReportContent(
|
|
||||||
state = state,
|
|
||||||
formState = formState,
|
|
||||||
onDescriptionChanged = viewModel::onSetDescription,
|
|
||||||
onSetSendLog = viewModel::onSetSendLog,
|
|
||||||
onSetSendCrashLog = viewModel::onSetSendCrashLog,
|
|
||||||
onSetCanContact = viewModel::onSetCanContact,
|
|
||||||
onSetSendScreenshot = viewModel::onSetSendScreenshot,
|
|
||||||
onSubmit = viewModel::onSubmit,
|
|
||||||
onFailureDialogClosed = viewModel::onFailureDialogClosed,
|
|
||||||
onDone = onDone,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun BugReportContent(
|
|
||||||
state: BugReportViewState,
|
|
||||||
formState: BugReportFormState,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onDescriptionChanged: (String) -> Unit = {},
|
onDescriptionChanged: (String) -> Unit = {},
|
||||||
onSetSendLog: (Boolean) -> Unit = {},
|
onSetSendLog: (Boolean) -> Unit = {},
|
||||||
|
|
@ -102,6 +72,10 @@ fun BugReportContent(
|
||||||
onFailureDialogClosed: () -> Unit = { },
|
onFailureDialogClosed: () -> Unit = { },
|
||||||
onDone: () -> Unit = { },
|
onDone: () -> Unit = { },
|
||||||
) {
|
) {
|
||||||
|
LogCompositions(tag = "Rageshake", msg = "Root")
|
||||||
|
if (state.sending is Async.Success) {
|
||||||
|
onDone()
|
||||||
|
}
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
color = MaterialTheme.colorScheme.background,
|
color = MaterialTheme.colorScheme.background,
|
||||||
|
|
@ -120,8 +94,8 @@ fun BugReportContent(
|
||||||
)
|
)
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
) {
|
) {
|
||||||
val isError = state.sending is Fail
|
val isError = state.sending is Async.Failure
|
||||||
val isFormEnabled = state.sending !is Loading
|
val isFormEnabled = state.sending !is Async.Loading
|
||||||
// Title
|
// Title
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = ElementR.string.send_bug_report),
|
text = stringResource(id = ElementR.string.send_bug_report),
|
||||||
|
|
@ -140,11 +114,12 @@ fun BugReportContent(
|
||||||
.padding(horizontal = 16.dp, vertical = 16.dp),
|
.padding(horizontal = 16.dp, vertical = 16.dp),
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
)
|
)
|
||||||
|
var descriptionFieldState by textFieldState(stateValue = state.formState.description)
|
||||||
Column(
|
Column(
|
||||||
// modifier = Modifier.weight(1f),
|
// modifier = Modifier.weight(1f),
|
||||||
) {
|
) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = formState.description,
|
value = descriptionFieldState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(top = 16.dp),
|
.padding(top = 16.dp),
|
||||||
|
|
@ -155,7 +130,10 @@ fun BugReportContent(
|
||||||
supportingText = {
|
supportingText = {
|
||||||
Text(text = stringResource(id = ElementR.string.send_bug_report_description_in_english))
|
Text(text = stringResource(id = ElementR.string.send_bug_report_description_in_english))
|
||||||
},
|
},
|
||||||
onValueChange = onDescriptionChanged,
|
onValueChange = {
|
||||||
|
descriptionFieldState = it
|
||||||
|
onDescriptionChanged(it)
|
||||||
|
},
|
||||||
keyboardOptions = KeyboardOptions(
|
keyboardOptions = KeyboardOptions(
|
||||||
keyboardType = KeyboardType.Text,
|
keyboardType = KeyboardType.Text,
|
||||||
imeAction = ImeAction.Next
|
imeAction = ImeAction.Next
|
||||||
|
|
@ -164,33 +142,33 @@ fun BugReportContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
LabelledCheckbox(
|
LabelledCheckbox(
|
||||||
checked = state.sendLogs,
|
checked = state.formState.sendLogs,
|
||||||
onCheckedChange = onSetSendLog,
|
onCheckedChange = onSetSendLog,
|
||||||
enabled = isFormEnabled,
|
enabled = isFormEnabled,
|
||||||
text = stringResource(id = ElementR.string.send_bug_report_include_logs)
|
text = stringResource(id = ElementR.string.send_bug_report_include_logs)
|
||||||
)
|
)
|
||||||
if (state.hasCrashLogs) {
|
if (state.hasCrashLogs) {
|
||||||
LabelledCheckbox(
|
LabelledCheckbox(
|
||||||
checked = state.sendCrashLogs,
|
checked = state.formState.sendCrashLogs,
|
||||||
onCheckedChange = onSetSendCrashLog,
|
onCheckedChange = onSetSendCrashLog,
|
||||||
enabled = isFormEnabled,
|
enabled = isFormEnabled,
|
||||||
text = stringResource(id = ElementR.string.send_bug_report_include_crash_logs)
|
text = stringResource(id = ElementR.string.send_bug_report_include_crash_logs)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
LabelledCheckbox(
|
LabelledCheckbox(
|
||||||
checked = state.canContact,
|
checked = state.formState.canContact,
|
||||||
onCheckedChange = onSetCanContact,
|
onCheckedChange = onSetCanContact,
|
||||||
enabled = isFormEnabled,
|
enabled = isFormEnabled,
|
||||||
text = stringResource(id = ElementR.string.you_may_contact_me)
|
text = stringResource(id = ElementR.string.you_may_contact_me)
|
||||||
)
|
)
|
||||||
if (state.screenshotUri != null) {
|
if (state.screenshotUri != null) {
|
||||||
LabelledCheckbox(
|
LabelledCheckbox(
|
||||||
checked = state.sendScreenshot,
|
checked = state.formState.sendScreenshot,
|
||||||
onCheckedChange = onSetSendScreenshot,
|
onCheckedChange = onSetSendScreenshot,
|
||||||
enabled = isFormEnabled,
|
enabled = isFormEnabled,
|
||||||
text = stringResource(id = ElementR.string.send_bug_report_include_screenshot)
|
text = stringResource(id = ElementR.string.send_bug_report_include_screenshot)
|
||||||
)
|
)
|
||||||
if (state.sendScreenshot) {
|
if (state.formState.sendScreenshot) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
|
|
@ -219,18 +197,18 @@ fun BugReportContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
when (state.sending) {
|
when (state.sending) {
|
||||||
Uninitialized -> Unit
|
Async.Uninitialized -> Unit
|
||||||
is Loading -> {
|
is Async.Loading -> {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
progress = state.sendingProgress,
|
progress = state.sendingProgress,
|
||||||
modifier = Modifier.align(Alignment.Center)
|
modifier = Modifier.align(Alignment.Center)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is Fail -> ErrorDialog(
|
is Async.Failure -> ErrorDialog(
|
||||||
content = state.sending.error.toString(),
|
content = state.sending.error.toString(),
|
||||||
onDismiss = onFailureDialogClosed,
|
onDismiss = onFailureDialogClosed,
|
||||||
)
|
)
|
||||||
is Success -> onDone()
|
is Async.Success -> onDone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -240,9 +218,8 @@ fun BugReportContent(
|
||||||
@Preview
|
@Preview
|
||||||
fun BugReportContentPreview() {
|
fun BugReportContentPreview() {
|
||||||
ElementXTheme(darkTheme = false) {
|
ElementXTheme(darkTheme = false) {
|
||||||
BugReportContent(
|
BugReportView(
|
||||||
state = BugReportViewState(),
|
state = BugReportState(),
|
||||||
formState = BugReportFormState.Default
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,175 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2022 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 io.element.android.x.features.rageshake.bugreport
|
|
||||||
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.snapshotFlow
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import com.airbnb.mvrx.Fail
|
|
||||||
import com.airbnb.mvrx.Loading
|
|
||||||
import com.airbnb.mvrx.MavericksViewModel
|
|
||||||
import com.airbnb.mvrx.MavericksViewModelFactory
|
|
||||||
import com.airbnb.mvrx.Success
|
|
||||||
import com.airbnb.mvrx.Uninitialized
|
|
||||||
import dagger.assisted.Assisted
|
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import io.element.android.x.anvilannotations.ContributesViewModel
|
|
||||||
import io.element.android.x.architecture.viewmodel.daggerMavericksViewModelFactory
|
|
||||||
import io.element.android.x.di.AppScope
|
|
||||||
import io.element.android.x.features.rageshake.crash.CrashDataStore
|
|
||||||
import io.element.android.x.features.rageshake.logs.VectorFileLogger
|
|
||||||
import io.element.android.x.features.rageshake.reporter.BugReporter
|
|
||||||
import io.element.android.x.features.rageshake.reporter.ReportType
|
|
||||||
import io.element.android.x.features.rageshake.screenshot.ScreenshotHolder
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@ContributesViewModel(AppScope::class)
|
|
||||||
class BugReportViewModel @AssistedInject constructor(
|
|
||||||
@Assisted initialState: BugReportViewState,
|
|
||||||
private val bugReporter: BugReporter,
|
|
||||||
private val crashDataStore: CrashDataStore,
|
|
||||||
private val screenshotHolder: ScreenshotHolder,
|
|
||||||
private val appCoroutineScope: CoroutineScope
|
|
||||||
) :
|
|
||||||
MavericksViewModel<BugReportViewState>(initialState) {
|
|
||||||
|
|
||||||
companion object :
|
|
||||||
MavericksViewModelFactory<BugReportViewModel, BugReportViewState> by daggerMavericksViewModelFactory()
|
|
||||||
|
|
||||||
var formState = mutableStateOf(BugReportFormState.Default)
|
|
||||||
private set
|
|
||||||
|
|
||||||
init {
|
|
||||||
snapshotFlow { formState.value }
|
|
||||||
.onEach {
|
|
||||||
setState { copy(formState = it) }
|
|
||||||
}.launchIn(viewModelScope)
|
|
||||||
observerCrashDataStore()
|
|
||||||
setState {
|
|
||||||
copy(
|
|
||||||
screenshotUri = screenshotHolder.getFile()?.toUri()?.toString()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun observerCrashDataStore() {
|
|
||||||
viewModelScope.launch {
|
|
||||||
crashDataStore.crashInfo().collect {
|
|
||||||
setState {
|
|
||||||
copy(
|
|
||||||
hasCrashLogs = it.isNotEmpty()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val listener: BugReporter.IMXBugReportListener = object : BugReporter.IMXBugReportListener {
|
|
||||||
override fun onUploadCancelled() {
|
|
||||||
setState {
|
|
||||||
copy(
|
|
||||||
sendingProgress = 0F,
|
|
||||||
sending = Uninitialized
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUploadFailed(reason: String?) {
|
|
||||||
setState {
|
|
||||||
copy(
|
|
||||||
sendingProgress = 0F,
|
|
||||||
sending = Fail(Exception(reason))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onProgress(progress: Int) {
|
|
||||||
setState {
|
|
||||||
copy(
|
|
||||||
sendingProgress = progress.toFloat() / 100,
|
|
||||||
sending = Loading()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUploadSucceed(reportUrl: String?) {
|
|
||||||
setState {
|
|
||||||
copy(
|
|
||||||
sendingProgress = 1F,
|
|
||||||
sending = Success(Unit)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCleared() {
|
|
||||||
// Use appCoroutineScope because we don't want this coroutine to be cancelled
|
|
||||||
appCoroutineScope.launch(Dispatchers.IO) {
|
|
||||||
screenshotHolder.reset()
|
|
||||||
crashDataStore.reset()
|
|
||||||
VectorFileLogger.getFromTimber().reset()
|
|
||||||
}
|
|
||||||
super.onCleared()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onSubmit() {
|
|
||||||
setState {
|
|
||||||
copy(
|
|
||||||
sendingProgress = 0F,
|
|
||||||
sending = Loading()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
withState { state ->
|
|
||||||
bugReporter.sendBugReport(
|
|
||||||
coroutineScope = viewModelScope,
|
|
||||||
reportType = ReportType.BUG_REPORT,
|
|
||||||
withDevicesLogs = state.sendLogs,
|
|
||||||
withCrashLogs = state.hasCrashLogs && state.sendCrashLogs,
|
|
||||||
withKeyRequestHistory = false,
|
|
||||||
withScreenshot = state.sendScreenshot,
|
|
||||||
theBugDescription = state.formState.description,
|
|
||||||
serverVersion = "",
|
|
||||||
canContact = state.canContact,
|
|
||||||
customFields = emptyMap(),
|
|
||||||
listener = listener
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onFailureDialogClosed() {
|
|
||||||
setState {
|
|
||||||
copy(
|
|
||||||
sendingProgress = 0F,
|
|
||||||
sending = Uninitialized
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onSetDescription(str: String) {
|
|
||||||
formState.value = formState.value.copy(description = str)
|
|
||||||
setState { copy(sending = Uninitialized) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onSetSendLog(value: Boolean) = setState { copy(sendLogs = value) }
|
|
||||||
fun onSetSendCrashLog(value: Boolean) = setState { copy(sendCrashLogs = value) }
|
|
||||||
fun onSetCanContact(value: Boolean) = setState { copy(canContact = value) }
|
|
||||||
fun onSetSendScreenshot(value: Boolean) = setState { copy(sendScreenshot = value) }
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package io.element.android.x.features.rageshake.crash.ui
|
||||||
|
|
||||||
|
sealed interface CrashDetectionEvents {
|
||||||
|
object ResetAll : CrashDetectionEvents
|
||||||
|
object ResetAppHasCrashed : CrashDetectionEvents
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package io.element.android.x.features.rageshake.crash.ui
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import io.element.android.x.architecture.Presenter
|
||||||
|
import io.element.android.x.features.rageshake.crash.CrashDataStore
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class CrashDetectionPresenter @Inject constructor(private val crashDataStore: CrashDataStore) : Presenter<CrashDetectionState, CrashDetectionEvents> {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun present(events: Flow<CrashDetectionEvents>): CrashDetectionState {
|
||||||
|
val crashDetected = crashDataStore.appHasCrashed().collectAsState(initial = false)
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
events.collect { event ->
|
||||||
|
when (event) {
|
||||||
|
CrashDetectionEvents.ResetAll -> resetAll()
|
||||||
|
CrashDetectionEvents.ResetAppHasCrashed -> resetAppHasCrashed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CrashDetectionState(
|
||||||
|
crashDetected = crashDetected.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.resetAppHasCrashed() = launch {
|
||||||
|
crashDataStore.resetAppHasCrashed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun CoroutineScope.resetAll() = launch {
|
||||||
|
crashDataStore.reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,40 +17,33 @@
|
||||||
package io.element.android.x.features.rageshake.crash.ui
|
package io.element.android.x.features.rageshake.crash.ui
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import com.airbnb.mvrx.compose.collectAsState
|
|
||||||
import com.airbnb.mvrx.compose.mavericksViewModel
|
|
||||||
import io.element.android.x.core.compose.LogCompositions
|
import io.element.android.x.core.compose.LogCompositions
|
||||||
import io.element.android.x.designsystem.ElementXTheme
|
import io.element.android.x.designsystem.ElementXTheme
|
||||||
import io.element.android.x.designsystem.components.dialogs.ConfirmationDialog
|
import io.element.android.x.designsystem.components.dialogs.ConfirmationDialog
|
||||||
import io.element.android.x.element.resources.R as ElementR
|
import io.element.android.x.element.resources.R as ElementR
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CrashDetectionScreen(
|
fun CrashDetectionView(
|
||||||
viewModel: CrashDetectionViewModel = mavericksViewModel(),
|
state: CrashDetectionState,
|
||||||
onOpenBugReport: () -> Unit = { },
|
onOpenBugReport: () -> Unit = { },
|
||||||
|
onPopupDismissed: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val state: CrashDetectionViewState by viewModel.collectAsState()
|
|
||||||
LogCompositions(tag = "Crash", msg = "CrashDetectionScreen")
|
LogCompositions(tag = "Crash", msg = "CrashDetectionScreen")
|
||||||
|
|
||||||
if (state.crashDetected) {
|
if (state.crashDetected) {
|
||||||
CrashDetectionContent(
|
CrashDetectionContent(
|
||||||
state,
|
state,
|
||||||
onYesClicked = {
|
onYesClicked = onOpenBugReport,
|
||||||
viewModel.onYes()
|
onNoClicked = onPopupDismissed,
|
||||||
onOpenBugReport()
|
onDismiss = onPopupDismissed,
|
||||||
},
|
|
||||||
onNoClicked = viewModel::onPopupDismissed,
|
|
||||||
onDismiss = viewModel::onPopupDismissed,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CrashDetectionContent(
|
fun CrashDetectionContent(
|
||||||
state: CrashDetectionViewState,
|
state: CrashDetectionState,
|
||||||
onNoClicked: () -> Unit = { },
|
onNoClicked: () -> Unit = { },
|
||||||
onYesClicked: () -> Unit = { },
|
onYesClicked: () -> Unit = { },
|
||||||
onDismiss: () -> Unit = { },
|
onDismiss: () -> Unit = { },
|
||||||
|
|
@ -71,7 +64,7 @@ fun CrashDetectionContent(
|
||||||
fun CrashDetectionContentPreview() {
|
fun CrashDetectionContentPreview() {
|
||||||
ElementXTheme {
|
ElementXTheme {
|
||||||
CrashDetectionContent(
|
CrashDetectionContent(
|
||||||
state = CrashDetectionViewState()
|
state = CrashDetectionState()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,6 @@
|
||||||
|
|
||||||
package io.element.android.x.features.rageshake.crash.ui
|
package io.element.android.x.features.rageshake.crash.ui
|
||||||
|
|
||||||
import com.airbnb.mvrx.MavericksState
|
data class CrashDetectionState(
|
||||||
|
|
||||||
data class CrashDetectionViewState(
|
|
||||||
val crashDetected: Boolean = false,
|
val crashDetected: Boolean = false,
|
||||||
) : MavericksState
|
)
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2022 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 io.element.android.x.features.rageshake.crash.ui
|
|
||||||
|
|
||||||
import com.airbnb.mvrx.MavericksViewModel
|
|
||||||
import com.airbnb.mvrx.MavericksViewModelFactory
|
|
||||||
import dagger.assisted.Assisted
|
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import io.element.android.x.anvilannotations.ContributesViewModel
|
|
||||||
import io.element.android.x.architecture.viewmodel.daggerMavericksViewModelFactory
|
|
||||||
import io.element.android.x.di.AppScope
|
|
||||||
import io.element.android.x.features.rageshake.crash.CrashDataStore
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@ContributesViewModel(AppScope::class)
|
|
||||||
class CrashDetectionViewModel @AssistedInject constructor(
|
|
||||||
@Assisted initialState: CrashDetectionViewState,
|
|
||||||
private val crashDataStore: CrashDataStore,
|
|
||||||
) : MavericksViewModel<CrashDetectionViewState>(initialState) {
|
|
||||||
|
|
||||||
companion object :
|
|
||||||
MavericksViewModelFactory<CrashDetectionViewModel, CrashDetectionViewState> by daggerMavericksViewModelFactory()
|
|
||||||
|
|
||||||
init {
|
|
||||||
observeDataStore()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun observeDataStore() {
|
|
||||||
viewModelScope.launch {
|
|
||||||
crashDataStore.appHasCrashed().collect { appHasCrashed ->
|
|
||||||
setState {
|
|
||||||
copy(
|
|
||||||
crashDetected = appHasCrashed
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onYes() {
|
|
||||||
viewModelScope.launch {
|
|
||||||
crashDataStore.resetAppHasCrashed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onPopupDismissed() {
|
|
||||||
viewModelScope.launch {
|
|
||||||
crashDataStore.reset()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue