diff options
Diffstat (limited to 'android')
5 files changed, 163 insertions, 32 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ReportProblemScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ReportProblemScreen.kt index 007b7bacf2..6f030f37fe 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ReportProblemScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ReportProblemScreen.kt @@ -113,18 +113,29 @@ fun ReportProblemScreen( ) { var email by rememberSaveable { mutableStateOf("") } var description by rememberSaveable { mutableStateOf("") } + + // Dialog to show sending states + if (uiState.sendingState != null) { + ShowReportProblemStateDialog( + uiState.sendingState, + onDismiss = onClearSendResult, + onClearForm = { + email = "" + description = "" + }, + retry = { onSendReport(email, description) } + ) + } + + // Dialog to show confirm if no email was added + if (uiState.showConfirmNoEmail) { + ReportProblemNoEmailDialog( + onDismiss = onDismissNoEmailDialog, + onConfirm = { onSendReport(email, description) } + ) + } + Surface(color = MaterialTheme.colorScheme.background) { - if (uiState.sendingState != null) { - ShowReportProblemStateDialog( - uiState.sendingState, - onDismiss = onClearSendResult, - onClearForm = { - email = "" - description = "" - }, - retry = { onSendReport(email, description) } - ) - } Column( modifier = Modifier.padding( @@ -134,6 +145,7 @@ fun ReportProblemScreen( verticalArrangement = Arrangement.spacedBy(Dimens.mediumPadding) ) { Text(text = stringResource(id = R.string.problem_report_description)) + TextField( modifier = Modifier.fillMaxWidth(), value = email, @@ -143,6 +155,7 @@ fun ReportProblemScreen( placeholder = { Text(text = stringResource(id = R.string.user_email_hint)) }, colors = mullvadWhiteTextFieldColors() ) + TextField( modifier = Modifier.fillMaxWidth().weight(1f), value = description, @@ -160,6 +173,7 @@ fun ReportProblemScreen( onClick = onNavigateToViewLogs, text = stringResource(id = R.string.view_logs) ) + ActionButton( colors = ButtonDefaults.buttonColors( @@ -170,13 +184,6 @@ fun ReportProblemScreen( isEnabled = description.isNotEmpty(), text = stringResource(id = R.string.send) ) - - if (uiState.showConfirmNoEmail) { - ReportProblemNoEmailDialog( - onDismiss = onDismissNoEmailDialog, - onConfirm = { onSendReport(email, description) } - ) - } } } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/MullvadProblemReport.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/MullvadProblemReport.kt index ca9f4c7e23..67eaeca48d 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/MullvadProblemReport.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/MullvadProblemReport.kt @@ -2,6 +2,7 @@ package net.mullvad.mullvadvpn.dataproxy import android.content.Context import java.io.File +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -18,9 +19,10 @@ sealed interface SendProblemReportResult { } } -data class UserReport(val email: String, val message: String) +data class UserReport(val email: String?, val message: String) + +class MullvadProblemReport(context: Context, val dispatcher: CoroutineDispatcher = Dispatchers.IO) { -class MullvadProblemReport(context: Context) { private val cacheDirectory = File(context.cacheDir.toURI()) private val logDirectory = File(context.filesDir.toURI()) private val logsPath = File(logDirectory, PROBLEM_REPORT_LOGS_FILE) @@ -30,7 +32,7 @@ class MullvadProblemReport(context: Context) { } suspend fun collectLogs(): Boolean = - withContext(Dispatchers.IO) { + withContext(dispatcher) { // Delete any old report deleteLogs() @@ -44,9 +46,9 @@ class MullvadProblemReport(context: Context) { } val sentSuccessfully = - withContext(Dispatchers.IO) { + withContext(dispatcher) { sendProblemReport( - userReport.email, + userReport.email ?: "", userReport.message, logsPath.absolutePath, cacheDirectory.absolutePath @@ -73,8 +75,7 @@ class MullvadProblemReport(context: Context) { } } - private fun logsExists() = - logsPath.exists() + private fun logsExists() = logsPath.exists() fun deleteLogs() { logsPath.delete() diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ReportProblemViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ReportProblemViewModel.kt index 75b65eaca6..5b69ec9317 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ReportProblemViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ReportProblemViewModel.kt @@ -38,7 +38,8 @@ class ReportProblemViewModel(private val mullvadProblemReporter: MullvadProblemR ) { viewModelScope.launch { val userEmail = email.trim() - if (shouldShowConfirmNoEmail(userEmail)) { + val nullableEmail = if (email.isEmpty()) null else userEmail + if (shouldShowConfirmNoEmail(nullableEmail)) { _uiState.update { it.copy(showConfirmNoEmail = true) } } else { _uiState.update { @@ -46,10 +47,14 @@ class ReportProblemViewModel(private val mullvadProblemReporter: MullvadProblemR } // Ensure we show loading for at least 500 ms - val deferredResult = async { mullvadProblemReporter.sendReport(UserReport(userEmail, description)) } + val deferredResult = async { + mullvadProblemReporter.sendReport(UserReport(nullableEmail, description)) + } delay(MINIMUM_LOADING_SPINNER_TIME_MILLIS) - _uiState.update { it.copy(sendingState = deferredResult.await().toUiResult(userEmail)) } + _uiState.update { + it.copy(sendingState = deferredResult.await().toUiResult(nullableEmail)) + } } } } @@ -62,12 +67,12 @@ class ReportProblemViewModel(private val mullvadProblemReporter: MullvadProblemR _uiState.update { it.copy(showConfirmNoEmail = false) } } - private fun shouldShowConfirmNoEmail(userEmail: String): Boolean = - userEmail.isEmpty() && + private fun shouldShowConfirmNoEmail(userEmail: String?): Boolean = + userEmail.isNullOrEmpty() && !uiState.value.showConfirmNoEmail && uiState.value.sendingState !is SendingReportUiState - private fun SendProblemReportResult.toUiResult(email: String): SendingReportUiState = + private fun SendProblemReportResult.toUiResult(email: String?): SendingReportUiState = when (this) { is SendProblemReportResult.Error -> SendingReportUiState.Error(this) SendProblemReportResult.Success -> SendingReportUiState.Success(email) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ViewLogsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ViewLogsViewModel.kt index cff6bf6043..8ceae43289 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ViewLogsViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ViewLogsViewModel.kt @@ -20,7 +20,8 @@ class ViewLogsViewModel(private val mullvadProblemReporter: MullvadProblemReport viewModelScope.launch { // Loading this much text takes a while, so we show a loading indicator until the // fragment transitions is done. I'd very much prefer to use LazyColumn in the view - // which would make the loading way faster but then the SelectionContainer is broken. + // which would make the loading way faster but then the SelectionContainer is broken and + // text would not be copyable. delay(500) _uiState.update { it.copy( diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ReportProblemModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ReportProblemModelTest.kt new file mode 100644 index 0000000000..4f91189d0a --- /dev/null +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ReportProblemModelTest.kt @@ -0,0 +1,117 @@ +package net.mullvad.mullvadvpn.viewmodel + +import androidx.lifecycle.viewModelScope +import app.cash.turbine.test +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.impl.annotations.MockK +import kotlin.test.assertEquals +import kotlinx.coroutines.cancel +import kotlinx.coroutines.test.runTest +import net.mullvad.mullvadvpn.dataproxy.MullvadProblemReport +import net.mullvad.mullvadvpn.dataproxy.SendProblemReportResult +import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class ReportProblemModelTest { + @get:Rule val testCoroutineRule = TestCoroutineRule() + + @MockK private lateinit var mockMullvadProblemReport: MullvadProblemReport + + private lateinit var viewModel: ReportProblemViewModel + + @Before + fun setUp() { + MockKAnnotations.init(this) + coEvery { mockMullvadProblemReport.collectLogs() } returns true + viewModel = ReportProblemViewModel(mockMullvadProblemReport) + } + + @After + fun tearDown() { + viewModel.viewModelScope.coroutineContext.cancel() + } + + @Test + fun sendReportFailedToCollectLogs() = runTest { + // Arrange + coEvery { mockMullvadProblemReport.sendReport(any()) } returns + SendProblemReportResult.Error.CollectLog + val email = "my@email.com" + + // Act, Assert + viewModel.uiState.test { + assertEquals(null, awaitItem().sendingState) + viewModel.sendReport(email, "My description") + assertEquals(SendingReportUiState.Sending, awaitItem().sendingState) + assertEquals( + SendingReportUiState.Error(SendProblemReportResult.Error.CollectLog), + awaitItem().sendingState + ) + } + } + + @Test + fun sendReportFailedToSendReport() = runTest { + // Arrange + coEvery { mockMullvadProblemReport.sendReport(any()) } returns + SendProblemReportResult.Error.SendReport + val email = "my@email.com" + + // Act, Assert + viewModel.uiState.test { + assertEquals(null, awaitItem().sendingState) + viewModel.sendReport(email, "My description") + assertEquals(SendingReportUiState.Sending, awaitItem().sendingState) + assertEquals( + SendingReportUiState.Error(SendProblemReportResult.Error.SendReport), + awaitItem().sendingState + ) + } + } + + @Test + fun sendReportWithoutEmailSuccessfully() = runTest { + // Arrange + coEvery { mockMullvadProblemReport.sendReport(any()) } returns + SendProblemReportResult.Success + val email = "" + + // Act, Assert + viewModel.uiState.test { + assertEquals(ReportProblemUiState(false, null), awaitItem()) + viewModel.sendReport(email, "My description") + assertEquals(ReportProblemUiState(true, null), awaitItem()) + viewModel.sendReport(email, "My description") + assertEquals(ReportProblemUiState(false, SendingReportUiState.Sending), awaitItem()) + assertEquals( + ReportProblemUiState(false, SendingReportUiState.Success(null)), + awaitItem() + ) + } + } + + @Test + fun sendReportSuccessfully() = runTest { + // Arrange + coEvery { mockMullvadProblemReport.collectLogs() } returns true + coEvery { mockMullvadProblemReport.sendReport(any()) } returns + SendProblemReportResult.Success + val email = "my@email.com" + + // Act, Assert + viewModel.uiState.test { + assertEquals(awaitItem(), ReportProblemUiState(false, null)) + viewModel.sendReport(email, "My description") + + assertEquals(awaitItem(), ReportProblemUiState(false, SendingReportUiState.Sending)) + assertEquals( + awaitItem(), + ReportProblemUiState(false, SendingReportUiState.Success(email)) + ) + } + } +} |
