ganfra
2 years ago
12 changed files with 242 additions and 329 deletions
@ -0,0 +1,11 @@
@@ -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 @@
@@ -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() |
||||
} |
||||
} |
@ -1,175 +0,0 @@
@@ -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 @@
@@ -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 @@
@@ -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() |
||||
} |
||||
} |
@ -1,65 +0,0 @@
@@ -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…
Reference in new issue